《挑战程序设计竞赛》笔记 初出茅庐之二

2.2 一直往前!贪心法

       贪心发就是遵循某种规则,不断贪心地选取当前最优策略的算法设计方法。

      

       1.硬币问题

       有1元,5元,10元,50元,100元,500元的硬币各C1,C5,C10,C50,C100,C500枚。现在要用这些硬币支付A元,最少需要多少枚硬币?嘉定本体至少存在一种支付方案。

       限制条件:

       0<=C1,C5,C10,C50,C100,C500 <= 10^9

       0<=A <= 10^9

       样例:

       输入:

       C1= 3,C5 = 2,C10 = 1,C50 = 3,C100 = 0,C500 = 2,A = 620

       输出:

       6(500元硬币1枚,50元硬币2枚,10元硬币1枚,5元硬币2枚,合计6枚)

      

       问题比较贴近生活,想象一下,可以得到如下的方法:

       首先尽可能用500元硬币

       剩余部分尽可能用100元硬币

       剩余部分尽可能用50元硬币

       剩余部分尽可能用10元硬币

       剩余部分尽可能用5元硬币

       剩余部分尽可能用1元硬币

       简言之,优先使用面值最大的硬币

      

       穷竭搜索和动态规划是从多种策略中选取最优解,而贪心算法则不痛,遵循某种规则,不断地选取当前最优策略。并且,如果问题能够用贪心算法求解,那么通常是非常高效的。

      

       解题程序如下:

#include <iostream>
using namespace std;
 
const int V[6] = {1, 5, 10, 50, 100, 500};    // 定义面值
int C[6] = {3, 2, 1, 3, 0, 2};                       // 各面值的数量
int A = 620;                                              // 要凑齐面值
 
int main()
{
   int ans = 0;
   for(int i = 5; i >= 0; i--)
    {
       int t = min( A/V[i], C[i]);            //尽可能多地使用大面值
       A -= t * V[i];
       ans += t;
    }
 
   cout << "ans: " << ans << endl;
   return 0;
}

       2.区间调度问题

       有n项工作,每项工作分别在Si时间开始,在Ti时间结束,对于每项工作,都可以选择参与与否,如果选择参与,那么自始至终都必须全程参与。此外,参与工作的时间段不能重叠。

       目标是参与尽可能多的工作,那么最多能参加多少项工作呢?

       限制条件:

       1<= N <= 100000

       1<= Si <= Ti <= 10^9

      

       样例:

       输入:

       N= 5, s = { 1, 2, 4, 6, 8},t = { 3, 5, 7, 9, 10};

       输出:

       3(选取工作1,3,5)

      

       本题采用贪心算法可以有如下的几种方法:

       A在可选的工作中,每次选取开始时间最早的工作。

       B在可选的工作中,每次选取结束时间最早的工作

       C在可选的工作中,每次都选取用时最短的工作

       只有算法B是正确的,其他的都可以找到对应的反例,或者说某些情况下,他们呢选取的工作并非最优。

#include <iostream>
 
using namespace std;
 
const int MAX_N = 5;
 
int S[MAX_N] = {1, 2, 4, 6, 8};
int T[MAX_N] = {3, 5, 7, 9, 10};
 
// 用于对工作排序的pair数组
pair<int, int> itv[MAX_N];
 
int main()
{
   // 对pair进行赋值,并且排序
   for(int i = 0; i < MAX_N; i++)
    {
       itv[i].first = T[i];
       itv[i].second = S[i];
    }
   sort(itv, itv + MAX_N);
 
   // 排序时按照最早结束时间排序,因此只需查找其最早开始时间满足条件即可
   int ans = 0, t = 0;
   for( int i = 0; i < MAX_N; i++)
    {
       if(t < itv[i].second)
       {
           ans++;
           t = itv[i].first;
       }
    }
 
   cout << "ans: " << ans << endl;
   return 0;
}

3. 字典序最小问题

       给定长度为N的字符串S,要构造一个长度为N的字符串T,起初,T是一个空串,随后反复进行下列任意操作:

       从S的头部删除一个字符,加入到T的尾部

       从S的尾部删除一个字符,加入到T的尾部

       目标是要构造字典序尽可能小的字符串T

                                        S=”CDB”  T = “ABC”

                                                      |

                                        ————————

                                 开头 |                   | 末尾

                   S=”DB” T=”ABCC”      S=”CD” T=”ABCB”

       限制条件:

       1<=N <= 2000

       字符串S只包含大写英文字母

      

       样例:

       输入:    N= 6  S = “ACDBCB”

       输出: ABCBCD

      

       从字典序的性质上看,无论T的末尾有多大,只要前面部分的较小就可以,所以可以使用贪心算法:

       不断取S的开头和末尾中较小的一个字符放到T的末尾

      

       完整算法:

       按照字典序比较S和S翻转后的字符串S’

       如果S较小,就从S的开头取出一个字符,追加到T的末尾

       如果S’ 较小,就从S’ 的开头取出一个字符,追加到T的末尾 (如果相同,则取哪个都可以)

