第5章贪心算法 在求最优解问题的过程中,依据某种贪心标准,从问题的初始状态出发,直接去求每一步的最优解,通过若干次的贪心选择,最终得出整个问题的最优解,这种求解方法就是贪心算法。从贪心算法的定义可以看出,贪心算法并不是从整体上考虑问题,它所做出的选择只是在某种意义上的局部最优解,而由问题自身的特性决定了该题运用贪心算法可以得到最优解。贪心算法所做的选择可以依赖于以往所做过的选择,但既不依赖于将来的选择,也不依赖于子问题的解,因此贪心算法与其他算法相比具有一定的速度优势。如果一个问题可以同时用几种方法解决,贪心算法应该是最好的选择之一。 当一个问题具有最优子结构性质和贪心选择性质时,贪心算法通过一系列的选择来得到一个问题的解。它所做的每一个选择都是在当前状态下具有某种意义的最好选择,即贪心选择,并且每次贪心选择都能将问题化简为一个更小的与原问题具有相同形式的子问题。尽管贪心算法对许多问题不能总是产生整体最优解,但对诸如最短路径问题、最小生成树问题,以及哈夫曼编码问题等具有最优子结构和贪心选择性质的问题却可以获得整体最优解,而且所给出的算法一般比动态规划算法更加简单、直观和高效。 5.1活动安排问题 活动安排问题就是要在所给的活动集合中选出最大的相容活动子集合,是可以用贪心算法有效求解的很好例子。该问题要求高效地安排一系列争用某一公共资源的活动。贪心算法提供了一个简单、漂亮的方法使得尽可能多的活动能兼容地使用公共资源。 设有n个活动的集合E={1,2,…,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。每个活动i都有一个要求使用该资源的起始时间si和一个结束时间fi,且si<fi。如果选择了活动i,则它在半开时间区间[si,fi)内占用资源。若区间[si,fi)与区间[sj,fj)不相交,则称活动i与活动j是相容的。也就是说,当si≥fj或sj≥fi时,活动i与活动j相容。活动安排问题就是要在所给的活动集合中选出最大的相容活动子集合。 为了编程方便,我们把活动的起始时间和结束时间定义为结构体: struct action{ int s;   //起始时间 int f;   //结束时间 int index;   //活动的编号 }; 活动的集合E记为数组: action a[1000]; 然后按活动的结束时间升序排序: a[1].f≤a[2].f≤…≤a[n].f。排序比较因子: bool cmp(const action &a, const action &b) { if (a.f<=b.f) return true; return false; } 使用标准模板库函数排序(下标0未用): sort(a+1, a+n+1, cmp); 计算活动安排问题的贪心算法,如算法5.1所示。 算法5.1计算活动安排问题的贪心算法。 //形参数组b用来记录被选中的活动 void GreedySelector(int n, action a[], bool b[]) { b[1] = true;   //第1个活动是必选的 //记录最近一次加入集合b中的活动 int preEnd = 1; for(int i=2; i<=n; i++) if (a[i].s>=a[preEnd].f) { b[i] = true; preEnd = i; } } 算法实现源代码: actionArrangement.cpp。 活动i在集合b中,当且仅当b[i]的值为true。变量preEnd用来记录最近一次加入集合b中的活动。算法greedySelector首先选择活动1,并将preEnd初始化为1,然后依次检查活动i是否与当前已经选择的所有活动相容。若相容则将活动i加入已选择活动的集合b中,否则放弃,继续检查下一活动与集合b中活动的相容性。 例如有11个活动,其开始时间和结束时间如表51所示,并按结束时间升序排列。 表51样例数据(11个活动) i 1 2 3 4 5 6 7 8 9 10 11 a[i].s 1 3 0 5 3 5 6 8 8 2 12 a[i].f 4 5 6 7 8 9 10 11 12 13 14 b 1 0 0 1 0 0 0 1 0 0 1 由于输入的活动按完成时间升序排列,所以算法greedySelector每次总是选择具有最早完成时间的相容活动加入集合b中。直观上,按这种方法选择相容活动为未安排活动留下尽可能多的时间。该算法的贪心选择意义是使剩余的可安排时间段极大化,以便安排尽可能多的相容活动。 算法5.1的计算过程,如图51所示。 图51活动安排问题的几何意义 根据数组b的值,就可以输出选中的活动编号: for(int i=1; i<=n;i++) if(b[i]) printf("%d ",a[i].index); printf("\n"); 当输入的活动已按结束时间升序排列,算法只需O(n)的时间来安排n个活动,使最多的活动能相容地使用公共资源。如果所给出的活动未按升序排列,可用O(nlog2n)时间重排。 贪心算法并不总能求得问题的整体最优解。但对于活动安排问题,贪心算法greedySelector却总能求得整体最优解,即它最终所确定的相容活动集合b的规模最大。这个结论可以用数学归纳法证明,请参考文献[3]。 5.2贪心算法的理论基础 贪心算法是一种在每一步选择中都采取在当前状态下最好或最优的选择,从而希望得到结果是最好或最优的算法。贪心算法是一种能够得到某种度量意义下的最优解的分级处理方法,通过一系列的选择来得到一个问题的解,而它所做的每一次选择都是当前状态下某种意义的最好选择,即贪心选择。即希望通过问题的局部最优解来求出整个问题的最优解。这种算法是一种很简洁的方法,对许多问题它能产生整体最优解,但不能保证总是有效,因为它不是对所有问题都能得到整体最优解,只能说其解必然是最优解的很好近似值。 利用贪心算法解题,需要解决两个问题: (1) 该题是否适合用贪心算法求解; (2) 如何选择贪心标准,以得到问题的最优/较优解。 5.2.1贪心选择性质 贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。 (1) 在动态规划算法中,每步所做的选择往往依赖于相关子问题的解,因而只有在解出相关子问题后才能做出选择。 (2) 在贪心算法中,仅在当前状态下做出最好选择,即局部最优选择。然后再解出这个选择后产生的相应的子问题。贪心算法所做的贪心选择可以依赖于以往所做的选择,但不依赖于将来所做的选择,也不依赖于子问题的解。 正是由于这种差别,动态规划算法通常以自底向上的方式解各个子问题,而贪心算法则通常以自顶向下的方式进行,以迭代的方式做出相继的贪心选择,每做一次贪心选择就将所求问题简化为规模更小的子问题。 对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所做的贪心选择最终导致问题的整体最优解。首先考察问题的一个整体最优解,并证明可修改这个最优解,使其以贪心选择开始。做了贪心选择后,原问题简化为规模更小的类似子问题。然后,用数学归纳法证明,通过每一步做贪心选择,最终可得到问题的整体最优解。证明贪心选择后的问题简化为规模更小的类似子问题的关键在于利用该问题的最优子结构性质。 5.2.2最优子结构性质 当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。运用贪心算法在每一次转化时都取得了最优解。问题的最优子结构性质是该问题可用贪心算法或动态规划算法求解的关键特征。贪心算法的每一次操作都对结果产生直接影响,而动态规划算法则不是。贪心算法对每个子问题的解决方案都做出选择,不能回退; 动态规划算法则会根据以前的选择结果对当前进行选择,有回退功能。动态规划算法主要运用于二维或三维问题,而贪心算法一般是一维问题。 5.2.3贪心算法的求解过程 使用贪心算法求解问题应该考虑如下几个方面: (1) 候选集合A: 为了构造问题的解决方案,有一个候选集合A作为问题的可能解,即问题的最终解均取自于候选集合A。 (2) 解集合S: 随着贪心选择的进行,解集合S不断扩展,直到构成满足问题的完整解。 (3) 解决函数solution: 检查解集合S是否构成问题的完整解。 (4) 选择函数select: 即贪心策略,这是贪心算法的关键,它指出哪个候选对象最有希望构成问题的解,选择函数通常和目标函数有关。 (5) 可行函数feasible: 检查解集合中加入一个候选对象是否可行,即解集合扩展后是否满足约束条件。 贪心算法的一般流程,如算法5.2所示。 算法5.2贪心算法的一般流程。 //A是问题的输入集合即候选集合 Greedy(A) { S={ };   //初始解集合为空集 while (not solution(S))   //集合S没有构成问题的一个解 { x = select(A);   //在候选集合A中做贪心选择 if feasible(S, x)   //判断集合S中加入x后的解是否可行 S = S+{x}; A = A-{x}; } return S; } 5.3背包问题 给定一个载重量为M的背包,考虑n个物品,其中第i个物品的质量为wi,价值为vi(1≤i≤n),要求把物品装满背包,且使背包内的物品价值最大。有两类背包问题(根据物品是否可以分割),如果物品不可以分割,称为01背包问题(见4.5节); 如果物品可以分割,则称为背包问题,就是本节讨论的内容。 假设xi是物品i装入背包的部分(0≤xi≤1),当xi=0时表示物品i没有被装入背包; 当xi=1时表示物品i被全部装入背包。根据问题的要求,该问题可形式化描述为: max∑ni=1pixi,s.t. ∑ni=1wixi=M 其中,0≤xi≤1。 假设背包的容量是50,有3个物品,如表52所示。 表52背包问题的3个物品 n 1 2 3 质量 10 20 30 价值 60 100 120 性价比 6 5 4 有3种方法来选取物品: (1) 当作01背包问题,采用动态规划算法,如图52(b)所示,获得最优值220; (2) 当作01背包问题,采用贪心算法,按性价比从高到低的顺序选取物品,获得最优值160,如图52(c)所示。由于物品不可分割,剩下的空间20,没有相应的物品可以装入而白白浪费。 图52背包问题与01背包问题的比较 (3) 当作背包问题,采用贪心算法,按性价比从高到低的顺序选取物品,获得最优值240,如图52(d)所示。由于物品可以分割,剩下的空间20,装入物品3的一部分,而获得了更好的性能。 从上面的分析可知,贪心算法对01背包问题不能得到最优解。当物品按性价比递减排序后,应用贪心算法可以得到最优解。该结论的证明,可以阅读参考文献[9]。 为了方便计算,建立如下的数据结构,表示物品的参数: struct bag{ int w;   //物品的质量 int v;   //物品的价值 double c;   //性价比 }a[1001];   //存放物品的数组 排序因子(按性价比降序): bool cmp(bag a,bag b){ if(a.c >b.c) return true; return false; } 使用标准模板库函数排序(最好使用stable_sort()函数,在性价比相同时保持输入的顺序): sort(a, a+n, cmp); 按性价比排序后,背包问题的贪心算法如算法5.3所示。 算法5.3计算背包问题的贪心算法。 //形参n是物品的数量,c是背包的容量M,数组a是按物品的性价比降序排序 double knapsack(int n, bag a[], double c) { double cleft = c;   //背包的剩余容量 int i = 0; double b = 0;   //获得的价值 //当背包还能完全装入物品i while(ic) { printf("No answer!\n"); continue; } //贪心算法的实现,质量最轻者先装载 int i; for (i=1; i<=n && box[i].w<=c; i++) { x[box[i].index] = 1; c -= box[i].w; } //输出装载的集装箱数量 printf("%d\n",i-1); //输出装载的集装箱编号 for (i=1; i<=n; i++) if (x[i]) printf("%d ", i); printf("\n"); } 算法实现源代码: Loading.cpp。 5.5单源最短路径 给定带权有向图G=(V,E),其中每条边的权是非负实数。另外,还给定V中的一个顶点,称为源。现在要计算从源到所有其他各顶点的最短路长度,这里路的长度是指路上各边权之和。这个问题通常称为单源最短路径问题(SingleSource Shortest Paths)。 如图53所示,就是要计算源点v1到其他各个顶点的最短距离,并输出相应的路径。 图53带权有向图G 输入 本题有多组数据。 第1行有两个数据n和m,其中n表示结点的个数,m表示路径的数目。 接下来有m行,每行都有3个数据s、t和edge,其中s表示路径的起点,t表示路径的终点,edge表示该路径的长度。 当n=0,m=0时,输入数据结束。 输出 源点(统一规定为顶点v1,虽然其他顶点也是可以的)到所有其他各顶点的最短路径长度。 接下来有n-1行,是从各个顶点(按升序)回到源点的路径。 输入样例 5 7 1 2 10 1 4 25 1 5 80 2 3 40 3 5 10 4 3 20 4 5 50 0 0 输出样例 10 45 25 55 2<--1 3<--4<--1 4<--1 5<--3<--4<--1 【算法分析】 Dijkstra算法是解单源最短路径问题的一个贪心算法。其基本思想是,设置顶点集合S并不断地做贪心选择来扩充这个集合。一个顶点属于集合S当且仅当从源点到该顶点的最短路径长度已知。初始时,S中仅含有源点。设u是G的某一个顶点,把从源点到u且中间只经过S中顶点的路径称为从源点到u的特殊路径,并用数组dist记录当前每个顶点所对应的最短特殊路径长度。Dijkstra算法每次从VS中取出具有最短特殊路径长度的顶点u,将u添加到S中,同时对数组dist做必要的修改。一旦S包含了所有V中顶点,dist就记录了从源点到所有其他顶点之间的最短路径长度。 Dijkstra算法的实现如算法5.6所示。 算法5.6计算单源最短路径问题的Dijkstra算法。 #define NUM 100 #define maxint 10000 //顶点个数n,源点v,有向图的邻接矩阵为c //数组dist保存从源点v到每个顶点的最短特殊路径长度 //数组prev保存每个顶点在最短特殊路径上的前一个结点 void dijkstra(int n,int v,int dist[],int prev[],int c[][NUM]) { int i,j; bool s[NUM];   //集合S //初始化数组 for(i=1; i<=n; i++) { dist[i] = c[v][i]; s[i] = false; if (dist[i]>maxint) prev[i] = 0; else prev[i] = v; } //初始化源结点 dist[v] = 0; s[v] = true; for(i=1; iy) father[x] = y; if (x>a>>k; //如果k≥n,数字被删完了 if(k >= a.size()) a.erase(); else while(k > 0) { //寻找最近下降点 int i; for(i=0;(i 1 && a[0] == '0') a.erase(0,1); cout< client,int s) { //服务窗口的顾客等待时间 vector service(s+1,0); //服务窗口顾客等待时间的总和 vector sum(s+1,0); //顾客的数量 int n = client.size(); //按顾客的服务时间升序排序 sort(client.begin(),client.end()); //贪心算法的实现 int i=0;   //顾客的指针 int j=0;   //窗口的指针 while(i < n) { service[j] += client[i]; sum[j] += service[j]; ++i,++j; if(j == s) j = 0; } //计算所有窗口服务时间的总和 double t=0; for(i=0; ib.v; } 4. 报酬的计算 排序之后就可以实现贪心策略了。 在统计时限F之前到达的所有作业,能够运行的就计酬,不能运行的就罚款。 按照排好的顺序,依次将作业投入运行,资源足够的就投入运行,资源不够的作业就等待下一个小时。由于所有作业都只需要运行1小时,因此在每小时开始时,资源的数量都是原始数量,如算法5.16所示。 算法5.16大型计算机作业调度问题的贪心算法实现。 int i,j; int iCase = 0;   //测试例编号 int F;   //统计时限 while(scanf("%d",&F) && F) { iCase++; int Profit = 0;   //报酬 memset(Finished, 0, sizeof(Finished)); //分别表示CPU、内存空间和作业的数量 int M,N,L; scanf("%d%d%d",&M, &N, &L); for(i = 0; ii) break; //如果作业还没有运行,并且资源足够 if(!Finished[j] && cpu>=Job[j].a && memory>=Job[j].b) { cpu -= Job[j].a;   //占有资源 memory -= Job[j].b; Finished[j] = 1;   //设置完成标志 Profit += Job[j].v;   //计算报酬 if(i+1 <= Job[j].u)   //计算奖金 Profit += (Job[j].u-i-1)*Job[j].w; else   //计算罚款 Profit -= (i+1-Job[j].u)*Job[j].x; count++;   //完成了一项作业 } } } //在统计时限F之前到达的作业,没有运行的就要罚款 for(i = 0; imax) max=b[i]; return max; } 算法实现源代码: zju1025sort.cpp。 既然是统计质量w的单调递增子序列的个数,则可以应用活动安排问题的算法,找到第一个分组; 对剩下的数据继续应用活动安排问题的算法,找到第二个分组; 以此类推,直到所有的质量w都已经安排。最后分组的数量count就是答案。 算法实现源代码: zju1025greedy.cpp。 5.11ZOJ1029Moving Tables 【问题描述】 著名的ACM(Advanced Computer Maker)公司租用了一层有400个房间的办公楼,结构如图59所示。 房间1房间3房间5…房间397房间399 走廊 房间2房间4房间6…房间398房间400 图59ACM公司办公楼示意图 这层楼沿着走廊南北向的两边各有200个房间。最近,公司要做一次装修,需要在各个办公室之间搬运办公桌。由于走廊狭窄,办公桌都很大,走廊里一次只能通过一张办公桌。必须制订计划提高搬运效率。经理制订如下计划: 一张办公桌从一个房间移到另一个房间最多用10分钟。当从房间i移动一张办公桌到房间j,两个办公室之间的走廊都会被占用。所以,每10分钟内,只要不是同一段走廊,都可以在房间之间移动办公桌。为了说得更清楚一些,经理举例说明哪些情况可以同时移动,哪些情况不能同时移动,如表511所示。 表511办公桌移动的状态 分类 移动办公桌 理由 可行的 (房间30到50)和(房间60到90) 走廊不重叠 (房间11到12)和(房间14到13) 走廊不重叠 不可行的 (房间20到40)和(房间31到80) 房间31到房间40的走廊重叠 (房间1到4)和(房间3到6) 房间3前面的走廊重叠 (房间2到8)和(房间7到10) 房间7前面的走廊重叠 每个房间,只有一张办公桌进出。现在,经理想找到一种方案,使移动桌子的事情尽快完成。请编写程序解决经理的难题。 输入 输入数据有T组测试例,在第一行给出测试例个数T。 每个测试例的第一行是一个整数N(1≤N≤200),表示要搬运办公桌的次数。接下来的N行,每行都有两个正整数s和t,表示一张桌子是从房间号码s移到房间号码t。 有多组输入数据,输入第一行为一个表示输入数据总数的整数N,然后是N组输入数据。 输出 每组输入都有一行的输出数据,为一整数T,表示完成任务所花费的最小时间。 输入样例 3 423 10 201 310100 30 402 2002080 50 603050 70 80 输出样例 10 20 30 题目来源 Asia 2001,Taejon (South Korea) 【算法分析】 在经理给出的说明表格中,已经明确地描述了算法:  (从房间30到50)和(从房间60到90)是允许的,因为没有占用公共的走廊;  (从房间20到40)和(从房间31到80)是不允许的,因为要占用公共的走廊。 我们将每个房间之间的走廊作为一个统计单位,当所有的办公桌都搬运完成之后,看看这段走廊到底需要占用多少次?然后统计所有的走廊被占用的最大值max,这个值就是要单独安排的搬运次数,乘以10就是总的搬运时间。 房间与走廊的映射关系,如表512所示。 表512房间与走廊的映射关系 房间编号 1,2 3,4 5,6 7,8 … 397,398 399,400 表示走廊的数组下标 0 1 2 3 … 198 199 该算法应该属于贪心算法,因为它尽可能使搬运办公桌同时进行,以便使单独安排的搬运次数最少。程序实现如算法5.18所示。 算法5.18ACM公司搬运办公桌的贪心算法实现。 int i, j; //每个房间之间一个走廊,一共有200个公共走廊 int move[200]; int N;   //搬运次数 //每次搬运的起点和终点 int from, to; scanf("%d", &N); memset(move, 0, sizeof(move)); for(i = 0; i < N; i++) { scanf("%d%d", &from, &to); //将房间号映射为走廊号 from = (from - 1)/2; to = (to - 1)/2; //确保from to) swap(from, to); } //占用走廊情况 for(j = from; j <= to; j++) move[j]++; } //所有的走廊被占用的最大值 int max = 0; for(i = 0; i < 200; i++) if(move[i] > max) max = move[i]; printf("%d\n", max * 10); 算法实现源代码: zju1029.cpp。 例如样例1: (10,20),(30,40),(50,60)和(70,80),四次搬运都不占用公共的走廊,所以可以同时进行,一次搬运成功。 5.12ZOJ1076Gene Assembly 【问题描述】 随着大量的基因组DNA序列数据被获得,其对于了解基因越来越重要(基因组DNA的一部分是负责蛋白质合成的)。众所周知,在基因组序列中,由于存在垃圾的DNA中断基因的编码区,真核生物(相对于原核生物)的基因链更加复杂。也就是说,一个基因被分成几个编码片段(称为外显子)。虽然在蛋白质的合成过程中,外显子的顺序是固定的,但是外显子的数量和长度可以是任意的。 大多数基因识别算法分为两步: 第一步,寻找可能的外显子; 第二步,通过寻找一条拥有尽可能多的外显子的基因链,尽可能大地拼接一个基因。这条链必须遵循外显子出现在基因组序列中的顺序。外显子i在外显子j的前面的条件是i的末尾必须在j开头的前面。 本题的目标: 给定一组可能的外显子,找出一条拥有尽可能多的外显子链,拼接成一个基因。 输入 给出几组输入实例。每个实例的开头是基因组序列中可能的外显子数n(0<n<1000)。接着的n行,每行是一对整数,表示外显子在基因组序列中的起始和结束位置。假设基因组序列最长为50000。当一行是0时,表示输入结束。 输出 对于每个实例,找出尽可能多的外显子链,输出链中的外显子,并占一行。假如有多条链,但外显子数相同,那么可以输出其中任意一条。 输入样例 630 340 500705 773 220 470124 337 100 300453 665 880 943 525 556 612 776 输出样例 3 1 5 6 4 2 3 1 题目来源 South America 2001 【算法分析】 虽然本题有很多人使用动态规划算法,但最简单的算法是贪心算法,相当于只有一个资源的活动安排问题。其算法的详细描述,请读者阅读本书5.1节。 1. 数据结构 外显子数为n,外显子的结构体表示: struct gene { int start, end, pos;   //起始和结束位置,序号 }seq[1000];   //存放外显子的数组 2. 排序算法 按外显子的结束位置升序,排序因子: bool cmp(gene a, gene b) { if (a.end preEnd) { preEnd = seq[i].end; printf("%d", seq[i].pos); } printf("\n"); } 算法实现源代码: zju1076sort.cpp。对于样例数据1,计算结果如表513所示。 表513样例数据1的计算结果 原始顺序 1 2 3 4 5 6 340 500 220 470 100 300 880 943 525 556 612 776 拼接之后的顺序 3 1 5 6 4 100 300 340 500 525 556 612 776 880 943 5.13ZOJ1161Gone Fishing 【问题描述】 约翰想要去钓鱼。他有h(1≤h≤16)个小时的时间,在该地区有n(2≤n≤25)个湖,这些湖刚好分布在一条路线上,该路线是单向的。约翰从湖1出发,他可以在任一个湖结束钓鱼。但他只能从一个湖到达另一个与之相邻的湖,而且不必每个湖都停留。假设湖i(i=1,…,n-1),以5分钟为单位,从湖i到湖i+1需要的时间用ti(0<ti≤192)表示。例如t3=4,是指从湖3到湖4需要花20分钟时间。 为了制订钓鱼计划,约翰收集了一些湖的信息。已知在最初5分钟,湖i预计钓到鱼的数量为fi(fi≥0)。以后每隔5分钟,预计钓到鱼的数量将以常数di(di≥0)递减。如果某个时段预计钓到鱼的数量小于或等于di,那么在下一时段将钓不到鱼。为简单起见,假设没有其他的钓鱼者影响约翰的钓鱼数量。 编写一个程序,帮助约翰制订钓鱼旅行的计划,以便他尽可能多地钓到鱼。在每个湖上花费的时间必须是5的倍数。 本题可以包含多组测试例。 多组测试例的第一行是一个整数N,然后是一个空行,接着是N个输入数据块。每个数据块的格式都在问题描述中给出。每个数据块之间都有一个空行。 输出格式包括N个输出数据块,每个输出数据块之间都有一个空行。 输入 输入有多组测试例。对每组测试例,第一行都是n,接下来一行是h。下面一行是n个整数fi(1≤i≤n),然后是一行n个整数di(1≤i≤n),最后一行是n-1个整数ti(1≤i≤n-1)。当n=0时,输入结束。 输出 对每个测试例,输出在每个湖上花费的时间,用逗号分隔,这是约翰要实现钓到最多的鱼的计划(必须使整个计划在同一行输出,即使超过80个字符)。接下来一行是钓到的鱼的数量。如果存在很多方案,尽可能选择在湖1钓鱼所耗费的时间,即使有些时段没有钓到鱼; 如果还是无法区分,那就尽可能选择在湖2钓鱼所耗费的时间,以此类推。测试例之间有一个空行。 输入样例 144 44 210 15 20 1710 15 50 30 10 3 4 30 3 4 3 10 11 2 31 2 3 2 50 2 输出样例 45, 5 Number of fish expected: 31 240, 0, 0, 0 Number of fish expected: 480 115, 10, 50, 35 Number of fish expected: 724 题目来源 East Central North America 1999;Pacific Northwest 1999 【算法分析】 本题在刘汝佳的《算法艺术与信息学竞赛》第一章“算法与数据结构”中有描述。在网站上也有很多资料,基本上都是使用贪心算法。 1. 数据结构 每个湖预计钓到鱼的数量,定义为数组: #define NUM 30 int f[NUM]; 每个湖预计钓到鱼的数量的递减值,定义为数组: int d[NUM]; 相邻湖之间的旅行时间,定义为数组: int t[NUM]; 钓鱼计划,定义为数组: int plan[NUM]; 湖的个数n,用于钓鱼的时间h,尽可能多地钓鱼数量best。 2. 搜索,在任意一个湖结束钓鱼时的最优钓鱼计划 首先把用于钓鱼的时间h,由小时转换为以5分钟为单位的时间: h=h×60/5 这样把钓5分钟鱼的时间称为钓一次鱼。由于约翰从湖1出发,可以在任一个湖结束钓鱼,要得到最优解,就需要进行搜索。 设花费在路程上的时间为: inttime = 0; 假设约翰在第m个湖结束钓鱼,因为路程是单向的,所以路程上的时间: time=∑mi=1t[i],并且满足h-time>0。 于是展开搜索: for (i = 1; i <= n && h-time; ++i) { greedy(i, h - time); time += t[i]; } 其中函数greedy()为: void greedy(int pos, int time); 约翰在第pos个湖结束钓鱼,用于钓鱼的时间是time(不含路程),即钓鱼time次。该函数采用贪心算法构造约翰的钓鱼计划。可以认为约翰能从一个湖“瞬间转移”到另一个湖,即在任意一个时刻都可以从湖1到湖pos中任选一个钓一次鱼。 3. 采用贪心策略,每次选择鱼最多的湖钓一次鱼 对于每个湖来说,由于在任何时候鱼的数目只和约翰在该湖里钓鱼的次数有关,和钓鱼的总次数无关,所以这个策略是最优的。一共可以钓鱼time次,每次在n个湖中选择鱼最多的一个湖钓鱼,程序实现如算法5.20所示。 算法5.20选择鱼最多的湖钓鱼的贪心算法实现。 //从湖1起到湖pos止,花费时间time(不含路程)的钓鱼计划 void greedy(int pos, int time) { //时间已经用完 if (time <= 0) return; int i, j; int fish[MAXN]; int p[MAXN]; int t = 0; for (i = 0; i < pos; ++i) fish[i] = f[i]; memset(p, 0, sizeof(p)); //在时间time内,选择鱼最多的湖钓鱼;如果鱼都没有了,就把时间放在湖1上 for (i = 0; i < time; ++i) { //鱼最多的湖中,鱼的数量 int max = 0; //鱼最多的湖的编号 int id = -1; //查找鱼最多的湖中,鱼的数量和湖的编号 for (j = 0; j < pos; ++j) if (fish[j] > max){ max = fish[j]; id = j; } //找到了,进行钓鱼处理 if (id != -1) { ++p[id]; fish[id] -= d[id]; t += max; } //没有找到(从湖1起到湖pos全部钓完了),就把时间放在湖1上 else ++p[0]; } //处理最优方案 if (t > best) { best = t;   //最优值 memset(plan, 0, sizeof(plan)); for (i = 0; i < pos; ++i)   //最优解 plan[i] = p[i]; } } 输出钓鱼计划时,再把5乘回去,就变成实际的钓鱼时间(分钟): for (i=0; ib.ratio) return true; return false; } 使用C++的标准模板库函数排序: sort(a, a+n, cmp); 3. 应用贪心算法,用猫粮换取JavaBean 当猫粮足够多时,按库房的性价比由高到低换取; 当猫粮的量不足以换取整个库房的JavaBean时,则按比例换取。 程序实现如算法5.22所示。 算法5.22FatMouse用猫粮换取JavaBean的贪心算法实现。 double beans = 0;   //换取到的JavaBean int catfood = 0;   //花费的猫粮 //采用贪心算法换取猫粮 //m-catfood是当前FatMouse还剩下的猫粮 for (i=0; i=a[i].food; i++) { beans += a[i].java; catfood += a[i].food; } //剩下的猫粮不够换取一个库房的JavaBean,则按比例换取JavaBean if (i