5.1 找零问题 .5.1.1 实验目的及要求 (1)理解贪心算法求解问题的思路。 (2)练习编程实现贪心算法解决找零问题。 (3)思考什么问题可以考虑使用贪心算法。 .5.1.2 实验内容 试编程使用贪心算法求解找零钱问题。 设有50,20,10,5,1,0.5,0.1等面额的零钱,顾客购物花费n 元,该顾客只有整数张 100元,在支付(n/100+1)×100元后(恰好是>n 的最小的100倍数),收银员应如何 找零,才能使找回的钱的张数最少? 输入:n,表示顾客购物花费,最多包含一位小数。 输出:找零钱的各面额所对应张数。 样本输入1: 67.5 样本输出: 0 1 1 0 2 1 0 样本输入2: 243 样本输出: 1 0 0 1 2 0 0 1 09 .5.1.3 实验原理 贪心算法是指在对问题求解时,总是做出在当前看来最好的选择。不从整体最优 上加以考虑,所做出的仅仅是在某种意义上的局部最优解,所以贪心算法不是对所有问 题都能得到整体最优解。关键是贪心策略的选择,选择的贪心策略必须具备无后效性, 即某个状态以前的过程不会影响以后的状态,只与当前状态有关。 贪心算法分析问题的思路分为以下几步。 (1)将问题分解为若干个子问题。 (2)找出适合的贪心策略。 (3)求解每一个子问题的最优解。 (4)将局部最优解堆叠成全局最优解。 对于此题,在金额一定的情况下,想要找零钱的张数最少,需要面额尽可能大。所 以如果一张一张地找零钱,每次都选择小于剩余钱数的最大面额,就能保证总张数 最小。找 零钱的贪心算法例解:如果花费1元,总共需要找99元。50<99,所以首先考虑 找50元的,能找99/50=1张;然后第二大面额20元,能找49/20=2张;剩下9元,小于 9的最大面额,10元不符合,那么就考虑5元,以此类推。每次考虑当前看起来最优的 选择,也就是找当前所能找的最大面额。 .5.1.4 实验步骤 结合例解的步骤,试着应用贪心算法的思路编程实现找零问题算法。 (1)设置可找零的面额数组,并从大到小排序;设置结果数组记录对应的面额需要 找几张。 (2)计算需要找零的总额。 (3)从最大面额开始,依次判断每种面额需要的数量。 (4)打印结果数组中的值。 .5.1.5 参考代码 参考代码如下。 1. #include <iostream> 2. using namespace std; 3. //零钱面额的种类数 4. #define N 7 5. int main() 6. { 7. double m[N]= { 50, 20, 10, 5, 1, 0.5, 0.1 }; //定义每种零钱的面额 1 10 8. int result[N]= { 0, 0, 0, 0, 0, 0, 0 }; //保存每种零钱个数的结果,注意应该是整数 9. 10. double n; //输入顾客的花费n 元,最多一位小数 11. cin >> n; 12. //根据题目,顾客实际支付(n / 100 + 1) * 100 元,注意应该是整数张100 元 13. int num = (int(n / 100) + 1) * 100; 14. double change = num - n; //应找的金额 15. 16. //贪心算法开始 17. for (int i = 0; i < N; i++) 18. { 19. result[i]= change / m[i]; //从大面额开始,贪心算法取最多的数量 20. change = change - result[i]* m[i]; //计算剩余未找的钱 21. } 22. 23. //打印结果 24. for (int i = 0; i < N; i++) 25. { 26. cout << result[i]<< " "; 27. } 28. cout << endl; 29. } .5.1.6 实验结果 (1)实验结果验证如图5-1所示。 图5-1 实验结果截图 (2)算法复杂度分析。 找零数额一定小于100,且只需要按照面额循环一次就可以得到结果,而面额数是 一个常数,所以时间复杂度为O(1)。 .5.1.7 实验总结 下面对本次实验进行结论陈述。 (1)总结和学习贪心算法的解题思路,以及贪心算法分析问题的几个步骤。 1 11 (2)思考什么问题可以考虑用贪心算法。 (3)思考贪心算法为什么不一定能获得最优解。 5.2 活动安排问题 .5.2.1 实验目的及要求 (1)结合一些现实生活中的问题,进一步理解贪心算法求解问题的思路。 (2)练习编程实现贪心算法解决活动安排问题。 .5.2.2 实验内容 试编程使用贪心算法求解会场活动安排问题。 假设要在某一会场安排一批活动,并希望在不发生冲突的情况下,安排尽可能多的 活动,同一时间最多安排一个活动。设计一个有效的算法判断最多能安排的活动数量。 如果上一个活动在t 时间结束,下一个活动最早应该在t+1时间开始。 输入:每组测试数据的第一行是一个整数n(1<n<10000),表示该测试数据共有 n 个活动。随后的n 行,每行有两个正整数Bi,Ei(0≤Bi,Ei<10000),分别表示第i 个活动的起始与结束时间(Bi≤Ei)。 输出:对于每一组输入,输出最多能够安排的活动数量。 样例输入: 输入活动数量: 3 输入每个活动的开始时间和结束时间: 1 10 10 11 11 20 样例输出: 2 .5.2.3 实验原理 首先分析此问题的目标,不考虑活动的质量,只期望尽可能多地安排活动数量,而 需要有更多的可用时间才能安排更多活动。对于期望某种结果尽可能多或少的问题, 可以考虑使用贪心算法。分析此问题,试设想如果某活动安排后能剩余尽可能多的时 间留给其他活动,则对活动数量最大化更加有利。对于此设想,反过来用贪心算法的思 路验证,每次选择活动使得总活动数加1,同时剩余的时间尽可能多,再做当前状态下的 1 12 最优选择。 其次注意题目中有一个限制条件,活动不能冲突,即Bi +1>Ei(下一活动开始时 间>上一活动的结束时间),可以称作相容性,满足相容性的活动才能继续安排,否则应 淘汰,检查下一个活动。 根据以上两点,只要将所有活动按照结束时间排序,从第一个活动开始,依次检查 相容性,相容的活动保留,最后将获得可以安排的活动最大数量。 .5.2.4 实验步骤 结合上述思路,应用贪心算法解决活动安排问题的步骤如下。 (1)定义一个结构体Node存储所需要计算的活动的相关时间,其中,begin是开始 时间值,end是结束时间值。 (2)对所有的活动按结束时间排序。 (3)该算法应用贪心策略做选择的意义是,使剩余的可安排时间段极大化,以便安 排尽可能多的相容活动。依次取出已排序的数据,第一个数据直接选用,从第二个开始 判断相容性,即Bi+1>Ei 则活动数量Count++,否则舍弃此活动,继续循环判断下一 个活动。 根据此步骤,试使用贪心算法完成对活动安排问题的编码。 .5.2.5 参考代码 参考代码如下。 1. #define _CRT_SECURE_NO_WARNINGS 2. #include <stdio.h> 3. #include <stdlib.h> 4. #include <string.h> 5. 6. struct Note //活动 7. { 8. int begin; //开始时间 9. int end; //结束时间 10. }; 11. 12. void quicksort(int left, int right, struct Note q[]); //快速排序法 13. int activity_selector(int t, struct Note q[]); //计算可以安排的活动数量 14. 15. int main() //主函数 16. { 17. int result, i; 1 13 18. int t; 19. printf("输入活动数量:\n"); 20. scanf("%d", &t); 21. struct Note q[100]; 22. printf("输入每个活动的开始时间和结束时间:\n"); 23. for (i = 1; i <= t; i++) 24. { 25. scanf("%d%d", &q[i].begin, &q[i].end); 26. } 27. quicksort(1, t, q); //排序 28. result = activity_selector(t, q); //贪心算法求解 29. printf("%d\n", result); 30. return 0; 31. } 32. 33. //快速排序算法 34. void quicksort(int left, int right, struct Note q[]) 35. { 36. int temp = q[left].end; 37. int temp1 = q[left].begin; 38. int i, j, t, f; 39. i = left; 40. j = right; 41. if(right < left) 42. { 43. return; 44. } 45. while (i != j) 46. { 47. while (i < j && q[j].end >= temp) 48. { 49. j--; 50. } 51. while (i < j && q[i].end <= temp) 52. { 53. i++; 54. } 55. if(i < j) 56. { 57. t = q[i].end; 58. q[i].end = q[j].end; 59. q[j].end = t; 60. f = q[i].begin; 61. q[i].begin = q[j].begin; 1 14 62. q[j].begin = f; 63. } 64. } 65. q[left].end = q[i].end; 66. q[i].end = temp; 67. q[left].begin = q[i].begin; 68. q[i].begin = temp1; 69. quicksort(left, i - 1, q); 70. quicksort(i + 1, right, q); 71. } 72. 73. //计算可以安排的活动数量 74. int activity_selector(int t, struct Note q[]) 75. { 76. int count = 1, i = 1; 77. int j; 78. for (j = 2; j <= t; j++) 79. { 80. if(q[j].begin > q[i].end) 81. { 82. i = j; //更新最后一个已安排的活动 83. count++; 84. } 85. } 86. return count; 87. } .5.2.6 实验结果 (1)实验结果验证如图5-2所示。 图5-2 实验结果截图 (2)算法复杂度分析。 抛开排序算法,只关注贪心算法选择活动的过程,只使用了一层循环,且算法复杂 1 15 度与活动规模n 相关,所以活动选择部分的时间复杂度为O(n)。加上排序使用的快速 排序算法,总体时间复杂度为O(nlogn)。 .5.2.7 实验总结 下面对本次实验进行结论陈述。 (1)总结和学习贪心算法的解题思路,对期望获得最大或最小值的问题,可尝试用 贪心算法的思路做假设分析。 (2)复习了排序算法,参考代码使用了快速排序。 5.3 普通背包问题 .5.3.1 实验目的及要求 (1)使用贪心算法解决具有多个互相关联条件的问题。 (2)练习编程实现贪心算法解决普通背包问题。 .5.3.2 实验内容 试编程使用贪心算法求解普通背包问题。 给定n 种物品和一个背包。物品i 的重量是Wi,其价值为Vi,背包的容量为W 。 应如何选择物品装入背包,使得装入背包中的物品的总价值最大? 本题是普通背包问 题,简化于0-1背包问题。区别是普通背包问题允许物品切割,即可以放入某物体的一 部分。有 如下样例数据:假设有三种物品,代号分别为1,2,3,其重量分别为20kg,30kg, 60kg,对应的价值分别是40kg,90kg,240kg,背包容量为90kg。 输入:物品个数、各物品的价值和重量。 输出:物品建议取法(因为可以取部分物品,所以应使用浮点数)。 样例输入: 请输入物品个数: 3 请输入各物品的价值: 40 90 240 请输入各物品的重量: 20 30 60 样例输出: 物品建议取法: 0.000 1.000 1.000 116 .5.3 实验原理 3. 首先分析题目中需要计算的属性参数和限制条件,共有两个属性参数,一个是重量 维度,另一个是价值维度。重量维度上有一个限制条件,即背包的总重量,物品的重量 和必须小于 W ,满足此限制的同时在价值维度上,期望得到最大值。每个物品的价值不 同,重量也不同,但两个属性具有关系,可以求得单位重量下的价值,即通常所讲的“性 价比”。 物品1每千克价值40/20=2。 物品2每千克价值90/30=3。 物品3每千克价值240/60=4。 对于期望获取最大值的问题,尝试用贪心算法分析:要使装入的物品总价值最大, 应将性价比更高的物品先放入,因为普通背包问题可以放入物品的一部分,所以尽可能 多地放入物品直到将背包装满为止。 对于样例数据先装物品3,把60kg物品全部装进背包,还剩30kg物品,把30kg的 物品2全部装入,此时包的容量正好装满,且背包中的物品的总价值为240+90=330 。 可尝试用反证法验证此方法,假设使总价值最大的选取不包括性价比最高的物品, 则有操作如下:去掉其中一部分物品,换成性价比更高的物品,就得到了价值更高的选 择,与假设的总价值最大产生矛盾,所以假设不成立。得到结论:总价值最大的选择一 定包含性价比最高的物品。 从贪心算法的角度再次验证,每次选择都挑选当前最值得装入背包的物品,即性价 比最高的物品,做当前状态下最优的选择。所以编程中需要计算每种物品的性价比,并 进行排序。 .5.4 实验步骤 3. 结合上述思路,应用贪心算法解决普通背包问题的步骤如下。 (1)计算每种物品单位重量的价值Vi/Wi。 (2)单位重量价值高的优先选择,即按照性价比从大到小排序。 (3)根据贪心算法的思想,按照排序从最高性价比的物品开始,将尽可能多的物品 装入背包。若将这种物品全部装入背包后,背包内的物品总重量未达到 W ,则按照排序 选择下一个次高性价比的物品,并尽可能多地装入背包。依此策略一直进行下去,直到 达到背包的重量限制 W 。编程中应注意第一个物品也可能无法完全装入背包,需要注 意重量维度的限制条件。 根据此编程思路,试着使用贪心算法完成对背包问题的编程。 .5.5 参考代码 3. 参考代码如下。 1 17 1. #define _CRT_SECURE_NO_WARNINGS 2. 3. #include <stdio.h> 4. #include <stdlib.h> 5. #include <string.h> 6. 7. #define MAXSIZE 100 //物品最大数 8. #define M 90 //背包的容量 9. 10. void getData(float djz[], float dzw[], int* n); //输入 11. void sort(float tempArray[], int sortResult[], int n); //排序算法 12. void greedy(float dzw[], float x[], int sortResult[], int n); //贪心算法 13. void output(float x[], int n); //输出 14. 15. int main() //主函数 16. { 17. float djz[MAXSIZE], dzw[MAXSIZE], x[MAXSIZE]; //物品价值、重量和性价比 18. 19. int i = 0, n = 0; 20. int sortResult[MAXSIZE]; //待排序后输出的排序结果 21. getData(djz, dzw, &n); //输入 22. for (i = 0; i < n; i++) 23. { 24. x[i]= djz[i]/ dzw[i]; 25. } 26. sort(x, sortResult, n); //排序算法 27. greedy(dzw, x, sortResult, n); //贪心算法 28. output(x, n); //输出 29. return 0; 30. } 31. 32. //输入(参数为各物品的价值、重量以及物品个数) 33. void getData(float djz[], float dzw[], int* n) 34. { 35. int i = 0; 36. printf("请输入物品个数: \n"); 37. scanf("%d", n); 38. printf("请输入各物品的价值:\n"); 39. for (i = 0; i < (*n); i++) 40. { 41. scanf("%f", &djz[i]); 42. } 43. printf("请输入各物品的重量:\n"); 44. for (i = 0; i < (*n); i++) 45. {