前言
汉诺塔问题是一个经典的算法问题,是递归、栈等的重要应用。而我认为如果要理解该问题的解法,需要了解其中一个关键状态,即类似动态规划中的状态转移方程,而这个状态就是实现递归算法的关键。这里卖个关子,以下将详细分析汉诺塔问题:
一、汉诺塔问题
起源
大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
问题
上面所讲的是一个古老的传说,而实际上的问题便是由此而来:假设有三个塔台A、B、C,其中A塔台自上往下堆叠了n个从小到大的圆盘,编号为1,2,…n。如下图:
现要求我们,将塔台A中的n个圆盘移动到C塔台上且顺序不变,并且满足以下几个要求:
- 一次只能移动一个圆盘。
- 小圆盘上不能放大圆盘。
求出需要移动的最少步数,并将每次移动的步骤打印出来。
ps:这里有个点要明白:其中不管是A到C还是B到C还是C到A,它们的性质都是一样的,所以问题可以简化为:将某塔台中的n个圆盘移动到某塔台上。当然,实际分析还是用A到C,便于书写和理解。
二、问题分析
首先,给出最小步数的计算公式:2^n - 1。那么为什么是这个如此常见的一个数学公式?这其实可以通过数学递推公式证明得出,我们需要慢慢分析:
(1)圆盘个数n=1时。此时我们只需要将A中编号为1的圆盘搬运到C即可,A -> C。最小步数:1。注意,我们的算法代码是通过递归实现的,所以,n=1就是一个非常关键的边界条件:在这里,我们直接将A搬运到C。
(2)圆盘个数n=2时。首先,将1从A搬运到B,A -> B,此时A中只剩2,C中无圆盘;将2从A搬运到C,A -> C,A中无圆盘,C中只有一个圆盘2;将1从B搬运到C,B -> C,完成搬运。最小步数:3。
(3)圆盘个数n=3时。开始,将1从A搬运到C,A -> C,A中还剩2、3;将2从A搬运到B,A -> B,A中只剩3,C中有1;将1从C搬运到B,C -> B,A中只剩下3,C中无圆盘;接着,将3从A搬运到C,A -> C,A中无圆盘,C中只有一个圆盘3;这时候,我们会发现回到了第二步的操作,将B中的2个圆盘搬运到C,实现过程类似2:B -> A,B -> C,A -> C。细心的小伙伴已经发现,这不就是第二部中将A和B的位置调换吗。没错,实际上这也是递归的一个关键。最小步数:7。
在上面的分析,会发现我经常讲到:A中还有多少个元素,C中有多少个元素。因为我们可以看到,在按最小步数的分析下,有一个每次遍历到一定程度都会出现"时刻",也就是前言提到的关键状态:A中只剩一个圆盘n,B中有n-1个圆盘,C中无圆盘。因为分析到这一步时,下一步我们将最后一个编号为n的圆盘从A搬运到C,这时候,A中无圆盘而C中只有一个圆盘n,其余n-1圆盘在B中。做完这一个步,因为最头疼的圆盘n已经到了它改在的位置,所以我们只需要将n-1个圆盘从B到C就能完成问题了。而这时候我们发现,问题就回到问题规模为n-1次的操作:将某塔台上的n-1个圆盘移动到某塔台。比如原来求解3个圆盘的搬移,变成了求解2个圆盘的搬移了。没错,这时候我们可以根据这一性质推导递推公式了。我们假设f(n)为将n个圆盘从某塔台搬运到某塔台的最小步数,那么可以得出递推公式f(n):
- 将n-1个圆盘从A搬运到B,这时A中只有编号为n的一个圆盘,C中无圆盘;
- 将A中编号为n的圆盘搬运到C,A无圆盘,C有一个编号为n的圆盘;
- 回到规模为n-1次的问题,将n-1圆盘从B搬运到C,递归结束。
f(n) = f(n - 1) + 1 + f(n - 1),f(n) = 2f(n - 1) + 1。因为不论是将n-1个圆盘从A搬运到B 还是 将n-1圆盘从B搬运到C,其实都是将n-1个圆盘从某塔台搬运到某塔台,f(n - 1)。所以,根据递推公式,就可以很轻松地得出最小步数:2^n - 1。
还没听明白的小伙伴,最好自己在图上分析n=1、2、3时搬运的步骤。同时,在推导到关键步骤时,结合以上的分析,就能明白其中的含义。
三、代码实现
理解了以上的分析,代码的思路油然而生:先将n-1个圆盘从A至B,再将圆盘n从A搬运至C,最后将剩下n-1个圆盘从B至C,结束。所以,通过递归的方式,三行代码就可以完成以上思路,再加上边界n=1时的判断递归返回,就简单地完成了 。
#include <iostream>
using namespace std;
void visit(int n, char x, char y)
{
static int count = 1;
printf("%d:把编号%d从%c搬运到%c\n", count++, n, x, y);
}
void Hanoi(int n, char a, char b, char c)//实则真正操作的是a和c
{
if (n == 1)
{
visit(1, a, c);
}
else
{
Hanoi(n - 1, a, c, b);//首先将a搬运至b,注意第一个和第三个操作数。
visit(n, a, c);//将编号n的圆盘从A搬运到C
Hanoi(n - 1, b, a, c);//接着将b搬运至c
}
}
int main()
{
int n = 3;
cout << "需要搬运的次数:" << pow(2, n) - 1 << endl;//搬运次数:2^n - 1
Hanoi(n,'A', 'B', 'C');
}
点个赞再走呀~~~