Conquer and Divide经典例子之汉诺塔问题
递归是许多经典算法的backbone, 是一种常用的高效的编程策略。简单的几行代码就能把一团遭的问题迎刃而解。这篇博客主要通过解决汉诺塔问题来理解递归的精髓。
汉诺塔问题简介:
在印度,有这么一个古老的传说:在世界中心贝拿勒斯(在印度北部)的圣庙里,一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这就是所谓的汉诺塔。不论白天黑夜,总有一个僧侣在按照下面的法则移动这些金片,1. 一次只移动一片; 2. 不管在哪根针上,小片必在大片上面。当所有的金片都从梵天穿好的那根针上移到另外一概针上时,世界就将在一声霹雳中消灭,梵塔、庙宇和众生都将同归于尽。
在印度,有这么一个古老的传说:在世界中心贝拿勒斯(在印度北部)的圣庙里,一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这就是所谓的汉诺塔。不论白天黑夜,总有一个僧侣在按照下面的法则移动这些金片,1. 一次只移动一片; 2. 不管在哪根针上,小片必在大片上面。当所有的金片都从梵天穿好的那根针上移到另外一概针上时,世界就将在一声霹雳中消灭,梵塔、庙宇和众生都将同归于尽。
我们首先假设要把n个金片从宝针‘A’移动宝针‘B’,还有有另外一根'V'可以借用。
如果n = 1,那么直接从 A->B 即可。
如果n = 2,那么先把A最上面(第一个)金片移动到V,暂且放一下,然后第二个金片移动到B目的地,接着再把V上面的移动到B目的地,所以移动的次序是 A->V, A->B, V-B。
如果n = 3,可以这么考虑: 先不看出发地A最下面一个最大的金片,先解决怎么把上面两个移走,方法我们已经知道了,但是移到哪里呢?只有B, V可选,B肯定是不行,因为B是目的地,那么只能是通过B先把两个金片移动到V(A->B, A->V, B->V),然后最下面那个最大的金片移动到目的地B(A->B)。注意这时B上面仅有这个最大的金片,A上面空的,V上面有两个金片。接着把V上面两个金片借由A移动到B(V->A, V->B, A->B)。
如果n = 4, 同样的不考虑最大的一个,先把前三个移动到V,使用上面介绍的移动次序。
以此类推(其实 n = 2时,也可以看作是先解决最上面一个的问题,然后才是把最下面的金片移动到目的地)
通过分析,发现解决n个金片的方法是先借由B,把n-1个移动到V,然后把A最下面一个移动到B,在然后借由A把V上面n-1移动到B。
用java实现如下
// in java public class Hanoi{ private static int count = 0; public static void move(char src, char des, int n){ System.out.println("plate:"+ src +" pillar"+"->"+des+" pillar"); count++; } public static void hanoiSolver(char src, char via, char des, int n){ if (n==1){ move(src, des, n); } else{ hanoiSolver(src, des, via, n-1); move(src, des, n); hanoiSolver(via, src, des, n-1); } } public static void main(String[] args) { char src = 'A'; //source pillar char des = 'B'; //detination pillar char via = 'V'; //via pillar int n = 3;// the numer of plates hanoiSolver(src, via, des, n); System.out.println("the total number of moves is "+count); } }
仔细分析这个算法,可以学习到:
1. 初中高中大学数学解题中常常用到的归纳总结。一个看似复杂至极的问题,先心里不要慌,初步分析前面几个,看到规律,推而广之。
2. 计算过程中的调用自身函数,形成重入,并因此而把复杂问题化解为相对简单或已经知道答案的小问题。这也不经我想到解数列题的各种方法。(对不起,高中是博主学习数学最认真的时候)
3. 在递归中必须有明确递归结束条件称递归出口,否则就是死循环啦。如本问题中当n=1时,就是退出递归的时候。
故事中所说的如果成功得把金片移动结束,世界就灰飞烟灭,这是真的吗?我们来小研究下:
假设f(n)为n个盘子要移动的次数。
那么根据上面分析 f(n + 1) = f(n) + 1 +f(n) = 2*f(n)+1
那么根据上面分析 f(n + 1) = f(n) + 1 +f(n) = 2*f(n)+1
变换为 [f(n + 1) + 1] = 2*[f(n) + 1], 经典等比数列
因为f(1) = 1,所以可得f(n) = 2^n - 1。
因为f(1) = 1,所以可得f(n) = 2^n - 1。
f(64)= 2^64-1,
所以一共要移动 2^64-1次, 假使一秒钟移动一次,不吃不睡觉,一共要移动2.1350398e+14天,584,942,417,355年。
Take-away Tip:
递归Recursive 就是在程序中调用自身,形成重入,把复杂问题层层简化成一个已然解决的问题,
并在一定条件下退出递归。