找钱问题,可以很大程度上帮助我们理解动态规划法语贪心算法的区别
二、问题
现只有面额为 11元、5元、1元的三种人民币。
给定一个 数目为 money 的人民币,如何用这三种面额的人民币 找开它,且用的人民币张数最少
如:给定 10元,我们可以有以下找法:
2张 5元面额
1张 5元面额 + 5 张 1元面额
10张 1元面额
我们 选择第一种找法。只用两张人民币。
三、分析
利用动态规划法可以找到最优解。
利用贪心算法可以找到最优解(问题满足贪心选择性质时。该找钱问题在 11、5、1三种面额的情况下不满足该性质)
或者找到近似 最优解(在本题设定的三种面额的情况下 便是如此)
如果现在要找开 15元钱,则
1. 根据动态规划法的解题思想,得到最优解为 3张 5元面额的 , 总共 3张
2. 根据贪心算法的解题思想,得到的近似最优解为 1张 11元面额的 加上 4张 1元面额的, 总共 5张
从这就可以大略的看到 两个的区别
四、代码实现找钱问题的 动态规划法与贪心算法 两种解法,形成对比
view plaincopy to clipboardprint?
01./**********************************************************
02. *问 题:有最小面额为 11 5 1的三种人民币,用最少的张数找钱
03. *描 述:动态规划与贪心算法 解决问题 比较
04. *作 者:JarvisChu
05. **********************************************************/
06.#include<stdio.h>
07.#define N 4
08.#define VALUE1 11 //面额为 11元的人民币 (可以修改)
09.#define VALUE2 5 //面额为 5元的人民币 (可以修改)
10.#define VALUE3 1 //面额为 1元的人民币 (不要修改,不然会有找不开的情况)
11.#define MAX_MONEY 1000 //能找开的钱的上限
12.
13./***************************动态规划法********************************
14. *方法:
15. * int Num[MAX_MONEY]; //Num[i]保存要找开 i 元钱,需要的最小人民币张数
16. * int Num_Value[N][MAX_MONEY]; //Num_Value[i][j] 表示 要找 j元钱,需要面额 VALUEi 的人民币张数
17. *
18. * Num[i] = i; 0<= i <=4
19. * Num[i] = min(Num[i-VALUE1]+1,Num[i-VALUE2]+1,Num[i-VALUE3]+1)
20. */
21.
22.//-------------------------求最小值---------------------------------
23.int min(int a,int b,int c){
24. return a<b ? (a<c ? a:c):(b<c ? b:c);
25.}
26.//-------------------------求最优值---------------------------------
27.int DP_Money(int money,int Num[]){
28. //获得要找开money元钱,需要的人民币总张数
29. int i;
30. for(i=0;i<=VALUE2;i++){ //0~4 全用 1元
31. Num[i]=i;
32. }
33. for(i=VALUE2;i<=money;i++){ //从5元开始 凑钱
34. if(i-VALUE1 >= 0){ //如果比 11 元大,说明多了一种用11元面额人民币的可能
35. //从用 11元、5元、1元中 选择一个张数小的
36. Num[i] = min(Num[i-VALUE1]+1,Num[i-VALUE2]+1,Num[i-VALUE3]+1);
37. }
38. else{ //从5元、1元中 选择一个张数小的
39. Num[i] = (Num[i-VALUE2]+1) < (Num[i-VALUE3]+1) ? (Num[i-VALUE2]+1):(Num[i-VALUE3]+1);
40.// Num[i] = min(Num[i-VALUE2]+2,Num[i-VALUE2]+1,Num[i-VALUE3]+1);
41. }
42. }
43. return Num[money];
44.}
45.//-------------------------求最优解---------------------------------
46.void BestChoice(int money,int Num[],int Num_Value[N][MAX_MONEY]){
47. //要找 money 元钱,总人民币张数放在Num[money]中
48. //Num[1~3][money]分别保存三种面额的张数
49. int i;
50. for(i=0;i<VALUE2;i++){
51. Num_Value[1][i] = 0;
52. Num_Value[2][i] = 0;
53. Num_Value[3][i] = i;
54. }
55. for(i=VALUE2;i<=money;i++){
56. if((i>=VALUE1) && (Num[i] == (Num[i-VALUE1]+1))){ //i 是由 i-11+11 构成 即i元是由 i-11元 加上一张面额11元的人民币构成
57. Num_Value[1][i] = Num_Value[1][i-VALUE1]+1; //多一张 11元面额人民币
58. Num_Value[2][i] = Num_Value[2][i-VALUE1]; // 5元面额人民币 张数一样多
59. Num_Value[3][i] = Num_Value[3][i-VALUE1]; // 1元面额人民币 张数一样多
60. }
61. else if(Num[i] == (Num[i-VALUE2]+1)){ //i 是由 i-5+5 构成
62. Num_Value[1][i] = Num_Value[1][i-VALUE2]; //11元面额人民币 张数一样多
63. Num_Value[2][i] = Num_Value[2][i-VALUE2]+1; //多一张 5元面额人民币
64. Num_Value[3][i] = Num_Value[3][i-VALUE2]; // 1元面额人民币 张数一样多
65. }
66. else if(Num[i] == (Num[i-VALUE3]+1)){ //i 是由 i-1+1 构成
67. Num_Value[1][i] = Num_Value[1][i-VALUE3]; //11元面额人民币 张数一样多
68. Num_Value[2][i] = Num_Value[2][i-VALUE3]; // 5元面额人民币 张数一样多
69. Num_Value[3][i] = Num_Value[3][i-VALUE3]+1; //多一张 1元面额人民币
70. }
71. else{
72. }
73. }
74.}
75.
76./***************************贪心算法********************************
77. *方法:
78. * Num_Value[i]表示 面额为VALUEi 的人民币用的张数
79. * 能用大面额的人民币,就尽量用大面额
80. */
81.int Greed(int money,int Num_Value[]){
82. //要找开 money元人民币,Num_Value[1~3]保存 三种面额人民币的张数
83. int total=0; //总张数,返回值也即是总张数。
84. Num_Value[1] = 0;
85. Num_Value[2] = 0;
86. Num_Value[3] = 0;
87. for(int i=money;i>=1;){
88. if(i >= VALUE1){
89. Num_Value[1]++;
90. i -= VALUE1;
91. total++;
92. }
93. else if(i >= VALUE2){
94. Num_Value[2]++;
95. i -= VALUE2;
96. total++;
97. }
98. else if(i >= VALUE3){
99. Num_Value[3]++;
100. i -= VALUE3;
101. total++;
102. }
103. else{
104. }
105. }
106. return total;
107.}
108.void main(){
109. //测试 动态规划法
110./* int i;
111. int money = 23;
112. int Num[MAX_MONEY]; //Num[i]保存要找开 i 元钱,需要的最小人民币张数
113. int Num_Value[N][MAX_MONEY]; //Num_Value[i][j] 表示 要找 j元钱,需要面额 VALUEi 的人民币张数
114. printf("%d\n",DP_Money(money,Num));
115. printf("-------------------------------------------\n");
116. BestChoice(money,Num,Num_Value);
117. printf("-------------------------------------------\n");
118. for(i=0;i<=money;i++){
119. printf("Num[%d]=%4d, %3d, %3d, %3d\n",i,Num[i],Num_Value[1][i],Num_Value[2][i],Num_Value[3][i]);
120. }
121.*/
122.
123. //测试 贪心算法
124./* int i;
125. int Num_Value_Greed[4];
126. for(i=0;i<=40;i++){ //从0 元到 40 元的每一个找钱方式
127. Greed(i,Num_Value_Greed);
128. printf("%d---->>> %d,%d,%d\n",i,Num_Value_Greed[1],Num_Value_Greed[2],Num_Value_Greed[3]);
129. }
130.*/
131.
132. //比较两个算法
133. int i;
134. int dp,grd; //分别保存动态规划法和贪心算法得到的人民币总张数
135. int money; //要找的钱
136. int Num[MAX_MONEY]; //Num[i]保存要找i花费的银币的数目
137. int Num_Value[N][MAX_MONEY]; //Num_Value[i][j] 表示 要找 j 花费的 面值为 VALUEi 的硬币 的数目
138. int Num_Value_Greed[N]; //Num_Value_Greed[i] 表示 面值为VALUEi 的人民币 数目
139. money = 15; //可以为任意非负整型值(15 元是一个比较典型的可以区分两种算法的值)
140. dp = DP_Money(money,Num); //动态规划法
141. BestChoice(money,Num,Num_Value);
142. grd = Greed(money,Num_Value_Greed); //贪心算法
143. printf("要找的钱 为:%d\n\n",money);
144. printf(" 算法 张数 11元 5元 1元\n");
145. printf("动态规划 %-4d %-4d %-3d %-3d\n",dp,Num_Value[1][money],Num_Value[2][money],Num_Value[3][money]);
146. printf("贪心算法 %-4d %-4d %-3d %-3d\n",grd,Num_Value_Greed[1],Num_Value_Greed[2],Num_Value_Greed[3]);
147.}
另参考资料:
1.分治法
分治法(divide-and-conquer):将原问题划分成n个规模较小而结构与原问题相似的子问题;递归地解决这些子问题,然后再合并其结果,就得到原问题的解。
分治模式在每一层递归上都有三个步骤:
合并排序(merge sort)是一个典型分治法的例子。其对应的直观的操作如下:
2. 动态规划法
动态规划算法的设计可以分为如下4个步骤:
分治法是指将问题划分成一些独立地子问题,递归地求解各子问题,然后合并子问题的解而得到原问题的解。与此不同,动态规划适用于子问题独立且重叠的情况,也就是各子问题包含公共的子子问题。在这种情况下,若用分治法则会做许多不必要的工作,即重复地求解公共的子问题。动态规划算法对每个子子问题只求解一次,将其结果保存在一张表中,从而避免每次遇到各个子问题时重新计算答案。
适合采用动态规划方法的最优化问题中的两个要素:最优子结构和重叠子问题。
最优子结构:如果问题的一个最优解中包含了子问题的最优解,则该问题具有最优子结构。
重叠子问题:适用于动态规划求解的最优化问题必须具有的第二个要素是子问题的空间要很小,也就是用来求解原问题的递归算法课反复地解同样的子问题,而不是总在产生新的子问题。对两个子问题来说,如果它们确实是相同的子问题,只是作为不同问题的子问题出现的话,则它们是重叠的。
“分治法:各子问题独立 动态规划:各子问题重叠”
算法导论: 动态规划要求其子问题既要独立又要重叠,这看上去似乎有些奇怪。虽然这两点要求听起来可能矛盾的,但它们描述了两种不同的概念,而不是同一个问题的两个方面。如果同一个问题的两个子问题不共享资源,则它们就是独立的。对两个子问题俩说,如果它们确实是相同的子问题,只是作为不同问题的子问题出现的话,是重叠的,则它们是重叠的。
3. 贪心算法
对许多最优化问题来说,采用动态规划方法来决定最佳选择有点“杀鸡用牛刀”了,只要采用另一些更简单有效的算法就行了。贪心算法是使所做的选择看起来都是当前最佳的,期望通过所做的局部最优选择来产生出一个全局最优解。贪心算法对大多数优化问题来说能产生最优解,但也不一定总是这样的。
贪心算法只需考虑一个选择(亦即,贪心的选择);在做贪心选择时,子问题之一必须是空的,因此只留下一个非空子问题。
贪心算法与动态规划与很多相似之处。特别地,贪心算法适用的问题也是最优子结构。贪心算法与动态规划有一个显著的区别,就是贪心算法中,是以自顶向下的方式使用最优子结构的。贪心算法会先做选择,在当时看起来是最优的选择,然后再求解一个结果子问题,而不是先寻找子问题的最优解,然后再做选择。
贪心算法是通过做一系列的选择来给出某一问题的最优解。对算法中的每一个决策点,做一个当时看起来是最佳的选择。这一点是贪心算法不同于动态规划之处。在动态规划中,每一步都要做出选择,但是这些选择依赖于子问题的解。因此,解动态规划问题一般是自底向上,从小子问题处理至大子问题。贪心算法所做的当前选择可能要依赖于已经做出的所有选择,但不依赖于有待于做出的选择或子问题的解。因此,贪心算法通常是自顶向下地做出贪心选择,不断地将给定的问题实例归约为更小的问题。贪心算法划分子问题的结果,通常是仅存在一个非空的子问题。