#include <iostream>
using namespace std;
 
int N = 6;
char S[6] = {'A', 'C', 'D', 'B', 'C', 'B'};
 
int main()
{
   int a = 0, b = N - 1;
   while( a <= b)
    {
       bool left = false;
       for( int i = 0; a + i <= b; i++)
       {
           if(S[a+i] < S[b-i])
           {
                left = true;
                break;
           }
           else if(S[a+i] > S[b-i])
           {
                left = false;
                break;
           }
       }
 
       if(left) putchar(S[a++]);
       else putchar(S[b--]);
    }
   cout << endl;
   return 0;
}

       4.其他问题

       Saruman’sArmy

       直线上有N个点,点i的位置是Xi,从这N个点中选择若干个,给他们图上标记。对每一个点,其距离为R以内的区域里必须有带有标记的点(自己本身带有标记的点,可以认为与其距离为0的地方有一个带有标记的点)。在满足这个条件的情况下,希望能为尽可能少的点添加标记。请问要有多少点被添加标记。

      

       可知图中的三个黑色点,在R的范围内覆盖了所有的空心点。

       限制:

       1<= N <= 1000

       0<= R <= 1000

       0<= Xi <= 1000

      

       样例:

       输入:

       N= 6  R = 10

       X= { 1, 7, 15, 20, 30, 50}

      

       输出:

3(如上图所示)

      

       从最最左边的点开始考虑,对于这个点,到距离R内的区域内必须要有带有标记的点。带有标记的这个点,一定再此点的右侧

       从最左边的点开始,距离R以内的最远点,标记本点;然后先右继续找R距离内最远的点。从此点右边的一点开始,重复上述过程即可。

#include <iostream>
using namespace std;
 
int N = 6; int R = 10;
int X[6] = { 1, 7, 15, 20, 30, 50};
int main()
{
   sort(X, X + N);
   int i = 0, ans = 0;
   while( i < N)
    {
       // s是没有覆盖的最左边的点的位置
       int s = X[i++];
       // 一直向右,找到距离s的距离大于R的点
       while( i < N && X[i] <= s+R) i++;
 
       // p是新加上标记的点的位置
       int p = X[ i-1];
       // 一直向右前进直到距离p的距离大于R的点
       while(i < N && X[i] <= p+R) i++;
 
       ans++;
    }
 
   cout << "ans: " << ans << endl;
   return 0;
}

       FenceRepair:

       农夫约翰为了修理栅栏,要将一块很长的模板切割成N块。准备切成的木板长度为L1,L2,……,Ln,未切割的木板长度恰好为切割后木板长度的总和。每次切割木板时,需要的开销为木板的长度。例如长度为21的木板要切割为长度为5,8,8,的三块木板。长21的木板切割成为13和8的木板时,开销为21。再将长度为13的木板切割为长度5和8的板时,开销为13.于是合计开销为34.按照题目要求将木板切割的最小开销是多少?

       输入:

       N= 3  L= {8,5,8}

       输出:

       34(对应题目中的例子)

      

       由于木板的切割顺序不确定,自由度很高。这样可以用略微奇特的贪心法求解。

       切割的过程其实对应了一个二叉树,每次切割为两块,最终的叶子节点对应最终的木板的长度,开销的合计就是:木板的长度*节点的深度 的总和。

       最短的板与次短的板的节点应该是兄弟节点,最短板应当是深度最大的叶子节点之一。类似于霍夫曼树。

#include<iostream>
 
using namespace std;
 
typedef long long ll;
 
int N = 3;
int L[3] = {8, 5, 8};
 
void swap(int i, int j)
{  /*
   int tmp = L[i];
   L[i] = L[j];
   L[j] = tmp; */
   int tmp = i;
    i= j;
    j= tmp;
}
 
int main()
{
   ll ans = 0;
   while( N>1)
    {
       int mii1 = 0, mii2 = 1;
       if(L[mii1] > L[mii2]) swap(mii1, mii2); // 令两个值指向当前最小与次小
 
       for(int i = 2; i < N; i++)  // 找最小和次小的节点
       {
            if(L[i] < L[mii1])
           {
                mii2 = mii1;
                mii1 = i;
           }
           else if(L[i] < L[mii2])
           {
                mii2 = i;
           }
       }
 
       // 将两块板子合并
       int t = L[mii1] + L[mii2];
       ans += t;
              //由于先给mii1赋值,如果它正好是N-1单元的,mii2的会被重复
       if(mii1 == N-1) swap(mii1, mii2);  
       L[mii1] = t;
       L[mii2] = L[N-1];
       N--;
    }
 
   cout << "ans: " << ans << endl;
   return 0;
}

       霍夫曼编码是当码字长度为整数情况时最优的编码方案。

       By  Andy  @ 2013-08-28

你可能感兴趣的:(《挑战程序设计竞赛》笔记 初出茅庐之二)