背景
turtle --- Turtle graphics 中提供了关于 turtle 模块的介绍。我想到可以用 turtle 来展示汉诺塔 ( Tower of Hanoi) 的移动过程。于是写了本文。本文有两个主要部分
- 提供展示汉诺塔操作的完整 Python 代码
- 提供汉诺塔最小移动次数的分析
正文
完整的代码
我让 trae 帮我生成了相关的代码(我做了些调整),完整的代码如下 (为了简化互动过程,代码中只涉及4个盘子)
python
import turtle
# ===================== 游戏配置 =====================
NUM_DISKS = 4
TOWER_COUNT = 3
SCREEN_WIDTH = 700
SCREEN_HEIGHT = 500
TOWER_HEIGHT = 180
TOWER_WIDTH = 8
DISK_HEIGHT = 25
DISK_WIDTH_BASE = 50
TOWER_X = [-200, 0, 200]
TOWER_Y = -150
# ===================== 游戏数据 =====================
towers = [[4, 3, 2, 1], [], []]
selected = None
win = False
# ===================== 初始化屏幕和画笔 =====================
screen = turtle.Screen()
screen.setup(SCREEN_WIDTH, SCREEN_HEIGHT)
screen.title("汉诺塔 - Tower of Hanoi")
screen.tracer(0)
pen = turtle.Turtle()
pen.hideturtle()
# ===================== 绘制函数 =====================
def draw_tower(x, y, height):
pen.penup()
pen.goto(x - TOWER_WIDTH // 2, y)
pen.pendown()
pen.fillcolor("gray")
pen.begin_fill()
pen.goto(x + TOWER_WIDTH // 2, y)
pen.goto(x + TOWER_WIDTH // 2, y + height)
pen.goto(x - TOWER_WIDTH // 2, y + height)
pen.goto(x - TOWER_WIDTH // 2, y)
pen.end_fill()
def draw_disk(x, y, disk_size, is_selected=False):
width = DISK_WIDTH_BASE + disk_size * 25
pen.penup()
pen.goto(x - width // 2, y)
pen.pendown()
if is_selected:
pen.fillcolor("yellow")
else:
pen.fillcolor("orange" if disk_size % 2 == 0 else "skyblue")
pen.begin_fill()
pen.goto(x + width // 2, y)
pen.goto(x + width // 2, y + DISK_HEIGHT)
pen.goto(x - width // 2, y + DISK_HEIGHT)
pen.goto(x - width // 2, y)
pen.end_fill()
pen.pencolor("gray")
pen.pensize(2)
pen.goto(x + width // 2, y)
pen.goto(x + width // 2, y + DISK_HEIGHT)
pen.goto(x - width // 2, y + DISK_HEIGHT)
pen.goto(x - width // 2, y)
pen.pensize(1)
pen.pencolor("black")
def draw_text(msg, y_offset=150):
pen.penup()
pen.goto(0, y_offset)
pen.pendown()
pen.write(msg, align="center", font=("Arial", 18, "normal"))
def redraw():
pen.clear()
for i in range(TOWER_COUNT):
draw_tower(TOWER_X[i], TOWER_Y, TOWER_HEIGHT)
for tower_idx in range(TOWER_COUNT):
tower = towers[tower_idx]
x = TOWER_X[tower_idx]
for disk_idx, disk_size in enumerate(tower):
y = TOWER_Y + disk_idx * DISK_HEIGHT
is_selected = (selected == tower_idx)
draw_disk(x, y, disk_size, is_selected)
if win:
draw_text("🎉 You Win! 🎉")
elif selected is not None:
draw_text(
f"Selected Tower {selected + 1} - Click target tower to move"
)
else:
draw_text("Click a tower to pick a disk, then click target tower")
screen.update()
# ===================== 游戏逻辑 =====================
def move_disk(from_tower, to_tower):
if not towers[from_tower]:
return False
if not towers[to_tower] or towers[from_tower][-1] < towers[to_tower][-1]:
towers[to_tower].append(towers[from_tower].pop())
return True
return False
def check_win():
return len(towers[2]) == NUM_DISKS
# ===================== 点击事件处理 =====================
def on_click(x, y):
global selected, win
if win:
return
click_tower = None
for i in range(TOWER_COUNT):
if abs(x - TOWER_X[i]) < 80:
click_tower = i
break
if click_tower is not None:
if selected is None:
if towers[click_tower]:
selected = click_tower
else:
if move_disk(selected, click_tower):
win = check_win()
selected = None
redraw()
# ===================== 启动游戏 =====================
screen.onclick(on_click)
redraw()
screen.mainloop()
请将上述代码保存为 hanoi_turtle.py。用以下命令可以运行 hanoi_turtle.py
bash
python3 hanoi_turtle.py
初始效果如下图所示 ⬇️

运行中的效果如下图所示 ⬇️

胜利时的效果如下图所示 ⬇️

最小移动次数分析
相较于汉诺塔的实现,我更关心其背后的数学知识。我在 具体数学(英文版第2版) 一书第 1 章 Recurrent Problems 中的 1.1 小节 THE TOWER OF HANOI 里,看到了相关的分析。按照我的理解,转述如下(有兴趣的读者朋友,可以看看原文)。
我们将汉诺塔问题进行一般化:记盘子的数量为 n( n 是正整数),三根柱子分别称为 A,B,C,我们想把 n 个盘子从 A 移动到另一个柱子 B 或者 C,将所需要的最小移动次数称为 Tn。
先观察最简单的情况。
- n=1 时,把这个盘子从 A 移动到 B 或者 C 即可,所以 T1=1。
- n=2 时
- 只操作一次,肯定不行(因为两个盘子至少都得移动一次)
- 只操作两次,也做不到(暴力枚举即可,因为可行的操作非常有限)
- 操作三次是可以的(如果目标柱子是 C,那么可以这样操作:较小的盘子 A→B,然后较大的盘子 A→C,然后较小的盘子 B→C),所以 T2=3。
当 n 更大时,我们总是可以使用如下的三大步策略(假设目标柱子是 C)。
- 将除了最大盘子之外,其他 n−1 个盘子从 A 移动到 B (通过 Tn−1 次移动可以做到)
- 将最大的盘子从 A 移动到 C (通过 1 次移动可以做到)
- 将除了最大盘子之外,其他 n−1 个盘子从 B 移动到 C (通过 Tn−1 次移动可以做到)
因为我们这个策略只是可行策略之一,所以可以得出以下不等式 ⬇️
所以 Tn≤Tn−1+1+Tn−1,即 Tn≤2Tn−1+1
至此,我们得到了 Tn 的上界。那么这个策略是不是最优的策略呢?(即,这个策略所需要的移动步数是最少的吗?)。我们来分析一下。
对任意一个成功将 n 个盘子从 A 移动到 C 的完整流程而言,这个流程一定需要移动最大的那个盘子 Pbiggest。那么以下推断都成立 ⬇️
- 在第一次移动 Pbiggest 之前,我们已经将其余 n−1 个盘子移动到 B 或者 C 上了(按照规则,一个盘子在移动时,其上方不能有其他盘子)
- 我们至少需要移动 Pbiggest 一次(否则这个盘子不可能到目标柱子上)
- 在最后一次移动 Pbiggest 之后,我们一定需要将其余 n−1 个盘子移动到 C 上
所以,对这个完整流程而言,它的移动次数至少是 Tn−1+1+Tn−1=2Tn−1+1。
于是我们得到 Tn≥2Tn−1+1 (即, n 个盘子时的最优操作数至少是 2Tn−1+1)
既然以下两个不等式都成立
Tn≤2Tn−1+1
Tn≥2Tn−1+1
那么,可以得出 Tn=2Tn−1+1。
利用高中时学的数列小技巧,我们可以推出 Tn 的通项公式。具体过程如下 ⬇️
在 Tn=2Tn−1+1 这个等式两边各自加 1,得到
Tn+1=2(Tn−1+1)
记 Un=Tn+1,那么 Un 是以 2 为首项, 2 为公比的等比数列,所以 Un 的通项公式为
Un=2n
那么 Tn=Un−1=2n−1。也就是说,当 A 柱子上有 n 个盘子时,我们需要 2n−1 次移动操作,才能将这 n 个盘子移动到 B 柱子上(或者 C 柱子上)。