分治法
任何一个可以用计算机求解的问题所需的时间都与其规模有关。问题的规模越小,越容易直接求解,解题所需的计算时间也越少。例如,对于n个元素的排序问题,当n=1时,不需任何计算;当n=2时,只要做依次排序即可;而当n较大时,问题就不那么容易处理了。
“分治”(Divide and conque)就是分而治之,即讲一个难以直接解决的大问题。分割层一些规模较小的相同问题,以便各个击破,分而治之。基本步骤如下:
1)分解:将原问题分解成若干个规模较小、相互独立、与原问题性质想听的子问题;
2)解决:若子问题规模较小而容易解决则直接解决,否则递归的解决;
3)合并:将各个子问题的解合并为原问题的解。
应用一:
排队购票问题。
问题描述:
一场比赛开始前,售票工作正在进行。每张球票为50元,现有30个人排队等待购票,其中有20个人手持50元的钞票,另外10个人手持100元的钞票。假设开始售票时售票处没有零钱,求出这30个人排队购票,使售票处不会出现找不开钱的局面的不同排队方案。(拿同样面值钞票的人对换位置后为同一种派排队方案)。
算法分析:一般清醒,假设有m+n个人在排队购票,其中m个人手里有50元钞票,n个人手里拿100元钞票。求排队方案;
分三种情况来考虑问题:
1)n=0. 由于拿相同票值的人调换顺序后仍为同一种排队方案,所以f(m,0) = 1.
2)m < n. 在这种情况下,无论怎么样排队,售票处都会面临找不开钱的局面,因此f(m,n) = 0.
3)其他情况. 对于第 i+j 个人来说,他前面有 i+j-1 个人,对于他自己有两种情况。如果第 i+j 个人拿50元的钞票,则他前面的人有 i-1 个人拿50元钞票, j 个人拿100元钞票;如果第 i+j 个人拿100元钞票,则他前面有 i 个人拿50元钞票, j-1 个人拿100元钞票。因此,f(i, j) = f(i-1, j) + f(i, j-1).
因此,根据以上分析,我们得到的解决方案代码为:
public int solution(int m, int n) { if(n == 0) return 1; if(m < n) return 0; return solution(m-1, n) + solution(m, n-1); }
使用递推法,也就是动态规划,用数组存储之前计算的值,然后后面直接使用即可。
public int solution(int m, int n) { int[][] f = new int[m+1][n+1]; //设置n == 0的情况为1 for(int i=1; i<=m; i++) f[i][0] = 1; //设置m < n的情况为0 for(int i=0; i<=m; i++) { for(int j=i+1; j<=n; j++) f[i][j] = 0; } //设置其他情况 for(int j=1; j<=n; j++) { for(int i=j; i<=m; i++) f[i][j] = f[i-1][j] + f[i][j-1]; } return f[m][n]; }
应用二:
“放苹果”问题。把M个同样的苹果放在N个同样的盘子里,允许有的盘子空着不放,共有多少种不同的放法?注意:5,1,1和1,5,1是同一种放法。
算法分析:因为允许有的盘子空着,因此我们可以根据盘子中是否有苹果分情况讨论。分为至少有一个盘子空着和所有的盘子中都有苹果两种情况。
1)至少有一个盘子空着。即求m个苹果往n-1个盘子中放,有几种放法。
2)所有的盘子中都有苹果。即每个盘中至少有一个苹果,则问题变为m-n个苹果放到n个盘子中,有几种放法。
故对于一般情况来说,f(m, n) = f(m, n-1) + f(m-n, n);
但是并不是所有的m,n都符合上面的地推式,因此我们可以根据m和n之间的关系讨论:
1)m>n时,上面两种情况都满足,可以直接使用;
2)m<n时,则即使每个盘子中只放一个苹果,也会有n-m个盘子空着,去掉这n-m个盘子,则剩余的问题变为,m个苹果放到m个盘子中有多少种排放方式。因此f(m, n) = f(m, m).
出口:
当n=1,即只有一个盘子时,则只能把所有苹果都放到这一个盘子里,因此f(m, 1) = 1;
当m=0时,表示没有苹果可放,则f(0, n) = 1.
(对于递推式来说,n-1这样递减,因此一定会到达1,;而当n>m时,有f(n, m) = f(m, m),而m的变化为n-m,因此会到达0,因此递推式一定会到达出口)。
代码为:
public int putApple(int m, int n) { if(m == 0) return 1; if(n == 1) return 1; if(n > m) return putApple(m, m); return putApple(m, n-1) + putApple(m-n, n); }
应用三:
红与黑。有一间长方形的房子,地上铺满了红色和黑色两种颜色的正方形瓷砖。当站在其中一块黑色的瓷砖上面时,只能向上下左右相邻的黑色瓷砖移动。请写一个程序,计算总共能够到达多少块黑色的瓷砖。
算法分析:这个问题也可以描述成:给定一点,计算他所在的连通区域的面积。对于一般情况,设f(x, y)为从点(x, y)出发能够走过的黑瓷砖总数,则
f(x, y) = 1 + f(x-1, y) + f(x+1, y) + f(x, y-1) + f(x, y+1).
伪代码:
int findNum(int x, int y) { if(x和y超过了矩阵范围) return 0; if(遇到红色瓷砖) return 0; 将走过的瓷砖标记为红色; return 1 + findNum(x+1, y) + findNum(x-1, y) + findNum(x, y+1) + findNum(x, y-1); }