生椰丝绒:
你好,tatan,欢迎来到我的教室。今天我们要了解一个经典问题,叫做汉诺塔问题。
tatan:
kakakakakakka,你好!汉诺塔?
听起来好像是一个游戏,是不是要把一些圆盘从一个柱子移到另一个柱子?
生椰丝绒:
没错,你很聪明。汉诺塔问题是这样的,有三根柱子,分别叫做A、B和C,其中A柱子上有n个大小不同的圆盘,从小到大依次叠在一起,如下图所示:
tatan:
哇,好漂亮的圆盘,有好几种颜色呢。
生椰丝绒:
颜色不重要,重要的是大小。我们的目标是把这些圆盘全部移到C柱子上,但是有一些规则要遵守:
- 每次只能移动一个圆盘。
- 移动过程中不能出现大圆盘在小圆盘上方的情况。
- 可以借助B柱子作为中转。
tatan:
这听起来好难啊,有没有什么技巧呢?
生椰丝绒:
有的,其实这个问题可以用递归的思想来解决。你知道什么是递归吗?
tatan:
递归?好像是一个函数调用自己的方法,是吗?
生椰丝绒:
对,递归就是一个函数在执行过程中调用自己,这样可以把一个复杂的问题分解成更小的子问题,直到子问题可以直接解决为止。递归的函数通常有两个部分,一个是基本情况,也就是最简单的情况,可以直接给出答案;另一个是递归情况,也就是把问题转化成更小的问题,然后调用自己来解决。
tatan:
那汉诺塔问题怎么用递归来解决呢?
生椰丝绒:
我们可以这样想,如果只有一个圆盘,那么我们直接把它从A柱子移到C柱子就可以了,这是基本情况。如果有两个或以上的圆盘,那么我们可以先把除了最大的圆盘之外的所有圆盘从A柱子移到B柱子,这是一个子问题,可以用递归来解决;然后把最大的圆盘从A柱子移到C柱子,这是一个简单的操作;最后把B柱子上的所有圆盘从B柱子移到C柱子,这又是一个子问题,也可以用递归来解决。这就是递归情况。你明白了吗?
tatan:
我好像有点懂了,就是把一个大问题分成两个小问题,然后再分别解决,是吗?
生椰丝绒:
是的,递归的思考方式就是把大事化小的过程。
用C语言写这个递归函数很简单,我们只需要定义一个函数,叫做hanoi,它有四个参数,分别是n、a、b和c,表示要把n个圆盘从a柱子移到c柱子,借助b柱子作为中转。
然后在函数体中,我们先判断n是否等于1,
如果是,就直接输出把圆盘从a柱子移到c柱子的操作,例如“Move disk 1 from A to C”;
如果不是,就先调用hanoi函数,把n-1个圆盘从a柱子移到b柱子,借助c柱子作为中转,然后输出把最大的圆盘从a柱子移到c柱子的操作,例如“Move disk n from A to C”;
最后再调用hanoi函数,把n-1个圆盘从b柱子移到c柱子,借助a柱子作为中转。
这样就完成了递归函数的定义。你看看这段代码,能看懂吗?
// 定义汉诺塔递归函数 void hanoi(int n, char a, char b, char c) { // 如果只有一个圆盘,直接输出操作 if (n == 1) { printf("Move disk 1 from %c to %c\n", a, c); } else { // 否则,先把n-1个圆盘从a柱子移到b柱子,借助c柱子 hanoi(n - 1, a, c, b); // 然后把最大的圆盘从a柱子移到c柱子 printf("Move disk %d from %c to %c\n", n, a, c); // 最后把n-1个圆盘从b柱子移到c柱子,借助a柱子 hanoi(n - 1, b, a, c); } }
tatan:
我觉得我大概能看懂,就是按照刚才说的逻辑来写代码,是吗?
生椰丝绒:
是的,你很棒。那我们来试一下,如果有三个圆盘,我们要把它们从A柱子移到C柱子,我们应该怎么调用这个函数呢?
tatan:
是不是就是hanoi(3, ‘A’, ‘B’, ‘C’)?
生椰丝绒:
对,你说对了。那我们来看看这个函数会输出什么结果吧:
// 调用汉诺塔递归函数 hanoi(3, 'A', 'B', 'C'); // 输出结果 Move disk 1 from A to C Move disk 2 from A to B Move disk 1 from C to B Move disk 3 from A to C Move disk 1 from B to A Move disk 2 from B to C Move disk 1 from A to C
第一步,把最小的圆盘(编号为1)从A柱子移到C柱子;
第二步,把中等大小的圆盘(编号为2)从A柱子移到B柱子;
第三步,把最小的圆盘(编号为1)从C柱子移到B柱子;
第四步,把最大的圆盘(编号为3)从A柱子移到C柱子;
第五步,把最小的圆盘(编号为1)从B柱子移到A柱子;
第六步,把中等大小的圆盘(编号为2)从B柱子移到C柱子;
第七步,把最小的圆盘(编号为1)从A柱子移到C柱子。
这样,就完成了把三个圆盘从A柱子移到C柱子的操作,你看懂了吗?
tatan:
哇,好神奇,这样就可以把三个圆盘都移到C柱子上了。那如果有更多的圆盘呢,比如十个圆盘,会不会很复杂呢?
生椰丝绒:
其实不会,我们只需要把3改成10,就可以了,其他的都不用变。你想想,这个函数的逻辑是不是和圆盘的数量无关呢?
tatan:
是的,好像是这样。
生椰丝绒:
好,我们来看看吧:
// 调用汉诺塔递归函数 hanoi(10, 'A', 'B', 'C'); // 输出结果 Move disk 1 from A to C Move disk 2 from A to B Move disk 1 from C to B Move disk 3 from A to C Move disk 1 from B to A Move disk 2 from B to C Move disk 1 from A to C Move disk 4 from A to B Move disk 1 from C to B Move disk 2 from C to A Move disk 1 from B to A Move disk 3 from C to B Move disk 1 from A to C ......
tatan:
这样输出也太多了吧,QAQ
生椰丝绒:
是的,这样输出的操作太多了,不方便查看。其实,我们可以不用输出每一步的操作,因为移动方法都是类似的。现在,我们换一下思路,想想怎么用一个公式来计算汉诺塔问题的解法步骤数。你想想,这个公式应该是什么呢?
tatan:
公式?我不太擅长数学,你能给我一些提示吗?
生椰丝绒:
好的,我来给你一些提示。你看,如果只有一个圆盘,我们只需要一步就可以把它从A柱子移到C柱子,对吗?
tatan:
对,这很简单。
生椰丝绒:
那如果有两个圆盘呢,我们需要几步呢?
tatan:
两个圆盘的话,我记得刚才说过,需要三步:
先把小的圆盘从A柱子移到B柱子,
然后把大的圆盘从A柱子移到C柱子,
最后把小的圆盘从B柱子移到C柱子。
生椰丝绒:
你说得对。那如果有三个圆盘呢,我们需要几步呢?
tatan:
三个圆盘的话,我记得刚才输出的结果,需要七步。
生椰丝绒:
你发现了什么规律吗?
tatan:
规律?我好像发现了,每次增加一个圆盘,就要多两倍的步骤,再加一步,是吗?
生椰丝绒:
是的,你很聪明,这就是汉诺塔问题的公式,用数学语言来表示,就是:
f(n)=2f(n−1)+1
这个公式的意思是,如果有n个圆盘,那么需要的步骤数是前一个圆盘数的两倍,再加一。这个公式是一个递推公式,也就是说,它是用前一个状态来推导出后一个状态的。你看懂了吗?
tatan:
我觉得我大概能看懂,就是用一个简单的公式来表示复杂的问题,是吗?
生椰丝绒:
是的,你很棒。那我们来试一下,如果有十个圆盘,我们要把它们从A柱子移到C柱子,我们应该怎么用这个公式来计算呢?
tatan:
我们可以用这个公式来计算,如果有十个圆盘,我们需要的步骤数是:
f(10)=2f(9)+1
但是,我们还不知道f(9)是多少,所以我们还要继续用公式来推导:
f(9)=2f(8)+1
生椰丝绒:
嗯嗯,同理,我们还要继续推导f(8)、f(7)、f(6)、f(5)、f(4)、f(3)、f(2)和f(1),直到我们得到f(1)的值,也就是1。然后,我们就可以从下往上,依次计算出f(2)、f(3)、f(4)、f(5)、f(6)、f(7)、f(8)、f(9)和f(10)的值,如下:
f(1)=1
f(2)=2f(1)+1=2×1+1=3
f(3)=2f(2)+1=2×3+1=7
f(4)=2f(3)+1=2×7+1=15
f(5)=2f(4)+1=2×15+1=31
f(6)=2f(5)+1=2×31+1=63
f(7)=2f(6)+1=2×63+1=127
f(8)=2f(7)+1=2×127+1=255
f(9)=2f(8)+1=2×255+1=511
f(10)=2f(9)+1=2×511+1=1023
所以,如果有十个圆盘,我们需要的步骤数是1023,这是一个很大的数字,你能想象吗?
tatan:
哇,这么多步骤,我都不敢想象。那如果有一百个圆盘呢,会不会更多呢?
生椰丝绒:
当然会更多,如果有一百个圆盘,我们需要的步骤数是:
f(100)=2f(99)+1
但是,我们不用像刚才那样,一步一步地推导,我们可以用一个更简单的公式来表示,你想想,这个公式应该是什么呢?
tatan:
我归纳归纳,嗯,好像是
生椰丝绒:
是的,你很棒。那么总步骤数的问题就迎刃而解了。