算法2:Hanoi塔
一、背景介绍
在印度,有这么一个古老的传说:在世界中心贝拿勒斯(在印度北部)的圣庙里,一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这就是所谓的汉诺塔。不论白天黑夜,总有一个僧侣在按照下面的法则移动这些金片,一次只移动一片,不管在哪根针上,小片必在大片上面。当所有的金片都从梵天穿好的那根针上移到另外一概针上时,世界就将在一声霹雳中消灭,梵塔、庙宇和众生都将同归于尽。
二、数学定义
从左到右有三根柱子A、B、C,其中A柱子上面有从小叠到大的n个圆盘,现要求将A柱子上的圆盘移到C柱子上去,期间只有一个原则:一次只能移到一个盘子且大盘子不能在小盘子上面,求移动的步骤和移动的次数。
我们记移动总次数为sum。
不难发现,
当n==1时,sum=1,
第一次:A→C;
当n==2时,sum=3,
第一次:A→B,
第二次:A→C,
第三次:B→C;
当n==3时,sum=7;
第一次: A→C,
第二次: A→B,
第三次: C→B,
第四次 :A→C,
第五次 :B→A,
第六次 :B→C,
第七次: A→C;
因此,
不难发现规律:
1个圆盘的次数 2的1次方减1
2个圆盘的次数 2的2次方减1
3个圆盘的次数 2的3次方减1
……
n个圆盘的次数 2的n次方减1
故:移动次数为:2^n - 1。
三、递归算法
void hanoi(int n, int a, int b, int c) { if (n > 0) { hanoi(n - 1, a, c, b); move(a, b); hanoi(n - 1, c, b, a); } }
其中,hanoi(n, a, b, c)表示将塔座A上自下而上,由大到小叠放在一起的n个圆盘依移动规则移至塔座B上并按照同样的顺序叠放。在移动过程中,以塔座C作为辅助塔座。move(a, b)表示将塔座A上编号为n的圆盘移到塔座B上。
实现这个算法可以简单分为三个步骤:
(1) 把n-1个盘子由A 移到 B;
(2) 把第n个盘子由 A移到 C;
(3) 把n-1个盘子由B 移到 C;
从这里入手,在加上上面数学问题解法的分析,我们不难发现,移到的步数必定为奇数步:
(1)中间的一步是把最大的一个盘子由A移到C上去;
(2)中间一步之上可以看成把A上n-1个盘子通过借助辅助塔(C塔)移到了B上,
(3)中间一步之下可以看成把B上n-1个盘子通过借助辅助塔(A塔)移到了C上;
四、完整代码
#include<iostream> #include<math.h> using namespace std; void hanoi(int n, char A, char B, char C) { if (n == 1) cout << "将圆盘" << n << "从" << A << "移动到" << C << endl; else { hanoi(n - 1, A, C, B); cout<< "将圆盘" << n << "从" << A << "移动到" << C << endl; hanoi(n - 1, B, A, C); } } int main() { int n; cout << "请输入A上圆盘的个数n:"; cin >> n; cout << endl; hanoi(n, 'A', 'B', 'C'); cout << endl; int sum = pow(2, n) - 1; printf("移动的总次数sum为:%d",sum); return 0; }
五、非递归(拓展)
该方法设计了一个数据结构 struct act {int flag,num; char x, y, z;} S[2000]; 存储当前操作信息,其中flag==0表示直接移动num号圆盘,否则需要进一步分解;num表示当前操作移动盘子的编号,x,y,z表示当前操作对应的3个塔柱,分别是出发柱,中点柱和目标柱。
void Hanoi_1(int n, char a, char b, char c)//非递归算法1 { struct act { int flag, num; char x, y, z; } S[2000]; //存储当前操作信息,flag==0表示直接移动num号圆盘,否则需要进一步分解 int top, m; char ta, tb, tc; S[0].flag = 1;//初值入栈 S[0].num = n; S[0].x = a; S[0].y = b; S[0].z = c; top = 0; int count = 0; while (top >= 0) { if (S[top].flag == 0 || S[top].num == 1)//直接将num号圆盘从x移动到z { printf("%d: %d %c -> %c ", ++count, S[top].num, S[top].x, S[top].z); --top; } else { //提取栈顶信息 m = S[top].num; ta = S[top].x; tb = S[top].y; tc = S[top].z; //将 Hanoi(n-1, b, a, c); 操作入栈 ,覆盖原栈顶信息 S[top].num = m - 1; S[top].x = tb; S[top].y = ta; S[top++].z = tc; //将 将第m个盘从a柱移动到c柱 操作入栈 S[top].flag = 0; S[top].num = m; S[top].x = ta; S[top++].z = tc; //将 Hanoi(n-1, a, c, b); 操作入栈 S[top].flag = 1; S[top].num = m - 1; S[top].x = ta; S[top].y = tc; S[top].z = tb; } } }
以上非递归算法利用栈模拟递归过程的基本方法。对于有n个盘子的汉诺塔问题,需要操作的步骤为2^n – 1,如果每一个步骤看成一个节点,则刚好构成一棵满二叉树,树高h与盘子数量的关系为h==n。结点所在的层数与对应盘子的编号关系为level==n+1-level,即盘子1在第n层,盘子n在第1层;若某个结点的操作为“盘子n从A->C”,则其左孩子操作为“盘子n-1从A->B”,右孩子操作为“盘子n-1从B->C”;中序遍历满二叉树,结点的编号恰好对应移动的次序。
因此我们可以构造一棵满二叉树,然后中序遍历该二叉树。
void Hanoi_3(int n, char a, char b, char c) { struct act { int num; char x, y, z; } BT[132000]; //存储每一步操作的满二叉树 int S[MAX] = { 0 }; int i, top, count = 0; BT[1].num = n;//为根结点赋值 BT[1].x = a; BT[1].y = b; BT[1].z = c; n = pow(2, n - 1); for (i = 1; i < n; i++)//为每个节点的左右孩子赋值 { BT[i + i].num = BT[i + i + 1].num = BT[i].num - 1; BT[i + i].x = BT[i + i + 1].y = BT[i].x; BT[i + i].z = BT[i + i + 1].x = BT[i].y; BT[i + i].y = BT[i + i + 1].z = BT[i].z; } //中序遍历满二叉树 n += n; i = 1; top = -1; while (i < n || top >= 0) { if (i < n)//入栈,并搜索左孩子 { S[++top] = i; i += i; } else//输出并退栈,搜索右孩子 { i = S[top--]; printf("%d: %d %c -> %c ", ++count, BT[i].num, BT[i].x, BT[i].z); i += i + 1; } } }
鸣谢:http://blog.csdn.net/qiaoruozhuo