算法学习(4)----汉诺塔递归算法和非递归算法
学习《算法设计与分析基础》,习题2.4 第5题要求为汉诺塔游戏设计一个非递归的算法。
思,不得其解。看书后答案提示:
你如果做不到,也不要沮丧:这个问题的非递归算法虽然不复杂,但却不容易发现。作为一种安慰,可以在因特网上寻找答案。
好吧,话都说得这么直接了,遂百度之,得到一个感觉很好的答案,略做修改,摘录于下:
原文地址:http://blog.sina.com.cn/s/blog_48e3f9cd01000474.html
#################################################################################
#################################################################################
在版上看有人讨论汉诺塔的非递归算法,有人介绍怎么样非递归,自己想了半天,总算想明白了。整理了下方便大家:
汉诺塔问题介绍:
在印度,有这么一个古老的传说:在世界中心贝拿勒斯(在印度北部)的圣庙里,一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这就是所谓的汉诺塔。不论白天黑夜,总有一个僧侣在按照下面的法则移动这些金片,一次只移动一片,不管在哪根针上,小片必在大片上面。当所有的金片都从梵天穿好的那根针上移到另外一概针上时,世界就将在一声霹雳中消灭,梵塔、庙宇和众生都将同归于尽。
在印度,有这么一个古老的传说:在世界中心贝拿勒斯(在印度北部)的圣庙里,一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这就是所谓的汉诺塔。不论白天黑夜,总有一个僧侣在按照下面的法则移动这些金片,一次只移动一片,不管在哪根针上,小片必在大片上面。当所有的金片都从梵天穿好的那根针上移到另外一概针上时,世界就将在一声霹雳中消灭,梵塔、庙宇和众生都将同归于尽。
递归算法:
定义 void Hanoi(char src, char des, char via, int n)
表示把n个盘子从src上借助via移动到des上。
显然有
表示把n个盘子从src上借助via移动到des上。
显然有
void Hanoi(char src, char des, char via, int n) { Hanoi(src, via, des, n - 1); Move(src, des, n); //把第n个盘子直接从src移动到des Hanoi(via,des, src, n - 1); }
根据递归算法,设f(n)为n个盘子要移动的次数。
那么显然 :
那么显然 :
f(n + 1) = 2*f(n) + 1 -> [f(n + 1) + 1] = 2*[f(n) + 1] f(1) = 1,-> f(n) + 1 = (1 + 1)^n -> f(n) = 2^n - 1。 f(64)= 2^64-1=18446744073709551615
假如每秒钟一次,共需多长时间呢?一年大约有 31536926 秒,计算表明移完这些金片需要5800多亿年,比地球寿命还要长,事实上,世界、梵塔、
庙宇和众生都已经灰飞烟灭。
非递归算法:
定义从小到大的盘子序号分别为1,2,……n。
可以用一个1到2^n - 1的2进制序列可以模拟出n个盘子的汉诺塔过程中被移动的盘子的序号序列。
定义从小到大的盘子序号分别为1,2,……n。
可以用一个1到2^n - 1的2进制序列可以模拟出n个盘子的汉诺塔过程中被移动的盘子的序号序列。
即给定一个n,我们通过0到2^n - 1序列可以判断出任意一步应该移动那个盘子。
判断方法:第m步移动的盘子序号是m用二进制表示的最低位bit为1的位置。
判断方法:第m步移动的盘子序号是m用二进制表示的最低位bit为1的位置。
证明: n = 1,显然成立。
假设n = k 成立。
n = k + 1时,对应序列1到2^(k+1) - 1,显然这个序列关于2^k左右对称。
假设我们要把k + 1个盘子从A移动C。
那么2^k可以对应着Move(k + 1, A, C)。 1 到 2^k - 1 根据假设可以
对应Hanoi(A, B, C, k)。至于2^k + 1 到 2^(k + 1) - 1把最高位的1去掉对应序列变成1到2^k - 1,显然2^k + 1 到 2^(k + 1) - 1和1到2^k - 1这两个序列中的对应元素的最低位bit为1的位置相同。因此2^k + 1 到 2^(k + 1) - 1可以对应Hanoi(B, C,A,k)。
所以对n = k + 1也成立。
假设n = k 成立。
n = k + 1时,对应序列1到2^(k+1) - 1,显然这个序列关于2^k左右对称。
假设我们要把k + 1个盘子从A移动C。
那么2^k可以对应着Move(k + 1, A, C)。 1 到 2^k - 1 根据假设可以
对应Hanoi(A, B, C, k)。至于2^k + 1 到 2^(k + 1) - 1把最高位的1去掉对应序列变成1到2^k - 1,显然2^k + 1 到 2^(k + 1) - 1和1到2^k - 1这两个序列中的对应元素的最低位bit为1的位置相同。因此2^k + 1 到 2^(k + 1) - 1可以对应Hanoi(B, C,A,k)。
所以对n = k + 1也成立。
下面讨论第m步应该移动对应的盘子从哪到哪?
定义顺序为 A->B->C->A, 逆序为C->B->A->C。
定义顺序为 A->B->C->A, 逆序为C->B->A->C。
性质对n个盘子的汉诺塔,任意一个盘子k(k <= n)k在整个汉诺塔的移动过程中要么一直顺序的,要么一直逆序的。而且如果k在n个盘子移动过程的顺序和k - 1(如果k > 1)以及k + 1(如果k < n)的顺序是反序。
比如:n = 3
1 A->C 2 A->B 1 C->B 3 A->C 1 B->A 2 B->C 1 A->C
其中1的轨迹A->C->B->A>C逆序,2的轨迹A->B->C顺序,3的轨迹A->C逆序
证明:假设n <= k成立
对于n = k + 1 根据递归算法
Hanoi(A,C,B,k + 1) = Hanoi(A, B, C, k) + Move(A, C, k + 1) + Hanoi(B, C,A,k);
整个过程中盘子k + 1只移动一次A->C为逆序对应着2^k。
对于任意盘子m < k + 1,
m盘子的移动由两部分组成一部分是前半部分Hanoi(A, B, C, k)以及后半部分的Hanoi(B, C,A,k)组成。显然有如果m在Hanoi(A, C, B, k)轨迹顺序的话,则m在Hanoi(A, B, C, k)以及Hanoi(B, C,A,k)都是逆序。反之亦然。这两部分衔接起来就会证明m在Hanoi(A,C,B,k)和Hanoi(A,C,B,k + 1)中是反序的。
同时有Hanoi塔中最大的盘子永远是逆序且只移动1步,A->C。
这样的话:
m盘子的移动由两部分组成一部分是前半部分Hanoi(A, B, C, k)以及后半部分的Hanoi(B, C,A,k)组成。显然有如果m在Hanoi(A, C, B, k)轨迹顺序的话,则m在Hanoi(A, B, C, k)以及Hanoi(B, C,A,k)都是逆序。反之亦然。这两部分衔接起来就会证明m在Hanoi(A,C,B,k)和Hanoi(A,C,B,k + 1)中是反序的。
同时有Hanoi塔中最大的盘子永远是逆序且只移动1步,A->C。
这样的话:
m = k + 1,在Hanoi(A,C,B,k + 1)中是逆序。
m = k,由于在Hanoi(A,C,B,k)中是逆序的,所以Hanoi(A,C,B,k + 1)中是顺序的。
m = k - 1,由于在Hanoi(A,C,B,k - 1)是逆序的,所以Hanoi(A,C,B,k)是顺序的,所以Hanoi(A,C,B,k + 1)是逆序的。
依次下去……
结论得证。
m = k,由于在Hanoi(A,C,B,k)中是逆序的,所以Hanoi(A,C,B,k + 1)中是顺序的。
m = k - 1,由于在Hanoi(A,C,B,k - 1)是逆序的,所以Hanoi(A,C,B,k)是顺序的,所以Hanoi(A,C,B,k + 1)是逆序的。
依次下去……
结论得证。
总结:在n个汉诺中n, n - 2, n - 4……是逆序移动,n - 1, n - 3,n - 5……是顺序移动。
有了以上结论,非递归的程序就很好写了。写了个递归和非递归比较程序:
#include <iostream> using namespace std; void Hanoi(char src, char des, char via, int n) { if(n == 1) { cout << n <<" : "<< src <<" --> " <<des << endl; return; } Hanoi(src, via, des, n - 1); cout << n <<" : "<< src <<" --> " <<des << endl; Hanoi(via, des, src, n - 1); } int main() { int n; cin >> n; cout<<"recusive:"<< endl; Hanoi('A','C','B', n); cout << endl; cout<<"normal:"<<endl; char order[2][256]; char pos[64]; for(int i=0;i<64;i++) { pos[i]='A'; //初始的时候,所有的圆盘位置都是 'A'; } order[0]['A'] = 'B'; order[0]['B'] = 'C'; order[0]['C'] = 'A'; order[1]['A'] = 'C'; order[1]['B'] = 'A'; order[1]['C'] = 'B'; //0是顺序 1是逆序 int index[64]; //确定轨迹的顺序还是逆序 int i, j, m; for(i = n; i > 0; i -= 2) index[i] = 1; for(i = n - 1; i > 0; i -= 2) index[i] = 0; memset(pos, 'A', sizeof(pos)); for(i = 1; i < (1 << n); i ++) { for(m = 1, j = i; j%2 == 0; j/=2, m ++); //计算出当前步骤序号的最低的 bit 为 1 的位置。 cout << m <<" : "<< pos[m] <<" --> " << order[index[m]][pos[m]] << endl; pos[m] = order[index[m]][pos[m]]; //更改当前位置 } return 0; }
感叹原作者提出的算法真是精妙。
另外补充:关于计算一个整数 bit 为 1 的最低位的问题,可以如下计算:
int lowestbit(int n) { int tmp=n-1; tmp=tmp^n; //假设n 的最低bit 为 1 的位为m,则此时 tmp 为低 m 位都为 1 、剩余高位都为 0 的数字。 tmp+=1; //此时 tmp= pow(2,m); return log2(tmp); }