从方格游戏到动态规划:用Python手把手解‘踩方格’问题(附两种递推思路对比)

想象你站在一个无限延伸的方格纸上,每次只能向北、东或西迈出一步。但有个有趣的规则:走过的格子会立刻塌陷,无法回头。当允许走n步时,究竟有多少种独特的行走方案?这个问题看似简单,却蕴含着动态规划的精妙思想。今天,我们将用Python从零开始拆解这个经典问题,通过可视化手段让抽象的递推关系变得触手可及。

1. 问题建模与直观理解

让我们先用一个2步的案例建立直观感受。当n=1时,有三种选择:北、东、西。当n=2时,情况开始变得有趣:

  • 如果第一步向北,第二步仍有三个选择(新位置的北、东、西)
  • 如果第一步向东,第二步只能选择北或东(不能向西,因为会踩到已塌陷的起点)
  • 第一步向西同理,第二步只能选北或西

通过枚举所有可能性,我们得到7种方案。这个数字看似随机,实则隐藏着规律。要理解这个规律,我们需要建立状态定义:

# 状态定义示例
def count_paths_naive(n):
    if n == 1: return 3
    if n == 2: return 7
    # 更复杂的递归逻辑...

但这种暴力枚举法在n增大时会遇到性能瓶颈。我们需要更聪明的办法——动态规划。

2. 第一种递推思路:状态压缩法

观察n=1到n=4的结果序列:3, 7, 17, 41... 能发现每个数字都是前一个数字的两倍加上再前一个数字。这就是我们的第一个递推公式:

f(n) = 2 × f(n-1) + f(n-2)

为什么这样设计?因为:

  • 上一步向东/西的走法:每种都会在当前步产生2种新走法(不能回头)
  • 上一步向北的走法:每种都会在当前步产生3种新走法
  • 但上一步向北的走法数量恰好等于大上一步的所有走法

用Python实现这个逻辑:

def count_paths_simple(n):
    if n == 1: return 3
    if n == 2: return 7
    dp = [0] * (n + 1)
    dp[1], dp[2] = 3, 7
    for i in range(3, n + 1):
        dp[i] = 2 * dp[i-1] + dp[i-2]
    return dp[n]

这种方法的优势在于状态定义简单,只需要一维数组。但它的缺点是掩盖了行走方向的细节,不利于理解问题本质。

3. 第二种递推思路:双状态法

更精细的做法是区分最后一步的方向。我们定义两个状态:

  • a[i] : 第i步最后向北的方案数
  • b[i] : 第i步最后向东或西的方案数

递推关系变得直观:

  • 当前向北的走法 = 上一步所有走法(因为无论上一步方向如何,都可以转向北)
  • 当前向东/西的走法 = 上一步向北的走法×2 + 上一步同方向的走法

Python实现如下:

def count_paths_detailed(n):
    if n == 1: return 3
    a = [0] * (n + 1)  # 最后一步向北
    b = [0] * (n + 1)  # 最后一步向东或西
    a[1], b[1] = 1, 2
    for i in range(2, n + 1):
        a[i] = a[i-1] + b[i-1]
        b[i] = 2 * a[i-1] + b[i-1]
    return a[n] + b[n]

这种方法的计算过程可以清晰展示在表格中:

步数n a[n] (北) b[n] (东西) 总和
1 1 2 3
2 3 4 7
3 7 10 17
4 17 24 41

4. 可视化验证与调试技巧

为了验证我们的理解,可以用Python的turtle库模拟走法:

import turtle

def draw_path(directions):
    t = turtle.Turtle()
    t.speed(0)
    visited = set()
    pos = (0, 0)
    visited.add(pos)
    
    for d in directions:
        x, y = pos
        if d == 'N': new_pos = (x, y+1)
        elif d == 'E': new_pos = (x+1, y)
        else: new_pos = (x-1, y)
        
        if new_pos in visited:
            t.color('red')
            t.goto(new_pos[0]*30, new_pos[1]*30)
            return False
        
        visited.add(new_pos)
        t.goto(new_pos[0]*30, new_pos[1]*30)
        pos = new_pos
    return True

常见调试陷阱包括:

  1. 数组越界(确保n=1和n=2的基础情况)
  2. 整数溢出(Python不用担心,但其他语言需注意)
  3. 方向混淆(东和西的对称性)

5. 算法优化与扩展思考

虽然两种方法时间复杂度都是O(n),但空间可以优化到O(1):

def count_paths_optimized(n):
    if n == 1: return 3
    if n == 2: return 7
    prev_prev, prev = 3, 7
    for _ in range(3, n+1):
        current = 2 * prev + prev_prev
        prev_prev, prev = prev, current
    return prev

这个问题还可以扩展:

  • 如果允许停留原地,递推公式如何变化?
  • 如果格子不会塌陷,变成经典的随机游走问题
  • 在三维空间中的推广(增加上下方向)

理解这个问题的核心在于抓住状态定义的粒度——有时更细的状态划分反而让转移方程更直观。这也是动态规划的艺术所在:在状态复杂度和转移简洁性之间找到平衡点。

更多推荐