题目描述
思路解析
一句话答案:LeetCode 576 出界的路径数:球在网格最多走 maxMove 步、问几条路径出界。按步数分层递推,每步把 dp 往四邻扩散、越界的计入答案取模 1e9+7,时间 O(maxMove·m·n)、空间 O(m·n)。
球在网格里怎么走才算一条出界路径
给一个 m 行 n 列网格,球在起点 (startRow, startColumn),每步往上下左右挪一格。问最多走 maxMove 步内有多少条路径把球带出边界。路径数涨得极快、会大到变量存不下,所以答案对 1000000007 取模(除以它取余数)。题面例子 m=2、n=2、maxMove=2、起点 (0,0),答案 6。数的是出界路径不是格子,同一位置走法不同算不同路径。
为什么把每条走法都试一遍会爆炸
最直接是从起点递归(函数自己调用自己去处理下一步)枚举每条走法,往四个方向试一遍,步数用完或出界就停。可每步分四个岔,走 maxMove 步就是 4 的 maxMove 次方条路径,maxMove 到几十就数不完。而且绕到同一格子、剩步数一样时往后能出界多少条是相同子问题,却被反复重算。记下「某格子加剩余步数」复用就能压平。
dp[r][c] 存什么,为什么按步数分层
记每个格子上攒了多少条到达它的路径。定义 dp[r][c] 为走若干步后恰好停在 (r,c) 的路径数。一开始球在起点没走,dp[startRow][startColumn]=1,其余为 0。
关键是按步数分层推进(一步一层、算完这步整张网格再算下步)。走满 k 步那层 dp 只由走满 k−1 步那层推出,与更早层无关,路径长度被步数锁死,不会串层。
每步怎么扩散,出界那一下如何计入答案
每走一步,把这层 dp 每个格子往四个方向摊开:走到界内邻居 (nr,nc) 就把 dp[r][c] 累加进新一层 ndp[nr][nc];走到界外正好出界,把 dp[r][c] 条路径加进 ans。四方向摊完,用 ndp 覆盖 dp 进下一步。
必须用独立的 ndp 接结果,不能在 dp 上原地改——否则刚更新的格子会在同一步里又被当成源头二次扩散,等于一步走了两格。累加还要随手取模,哪几步见末节。
拿 m=2、n=2、maxMove=2 亲手扩两步
初始 dp 只有 dp[0][0]=1,ans=0。第 1 步 dp[0][0] 往下界内 (1,0)、往右界内 (0,1) 各让 ndp 记 1,往上往左两次出界 ans=2。这步完 dp 只剩 (0,1)、(1,0) 为 1。
第 2 步:dp[0][1] 往下(1,1)、往左(0,0)各进 ndp,往上往右各出界 ans 到 4;dp[1][0] 往上(0,0)、往右(1,1)各进 ndp,往下往左各出界 ans 到 6。两步走满 ans=6,和题面对上;dp 剩的非零格是停界内没出去的路径,不算数。
1×1 一步为什么是 4,取模在哪几步做
每步遍历整张 m×n 网格、每格看 4 个方向,共 maxMove 步,时间 O(maxMove·m·n)。空间只需 dp、ndp 两张网格轮换,O(m·n),不必存每步网格。
两个边界要盯住。一是极小网格 m=1、n=1、maxMove=1:起点四个方向全在界外,一步贡献 4 条路径,答案 4。二是防溢出(数大到超出变量能存范围):ans 和 ndp 累加都对 1000000007 取模、用 long 暂存,漏取模一溢出就废。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住「每步把 dp 往四周扩散:界内进 ndp、越界进 ans」,下面逐步套它。
初始 dp 网格:只有起点 (0,0) 是 1(高亮),其余为 0。ans 从 0 开始累计出界路径。
开始第 1/2 步。新建空的 ndp,准备接收扩散结果;ans 当前 = 0。
看 dp[0][0]=1(紫):它会把这 1 条路径分别推向上下左右四个邻居。
从 (0,0) 往下到界内的 (1,0)(蓝),把 1 条路径累加进 ndp,ndp[1][0] 现为 1。
从 (0,0) 往上走出网格了,这 1 条路径正好在第 1 步出界,计入 ans,现 ans = 1。
从 (0,0) 往右到界内的 (0,1)(蓝),把 1 条路径累加进 ndp,ndp[0][1] 现为 1。
从 (0,0) 往左走出网格了,这 1 条路径正好在第 1 步出界,计入 ans,现 ans = 2。
第 1 步扩散完毕,用 ndp 替换 dp(现在网格里的数是「走 1 步停在各格的路径数」)。累计出界 ans = 2。
开始第 2/2 步。新建空的 ndp,准备接收扩散结果;ans 当前 = 2。
看 dp[0][1]=1(紫):它会把这 1 条路径分别推向上下左右四个邻居。
从 (0,1) 往下到界内的 (1,1)(蓝),把 1 条路径累加进 ndp,ndp[1][1] 现为 1。
从 (0,1) 往上走出网格了,这 1 条路径正好在第 2 步出界,计入 ans,现 ans = 3。
从 (0,1) 往右走出网格了,这 1 条路径正好在第 2 步出界,计入 ans,现 ans = 4。
从 (0,1) 往左到界内的 (0,0)(蓝),把 1 条路径累加进 ndp,ndp[0][0] 现为 1。
看 dp[1][0]=1(紫):它会把这 1 条路径分别推向上下左右四个邻居。
从 (1,0) 往下走出网格了,这 1 条路径正好在第 2 步出界,计入 ans,现 ans = 5。
从 (1,0) 往上到界内的 (0,0)(蓝),把 1 条路径累加进 ndp,ndp[0][0] 现为 2。
从 (1,0) 往右到界内的 (1,1)(蓝),把 1 条路径累加进 ndp,ndp[1][1] 现为 2。
从 (1,0) 往左走出网格了,这 1 条路径正好在第 2 步出界,计入 ans,现 ans = 6。
第 2 步扩散完毕,用 ndp 替换 dp(现在网格里的数是「走 2 步停在各格的路径数」)。累计出界 ans = 6。
走满 2 步,累计出界路径 ans = 6。注意 dp 里还剩下的非零格,是「停在界内还没出去」的路径,不计入答案;只有真正跨出边界的才算。
边界:0 步为 0;1×1 一步为 4;大步数靠取模。
两个延伸:可记忆化 DFS(位置,剩余步数)等价;停界内的路径不算出界。
参考代码
class Solution: def findPaths(self, m: int, n: int, maxMove: int, startRow: int, startColumn: int) -> int: MOD = 10**9 + 7 dp = [[0] * n for _ in range(m)] dp[startRow][startColumn] = 1 ans = 0 for _ in range(maxMove): ndp = [[0] * n for _ in range(m)] for r in range(m): for c in range(n): if dp[r][c]: for dr, dc in ((1,0),(-1,0),(0,1),(0,-1)): nr, nc = r + dr, c + dc if nr < 0 or nr >= m or nc < 0 or nc >= n: ans = (ans + dp[r][c]) % MOD else: ndp[nr][nc] = (ndp[nr][nc] + dp[r][c]) % MOD dp = ndp return ans复杂度
- 时间:O(maxMove·m·n),外层走 maxMove 步,每步遍历整张 m×n 网格、每格看 4 个方向(常数);整体 O(maxMove·m·n)
- 空间:O(m·n),只需 dp 和 ndp 两张网格(滚动),O(m·n);不必存每一步的网格
易错点
面试追问把动画讲成自己的话
追问能不能用记忆化 DFS(从 (r,c,剩余步数) 出发)来做?
追问为什么 dp 里剩下的非零格不计入答案?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
两个字符串的删除操作
LeetCode 583 · 中等 · 沿着 动态规划套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题