分治(Divide-and-Conquer)
分治法,字面意思是“分而治之”,就是把一个复杂的问题分成两个或多个相同的子问题,再把子问题分成更小的子问题直到最后子问题可以简单地直接求解,原问题的解即子问题的解的合并,这里父子问题不存在依赖关系,即父问题在解决时可以把子问题当做1个成员,子问题在求解是也可以完全脱离父问题的状态。
但并不是所有复杂问题都可以完美拆分成多个子问题,要想使用分治思想必须满足如下条件:
- 该问题的规模缩小到一定的程度就可以容易的解决。
- 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
- 利用该问题分解出的子问题的解可以合并为该问题的解。
- 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。
第3、4条最为关键,假如不满足这两个条件,即表示子问题解会影响到上一级问题的解,此时不能使用分治来简单地组合各个子问题解获得最终解,而是使用动态规划。
分治法的总体步骤为三步:
-
分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
-
解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题;
-
合并:将各个子问题的解合并为原问题的解。
分治子问题的独立性与无后效性决定,分治天然与递归手段是一对。即分治大多都是采用递归来进行的,例如经典的汉诺塔游戏。下面将以汉诺塔递归公式的推导来解释分治的前提条件与解决步骤。
汉诺塔游戏:
在印度,有这么一个古老的传说:在世界中心贝拿勒斯(在印度北部)的圣庙里,一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这就是所谓的汉诺塔。不论白天黑夜,总有一个僧侣在按照下面的法则移动这些金片:一次只移动一片,不管在哪根针上,小片必须在大片上面。僧侣们预言,当所有的金片都从梵天穿好的那根针上移到另外一根针上时,世界就将在一声霹雳中消灭,而梵塔、庙宇和众生也都将同归于尽。
其中的有用信息:
- 三根宝石针,要求将一根针上的所有金片移动到另一根针上
- 每次只能移动一片金币
- 任何时刻,大金片都要在小金片上面
计算过程
很明显,金币数量决定了游戏的复杂度,针其实是没有顺序的,我们用红黄绿来表示
1. 一块金币
一块金币只需移动一步,就可将红色针上的金币移动到黄色针上:
1--------------------------------->1
红 ++++黄++++ 绿___________ 红 ++++黄++++ 绿
2. 两块金币
首先将1号金币放到绿色针,2号金币位置不变:
----------------------------------------->1
____________________________2
红 ++++黄++++ 绿___________ 红 ++++黄++++ 绿
再将2号金币放到黄色针:
—————————————————————————————————————————1
---------------------------------->2
红 ++++黄++++ 绿___________ 红 ++++黄++++ 绿
最后将1号金币放到黄色针,此时已将所有金币从一个针移动到另一个针上面(红到黄):
----------------------------------1<---------
__________________________________2
红 ++++黄++++ 绿___________ 红 ++++黄++++ 绿
针对两块金币的情况,可归纳出一个典型的步骤:
1、首先将1号金币放到一根针(本例选绿针)
2、再将2号金币放到另一根针(只剩下黄针)
3、将1号金币放到2号金币上面(黄针)
3. 三块金币
1
2
3
红 ++++黄++++ 绿___________ 红 ++++黄++++ 绿
3块金币情况,我们先假设1、2号金币为一个整体称之为0号金币,且小于3号金币,此时问题简化为如下:
0
3
红 ++++黄++++ 绿___________ 红 ++++黄++++ 绿
是不是非常熟悉,这其实与两块金币情况是一样的,我们可以继续使用上面的三步将0、3号金币放到黄色针上。
1、首先将0号金币放到绿针
2、再将3号金币放到黄针
3、将0号金币放到黄针
但是这仅仅是个假设,0号金币不存在,而实际上是两块金币。那么上述的第一步“首先将0号金币放到一根针”实际是指将1、2
金币放到绿针,仔细回忆,这是不是又变成了”把两个金币从一根针移动到另一根针“的问题?没错,此时根本不用考虑3号金币,
因为在这个阶段,它一直老老实实的待在红针上。只是这次的目标针为绿色,那么我们就执行以下三步:
1.1、首先将1号金币放到黄针
1.2、再将2号金币放到绿针
1.3、将1号金币放到绿针
此时情况应该是这样的:
----------------------------------------->0(1、2)
3
红 ++++黄++++ 绿___________ 红 ++++黄++++ 绿
执行完“首先将0号金币放到绿针”,继续执行“再将3号金币放到黄针”,这步很简单
___________________________________________0(1、2)
---------------------------------->3
红 ++++黄++++ 绿___________ 红 ++++黄+++++++绿
再来看“将0号金币放到黄针”,很明显这又变成了一个“把两个金币从一根针移动到另一根针“的问题,那么就做下面三步:
2.1、首先将1号金币放到红针
2.2、再将2号金币放到黄针
2.3、将1号金币放到黄针
此时情况应该是这样的:
------------------------------------>0(1、2)
_____________________________________3
红 ++++黄++++ 绿--------------红 ++++黄++++ 绿
至此,需总结如下:
- 当金币数等于2时,将所有金币从一个位置移动到另一个位置所需的步骤是固定的三步
- 首先将1号金币放到一根针
- 再将2号金币放到另一根针
- 最后将1号金币放回到2号金币之上
很明显,这是一个子问题,而且这个子问题满足独立性(不受后面操作影响)满足分治法要求3。
-
金币数很多时,要依次递归,直到金币数量变为1时,此时无需再次拆解,因为1个金币直接移动即可,这一过程就是分治法步骤中提到的“分解”
-
分解完成后,利用”三步骤“完成子任务,这就是分治法中的“解决”
-
各层子任务完成后,会跳到上一层递归,继续完成父任务,直到到递归栈的最外层,此过程则是结果的“合并”
下图是描述了4枚金币时的递归过程、子任务解决以及结果合并的全过程
从最左侧开始,首先需解决四枚金币的情况。假设前三枚金币为一个整体,那么四枚金币这一层可分为三步,其中的第一步变为三枚金币的情况,继续分解。直到n=1,即金币数目为1,此时停止分解(递归)。
分解完成,此时并没有移动任何一枚金币,从最深一层递归栈开始,反序依次执行"三步骤",执行完毕后跳出本层递归栈,继续执行上一层的“三步骤”,最终完成4枚金币的移动。图中绿色虚线即为执行顺序流。
代码实现
引用一个C语言实现实例
#include <stdio.h>
#include <string.h>
/*
算法思路:1将 n-1个盘子先放到B座位上
2.将A座上地剩下的一个盘移动到C盘上
3、将n-1个盘从B座移动到C座上
*/
//函数声明
void move(int n, char x, char y);
void hannuo(int n, char one, char two, char three)
{
if(n==1)
move(n, one, three); //递归截止条件
else
{
hannuo(n-1, one, three,two);//递归分解任务:将 n-1个盘子先放到B座位上
move(n, one, three);//将A座上地剩下的一个盘移动到C盘上
hannuo(n-1, two, one, three);//递归分解任务:将n-1个盘从B座移动到C座上
}
}
void move(int n, char x,char y)
{
static step = 1;
printf("step:%d, move golden[%d] to %c from %c",y,x)
if(step++ %3 ==0)
printf("\n");
}
int main()
{
int n;
printf("input your number");
scanf("%d",&n);
hannuo(n,'A','B','C');
return 0;
}