1.硬币找零问题
在学习算法分析时候的一个经典问题就是硬币找零问题。该问题是给一些不同面值的硬币,其中
=1,以及数值M。要计算出找M所需要的最少硬币数。
比如我们有硬币<1,5,10,20>,那么如果要找33块钱的话,最少的零钱数是20+10+1+1+1,一共5个硬币。这就是硬币找零问题的简介。
2.贪婪算法(Greedy algorithm)
从上面很容易看出,想要计算最少的硬币数,用贪婪算法即可。即每次我们都找面值最大的硬币,直到不能找了,就开始找第二大的,一直往下。贪婪算法的伪代码如下:
coin_change(<a0,a1,....an>,M)
1 coin_num <- 0
2 for i from n down to 0
3 while M <= an
4 M <- M - an
5 coin_num <- coin_num + 1
6 return coin_num
3.动态规划(Dynamic programming)
贪婪算法十分简单,但是在许多情况下是得不到最优解的。比如说零钱数是<1,3,4>,而我们想找6块钱。用贪婪算法计算得到的是4+1+1,一共三个硬币。而最优解是2个硬币。如果对于什么样的零钱数集合不能用贪婪算法解得最优解感兴趣的朋友,可以参考相关的论文,本人之前看过一篇,还是蛮有意思的。
这里介绍另外一种方法,动态规划。其原理大致上是说若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。
用O(n)表示找n块所需最少的硬币数。这里可以看到,如果有硬币<1,3,4>,当想要求6块多需要的最少硬币数时O(6) = min{O(5)+1, O(3)+1, O(2)+1}。即我们想要找6块钱,我们只需要找出6-1=5,6-3=3,6-4=2这三种情况哪一种所需硬币数最少即可。因为我们只要在这三个数上加1即可。
我们可以总结出对于硬币集合,想要找M块钱,则O(M) = min{O(M-a0)+1,......O(M-an)+1}.
因此动态规划的伪代码如下。
DP_coin_change(<a0,a1,...an>,M)
1 coin_num <- Recurse_coin_change(<a0,a1,...an,M>)
2 return coin_num
Re_coin_change(<a0,a1,...an>,M>)
1 if M < 0 return error
2 if M = 0 return 0
3 if M = 1 return 1
4 coin_num <- min{Re_coin_change(<a0,a1,...an>,M-a0)+1, Re_coin_change(<a0,a1,...an>,M-a1)+1,...Re_coin_change(<a0,a1,...an>,M-an+1)}
5 return coin_num
这种方法是自上往下,利用递归计算。可以看出来这样计算的话有许多次计算是重复的。因此我们不妨将之前计算过的一些数值所需最少硬币数记录起来,这种方法可以省去很多计算。伪代码如下:
DP_Memory_coin_change(<a0,a1,...an>,M)
1 c[M] <- {}
2 set all elements in c[] as INF
3 coin_num <- Recurse_coin_change(<a0,a1,...an,M>,c)
4 return coin_num
Re_coin_change(<a0,a1,...an>,M>,c)
1 if c[M] != INF return c[M]
2 if M < 0 return error
3 if M = 0 return 0
4 if M = 1 return 1
5 coin_num <- min{Re_coin_change(<a0,a1,...an>,M-a0,c)+1, Re_coin_change(<a0,a1,...an>,M-a1,c)+1,...Re_coin_change(<a0,a1,...an>,M-an+1,c)}
6 c[M] <- coin_num
7 return coin_num
对于一些不太擅长编程的朋友来说,动态规划利用递归进行求解理解起来是挺费脑的。那么有没有不用递归的方法呢?有!
我们之前一直想着的是从上而下去求解M的最小找零数。但是如果我们自下而上去寻找呢?从0开始计算一直计算到M,这样是不需要进行递归的。并且计算次数也更少。 该方法的伪代码如下:
DP_coin_change_bottom_up(<a0,a1,...an>,M)
1 c[M] <- {}
2 for i from 1 to M
3 if i = a0 or a1 or .... an c[i] = 1
4 else c[i] = min(c[i-a0]+1,c[i-a1]+1,.....c[i-an]+1)
5 return c[M]