[Python] 借助 Turtle 来展示汉诺塔

背景

turtle --- Turtle graphics 中提供了关于 turtle\text{turtle} turtle 模块的介绍。我想到可以用 turtle\text{turtle} turtle 来展示汉诺塔 ( Tower of Hanoi\text{Tower of Hanoi} Tower of Hanoi) 的移动过程。于是写了本文。本文有两个主要部分

  • 提供展示汉诺塔操作的完整 Python\text{Python} 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\text{hanoi\_turtle.py} hanoi_turtle.py。用以下命令可以运行 hanoi_turtle.py\text{hanoi\_turtle.py} hanoi_turtle.py

bash 复制代码
python3 hanoi_turtle.py

初始效果如下图所示 ⬇️

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

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

最小移动次数分析

相较于汉诺塔的实现,我更关心其背后的数学知识。我在 具体数学(英文版第2版) 一书第 11 1 章 Recurrent Problems\text{Recurrent Problems} Recurrent Problems 中的 1.11.1 1.1 小节 THE TOWER OF HANOI\text{THE TOWER OF HANOI} THE TOWER OF HANOI 里,看到了相关的分析。按照我的理解,转述如下(有兴趣的读者朋友,可以看看原文)。

我们将汉诺塔问题进行一般化:记盘子的数量为 nn n( nn n 是正整数),三根柱子分别称为 A,B,CA,B,C A,B,C,我们想把 nn n 个盘子从 AA A 移动到另一个柱子 BB B 或者 CC C,将所需要的最小移动次数称为 Tn T_n Tn。

先观察最简单的情况。

  • n=1n=1 n=1 时,把这个盘子从 AA A 移动到 BB B 或者 CC C 即可,所以 T1=1 T_1=1 T1=1。
  • n=2n=2 n=2 时
    • 只操作一次,肯定不行(因为两个盘子至少都得移动一次)
    • 只操作两次,也做不到(暴力枚举即可,因为可行的操作非常有限)
    • 操作三次是可以的(如果目标柱子是 CC C,那么可以这样操作:较小的盘子 A→BA\to B A→B,然后较大的盘子 A→CA\to C A→C,然后较小的盘子 B→CB\to C B→C),所以 T2=3 T_2=3 T2=3。

nn n 更大时,我们总是可以使用如下的三大步策略(假设目标柱子是 CC C)。

  1. 将除了最大盘子之外,其他 n−1n-1 n−1 个盘子从 AA A 移动到 BB B (通过 Tn−1 T_{n-1} Tn−1 次移动可以做到)
  2. 将最大的盘子从 AA A 移动到 CC C (通过 11 1 次移动可以做到)
  3. 将除了最大盘子之外,其他 n−1n-1 n−1 个盘子从 BB B 移动到 CC C (通过 Tn−1 T_{n-1} Tn−1 次移动可以做到)

因为我们这个策略只是可行策略之一,所以可以得出以下不等式 ⬇️

所以 Tn≤ Tn−1 +1+ Tn−1 T_n \le T_{n-1} + 1 + T_{n-1} Tn≤Tn−1+1+Tn−1,即 Tn≤2 Tn−1 +1 T_n \le 2 T_{n-1} + 1 Tn≤2Tn−1+1

至此,我们得到了 Tn T_n Tn 的上界。那么这个策略是不是最优的策略呢?(即,这个策略所需要的移动步数是最少的吗?)。我们来分析一下。

对任意一个成功将 nn n 个盘子从 AA A 移动到 CC C 的完整流程而言,这个流程一定需要移动最大的那个盘子 Pbiggest P_{biggest} Pbiggest。那么以下推断都成立 ⬇️

  1. 在第一次移动 Pbiggest P_{biggest} Pbiggest 之前,我们已经将其余 n−1n-1 n−1 个盘子移动到 BB B 或者 CC C 上了(按照规则,一个盘子在移动时,其上方不能有其他盘子)
  2. 我们至少需要移动 Pbiggest P_{biggest} Pbiggest 一次(否则这个盘子不可能到目标柱子上)
  3. 在最后一次移动 Pbiggest P_{biggest} Pbiggest 之后,我们一定需要将其余 n−1n-1 n−1 个盘子移动到 CC C 上

所以,对这个完整流程而言,它的移动次数至少是 Tn−1 +1+ Tn−1 =2 Tn−1 +1 T_{n-1} + 1 + T_{n-1}=2T_{n-1}+1 Tn−1+1+Tn−1=2Tn−1+1。

于是我们得到 Tn≥2 Tn−1 +1 T_n \ge 2 T_{n-1} + 1 Tn≥2Tn−1+1 (即, nn n 个盘子时的最优操作数至少是 2 Tn−1 +1 2T_{n-1}+1 2Tn−1+1)

既然以下两个不等式都成立
Tn≤2 Tn−1 +1 T_n \le 2 T_{n-1} + 1 Tn≤2Tn−1+1
Tn≥2 Tn−1 +1 T_n \ge 2 T_{n-1} + 1 Tn≥2Tn−1+1

那么,可以得出 Tn=2 Tn−1 +1 T_n = 2 T_{n-1} + 1 Tn=2Tn−1+1。

利用高中时学的数列小技巧,我们可以推出 Tn T_n Tn 的通项公式。具体过程如下 ⬇️

Tn=2 Tn−1 +1 T_n = 2 T_{n-1} + 1 Tn=2Tn−1+1 这个等式两边各自加 11 1,得到
Tn+1=2( Tn−1 +1) T_n + 1 = 2 (T_{n-1} + 1) Tn+1=2(Tn−1+1)

Un=Tn+1 U_n=T_n+1 Un=Tn+1,那么 Un U_n Un 是以 22 2 为首项, 22 2 为公比的等比数列,所以 Un U_n Un 的通项公式为
Un=2n U_n=2^n Un=2n

那么 Tn=Un−1=2n−1 T_n=U_n-1=2^n-1 Tn=Un−1=2n−1。也就是说,当 AA A 柱子上有 nn n 个盘子时,我们需要 2n−12^n-1 2n−1 次移动操作,才能将这 nn n 个盘子移动到 BB B 柱子上(或者 CC C 柱子上)。

相关推荐
暴躁的小鸟2 小时前
附近口碑好的斜视配镜训练的眼视光中心
人工智能·python·深度学习
Data_Journal2 小时前
如何使用 Java 和 Jsoup 解析 HTML
大数据·开发语言·数据库·python·scrapy
如何原谅奋力过但无声2 小时前
现代化Python工具:uv、Ruff、Pyright、Rich(只讲重点版)
vscode·python·uv
Logintern092 小时前
DeepAgents 主-子 Agent 的底层实现逻辑完全基于 LangGraph 的图机制
python
雪碧聊技术3 小时前
【数学/高数】洛必达法则详解:从原理到实战例题(附手写笔记解析)
数学·洛必达法则·等价无穷小
码云骑士3 小时前
97-Milvus从零到生产-Standalone-vs-Cluster-索引选择-分片监控扩容
python·milvus
accept 99%3 小时前
python版提取 PDF 的常用第三方库与工具
开发语言·python·pdf
小猴子爱上树3 小时前
跨境图片翻译工具,批量处理商品图视频字幕
python·音视频
用户298698530143 小时前
HTML 转 Word 指南:新手入门教程
人工智能·后端·python