
本文详解为何在实现「最大加号标志」问题时使用记忆化递归会触发 recursionerror,指出其根本原因在于无方向约束导致无限回溯,并提供高效、可扩展的四向预处理动态规划解法。
本文详解为何在实现「最大加号标志」问题时使用记忆化递归会触发 recursionerror,指出其根本原因在于无方向约束导致无限回溯,并提供高效、可扩展的四向预处理动态规划解法。
在 leetcode 第 764 题「最大加号标志」中,目标是找出以某点为中心、四个方向(上、下、左、右)延伸出的连续非矿格(即非 mines 中坐标)所能构成的最大「加号」阶数(order)。初学者常尝试用记忆化递归(如 @cache + DFS)直接模拟“从中心向外延展”,但很快会遇到 RecursionError: maximum recursion depth exceeded —— 即使已添加边界和矿格检查,也依然失败。
根本原因在于逻辑缺陷:递归未建模「方向性」
观察原代码中的 helper(r, c):
def helper(r,c): if r >= n or r < 0 or c < 0 or c >= n or (r,c) in mines: return 0 return 1 + min( helper(r + 1, c), # 下 helper(r - 1, c), # 上 helper(r, c + 1), # 右 helper(r, c - 1) # 左 )
该函数试图表达:“以 (r,c) 为起点,能向四个方向各延伸多远?取最小值即为该点的加号阶数”。但问题在于:helper(r+1,c) 会调用 helper(r,c)(因为向上走一步就回到原点),而 helper(r,c) 又再次调用 helper(r+1,c)……形成 无终止的双向依赖环。即使有 @cache,缓存的是 (r,c) 的返回值,但该值本身依赖于尚未计算完成的邻居——而由于无方向约束,递归图存在环,导致栈深度线性增长直至溢出。
✅ 正确思路:将“中心向外延伸”转化为“单向累积统计”
我们不再对每个点做四向 DFS,而是预处理四个方向的连续安全格长度:
- top[i][j]:从第 i 行第 j 列向上(含自身)最多有多少个连续非矿格;
- bot[i][j]:向下;
- left[i][j]:向左;
- right[i][j]:向右。
每个方向均可通过一次扫描 O(n²) 完成,且天然无环、无回溯。
以下是优化后的完整实现(已适配 Python 3.9+,使用 range(n) 和解包语法):
class Solution: def orderOfLargestPlusSign(self, n: int, mines: List[List[int]]) -> int: # 将 mines 转为集合,支持 O(1) 查找 mine_set = {tuple(mine) for mine in mines} R = range(n) # 初始化四向 DP 数组 top = [[0] * n for _ in R] bot = [[0] * n for _ in R] left = [[0] * n for _ in R] right = [[0] * n for _ in R] # ? 从左到右、从上到下:计算 top 和 left for i in R: s_top = s_left = 0 for j in R: # top[i][j]:第 i 行、第 j 列向上连续安全格数(含本格) if (i, j) in mine_set: s_top = 0 else: s_top += 1 top[i][j] = s_top # left[j][i]:注意此处巧妙转置——用于后续计算第 i 列从上到下的 left 累积? # 实际更清晰的做法是分开两层循环;下方采用标准四向扫描法重写以增强可读性 # ✅ 推荐更清晰的四次独立扫描(见下方修正版)
为提升可读性与正确性,我们采用四次明确方向的线性扫描:
class Solution: def orderOfLargestPlusSign(self, n: int, mines: List[List[int]]) -> int: mine_set = {tuple(mine) for mine in mines} R = range(n) # 四个方向 DP 表:dp_dir[i][j] 表示从 (i,j) 出发沿 dir 方向的连续安全格数量(含自身) up = [[0] * n for _ in R] down = [[0] * n for _ in R] left = [[0] * n for _ in R] right = [[0] * n for _ in R] # ? 向上扫描:每列从上到下累积 for j in R: cnt = 0 for i in R: if (i, j) in mine_set: cnt = 0 else: cnt += 1 up[i][j] = cnt # ? 向下扫描:每列从下到上累积 for j in R: cnt = 0 for i in reversed(R): if (i, j) in mine_set: cnt = 0 else: cnt += 1 down[i][j] = cnt # ◀️ 向左扫描:每行从左到右累积 for i in R: cnt = 0 for j in R: if (i, j) in mine_set: cnt = 0 else: cnt += 1 left[i][j] = cnt # ▶️ 向右扫描:每行从右到左累积 for i in R: cnt = 0 for j in reversed(R): if (i, j) in mine_set: cnt = 0 else: cnt += 1 right[i][j] = cnt # ? 对每个位置 (i,j),其最大加号阶数 = 四方向最小值 ans = 0 for i in R: for j in R: cross = min(up[i][j], down[i][j], left[i][j], right[i][j]) ans = max(ans, cross) return ans
关键优势总结:
- ✅ 时间复杂度:O(n²),远优于错误递归的指数/无限复杂度;
- ✅ 空间复杂度:O(n²),可接受(n ≤ 500);
- ✅ 无递归调用,彻底规避栈溢出风险;
- ✅ 逻辑清晰、可验证:每步扫描对应一个物理方向,易于调试与单元测试。
注意事项:
- mines 必须转为 set(而非 list),否则 in 操作退化为 O(k),整体变慢;
- 四个 DP 数组不可复用同一内存(如误用 [[0]*n]*n),需确保每行独立;
- 边界处理已隐含在扫描逻辑中(cnt=0 重置),无需额外判断;
- 若需空间优化,可只保留两个方向数组并滚动更新,但可读性下降,通常不必要。
掌握这种「将循环依赖递归重构为单向动态规划」的思维,是解决网格类 DFS 陷阱题的关键跃迁。