问题
初始时,给定3根柱子A,B,C,N个大小不一的圆盘,这些圆盘从小到大排列在其中的一根柱子上,假设为A,现在要通过B,将这N个圆盘全部移动到C柱子上,每次只能移动一个,移动的过程中不许出现大盘子放在小盘上面的情况,问完成这个任务至少需要将盘子移动多少次?
分析和程序
设N个盘子至少需要T(N)次移动,显然T(0)=0,T(1)=1。考虑到要将N个盘子从A通过B的辅助移动到C,则必须将A上的N-1个先从A通过C的辅助移动到B,然后将A上的最大的盘子从A移动到C,最后将B上的N-1个盘子从B通过A的辅助移动到C。上面没有直接说从A移动到C,而是啰嗦说:从A通过B的辅助移动到C,是为了表明“将N个盘子从A通过B的辅助移动到C”与将“A上的N-1个先从A通过C的辅助移动到B”相对于移动次数来说,虽然移动方向变了,但本质是一样的,只不过是问题的规模变小了,N个盘子和N-1个盘子的问题,一个最小次数为T(n),一个为T(n-1)。
好了,继续分析,n个盘子移动时,通过T(n-1)次的从A到B,1次的从A到C,T(n-1)次的从B到C,一共是2T(n-1)+1次,完全可以把问题搞定。但要找的是将N个盘子从A移到C的最少次数,显然发现了一个数不能就说它是最小的次数了,应该贪婪点,继续找!在继续找的过程中,至少有了个前提,我们找的不能比2T(n-1)+1次大,这样我们就有第一个等式了,T(n)<=2T(n-1)+1。
但贪婪归贪婪,能否找到呢?回过头来想,T(n-1)次的从A到B,1次的从A到C,T(n-1)次的从B到C,这2T(n-1)+1次移动都是我们必须做的,而且每步中已经不可能有更少次数的方法来代替了(我们已经假定T(n-1)次是完成N-1个盘子的任务的最少移动次数,这个次数是确定的),所以总的的次数不会小于这个次数,于是有了第二个等式,T(n)>=2T(n-1)+1。
结合起来,就有了 T(n)=2T(n-1)+1,T(0)=1[1] ,用整体的思想来进行化简(左右两边各加1),我们不难推出总次数的显式解:T(n)=2^n-1,有了这些思想,就可以很容易的编程了:
/*gigglesun 2010/4/16 ** **The tower of hanoi problem:When given a tower of disks stacked in decreasing size **,
* how to transfer the entire tower to one of the other pegs,one at a time and never a
* larger one onto a smaller one **/
#include
using namespace std;
#define FROM 'A'
#define MID 'B'
#define TO 'C'
int cnt = 0;
void move(int diskNum, char f, char m, char t) {
if (diskNum == 1) {
cnt++;
cout << cnt << "/t" << f << "/t->/t" << t << endl;
}
else {
move(diskNum - 1, f, t, m);
move(1, f, m, t);
move(diskNum - 1, m, f, t);
}
}
int main() {
cout << "Pegs are put as A/|/tB/|/tC/|" << endl;
cout << "Input the number of disks on peg A/n";
int diskNum;
cin >> diskNum;
cout << "From A to C,we should move as follows:" << endl;
cout << "times/tfrom/t/tto" << endl;
move(diskNum, FROM, MID, TO);
return 0;
}
输出结果:
Pegs are put as A| B| C|
Input the number of disks on peg A
4
From A to C,we should move as follows:
times from to
1 A -> B
2 A -> C
3 B -> C
4 A -> B
5 C -> A
6 C -> B
7 A -> B
8 A -> C
9 B -> C
10 B -> A
11 C -> A
12 B -> C
13 A -> B
14 A -> C
15 B -> C
将N=4代入数学公式也是15,多试几个数,也正确,说明我们的分析很正确,数学很强大!
但不要高兴得太早,要是这时候你碰到一个苛刻的老板,说:A和C的距离太远,要是你一不小心将我的盘子打碎了怎么办?我不许你从A往C或是从C往A直接放盘子,所有的从A往C或是从C往A的盘子你都必须先往B上放下歇会再接着干,当然还是一次一个,小的必须在大的上面。于是你边干边想,N个盘子要搬多少次呢?
还好你够聪明:N个盘子,由于每个盘子都必须经过B,必须将A的上面N - 1个从A移到到C(T(n-1)次),再将最下面的从A移动到B(1次),由于B上的这个是最大的,你接着必须将C上的刚移到的N-1个再移回到A(T(n-1)次),移的过程中依然可以在B上歇脚(这一点很重要),才能将B上的这个盘子移到C (1次),这样你终于移完了一个盘子到C,现在接下来,你只要再将A上的这N-1个盘子从A移到C(T(n-1)次)。一次减少一个,越搬越少,全部移完就大功告成了。
同样,T(n)=T(n-1)+1+T(n-1)+1+T(n-1)=3T(n-1)+2,不难可以得出T(n)=3^n-1,算出来后你想,每个盘子“放”在A,B,C上的柱子上,一共才有3^n个可能状态,你搬了3^n-1次,加上初始的状态,你搬的过程中居然碰到了在A,B,C上的所有小盘在上,大盘在下的盘子的可能情况,oh,my god...
再拿计算机来检验下你的结果:
/*by gigglesun 2010/4/16 **
* **The tower of hanoi problem:When given a tower of disks stacked in
* decreasing size on **A,how to transfer the entire tower to one of the other peg C through B,one
* at a time , **and never a larger one onto a smaller one,besides, direct moves from A to C or C to
* A are disallowed. **/
#include
using namespace std;
#define FROM 'A'
#define MID 'B'
#define TO 'C'
int cnt = 0;
void move(int diskNum, char f, char m, char t) {
if (diskNum == 1 && ((f == FROM) && (t == TO) || (f == TO) && (t == FROM))) {
cnt++;
cout << cnt << "/t" << f << "/t->/t" << m << endl;
cnt++;
cout << cnt << "/t" << m << "/t->/t" << t << endl;
}
else if (diskNum == 1) {
cnt++;
cout << cnt << "/t" << f << "/t->/t" << t << endl;
}
else {
move(diskNum - 1, f, m, t);
move(1, f, t, m);
move(diskNum - 1, t, m, f);
move(1, m, f, t);
move(diskNum - 1, f, m, t);
}
}
int main() {
cout << "Input disks numbers in A:" << endl;
cout << "Pegs are put as A/|/tB/|/tC/|" << endl;
cout << "Input the number of disks on peg A/n";
int diskNum;
cin >> diskNum;
cout << "From A to C,we should move as follows:" << endl;
cout << "times/tfrom/t/tto" << endl;
move(diskNum, FROM, MID, TO);
return 0;
}
运行结果:
Input disks numbers in A:
Pegs are put as A| B| C|
Input the number of disks on peg A
3
From A to C,we should move as follows:
times from to
1 A -> B
2 B -> C
3 A -> B
4 C -> B
5 B -> A
6 B -> C
7 A -> B
8 B -> C
9 A -> B
10 C -> B
11 B -> A
12 C -> B
13 A -> B
14 B -> C
15 B -> A
16 C -> B
17 B -> A
18 B -> C
19 A -> B
20 B -> C
21 A -> B
22 C -> B
23 B -> A
24 B -> C
25 A -> B
26 B -> C
拿计算机检验后,发现老板的方法确实不是个好办法。你向老板抱怨你的辛苦,因为6个盘子的任务就至少要搬728次了,老板也体谅到了你的辛苦,但老板对盘子的安全问题依然很在乎。你向老板建议:将三柱改为四柱,多放了一根柱子歇脚,间距相等。这样,柱子和柱子间的距离更短了,老板觉得多了一根柱子,距离变短了,要求也没变化,感到很放心,答应了你的请求,并且怀疑你是否真能减少工作量。你是个聪明的数学家,早就算好多放一根柱子后需要的最少的搬运次数,你是这样想的:
据此,计算出总步数为f(n,r),随后对所有的r(1≤r≤n)逐一进行心算比较。选择一个r使得f(n,r)取最小值,你得到:
F(n)=minf(n,r)= min[2F(n - r) + T(r)]= min[2F(n - r) + 2^r - 1][2]
还好,你一下就知道一个盘子时需要1次(虽然这种搬法是前一个规则不允许的,但老板同意了),二个盘子时为3次,根据这,你发现现在搬六个盘子只需要17次就搞定了,甚至比最初的三柱的2^6-1=63还少,大大减少了你的工作量,你不禁为你的聪明自喜,至于过程,你也很快就想到了:
/*by gigglesun 2010/4/20 ** **The tower of hanoi problem:When given a tower of disks stacked in
* decreasing size on **A,how to transfer the entire tower to one of the other peg D through B,C,one
* at a time , **and never a larger one onto a smaller one,4 Pegs this time. **/
#include
#include
using namespace std;
#define FROM 'A'
#define MID1 'B'
#define MID2 'C'
#define TO 'D'
int cnt = 0;
int find_k(int diskNum) {
int k = ( (sqrt(8 * diskNum + 1) - 1) / 2);
return k;
}
void ThreePegHanoi(int disNum, char from, char mid , char to) {
if (disNum == 1) {
cnt++;
cout << cnt << "/t" << from << "/t->/t" << to << endl;
}
else {
ThreePegHanoi(disNum - 1, from, to, mid);
ThreePegHanoi(1, from, mid, to);
ThreePegHanoi(disNum - 1, mid, from, to);
}
}
void FourPegHanoi(int diskNum, char from, char mid1, char mid2, char to) {
int k = find_k(diskNum);
if (diskNum == 1) {
cnt++;
cout << cnt << "/t" << from << "/t->/t" << to << endl;
}
else {
FourPegHanoi(diskNum - k, from, mid2, to, mid1);
ThreePegHanoi(k, from, mid2, to);
FourPegHanoi(diskNum - k, mid1, from, mid2, to);
}
}
int main() {
cout << "Input disks numbers in A:" << endl;
cout << "Pegs are put as A/|/tB/|/tC/|/tD/|" << endl;
cout << "Input the number of disks on peg A/n";
int diskNum;
cin >> diskNum;
cout << "From A to D,we should move as follows:" << endl;
cout << "times/tfrom/t/tto" << endl;
FourPegHanoi(diskNum, FROM, MID1, MID2, TO);
return 0;
}
至此,搬盘子的任务终于被你的聪明才智化解了。
补充:曾在wiki上搜索hanoi,看到过一个Binary solutions,发现想法很巧妙漂亮,但仔细研究后感觉其给的例子有问题,找相关原始材料也很难找;我试着第一次按我的想法编辑了下这个小条目,居然能够保存,现在大家看到的那个Binary solutions的实例部分是我编辑后的结果。如果有谁实现过这个算法或有相关的资料,希望能和我交流分享下,我的邮箱gigglesun AT 163.com。
参考文献:
[1]concrete mathematics page1-4 second edition by Graham knuth patashnik
[2]四柱汉诺塔之初步探究 北京大学学报(自然科学版) ,第40卷,第1期,杨 楷 徐 川