出界的路径数 图解题解
这道题到底在问什么
- 输入
- m=2,n=2,maxMove=2,start=(0,0)
- 输出
- 6(两步内 6 条出界路径)
- 输入
- m=1,n=1,maxMove=1,start=(0,0)
- 输出
- 4(一步,四个方向都出界)
最优解:为什么这么做
一句话答案: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 暂存,漏取模一溢出就废。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记住「每步把 dp 往四周扩散:界内进 ndp、越界进 ans」,下面逐步套它。
- 4初始 dp 网格:只有起点 (0,0) 是 1(高亮),其余为 0。ans 从 0 开始累计出界路径。
- 5开始第 1/2 步。新建空的 ndp,准备接收扩散结果;ans 当前 = 0。
- 6看 dp[0][0]=1(紫):它会把这 1 条路径分别推向上下左右四个邻居。
- 7从 (0,0) 往下到界内的 (1,0)(蓝),把 1 条路径累加进 ndp,ndp[1][0] 现为 1。
- 8从 (0,0) 往上走出网格了,这 1 条路径正好在第 1 步出界,计入 ans,现 ans = 1。
- 9从 (0,0) 往右到界内的 (0,1)(蓝),把 1 条路径累加进 ndp,ndp[0][1] 现为 1。
- 10从 (0,0) 往左走出网格了,这 1 条路径正好在第 1 步出界,计入 ans,现 ans = 2。
- 11第 1 步扩散完毕,用 ndp 替换 dp(现在网格里的数是「走 1 步停在各格的路径数」)。累计出界 ans = 2。
- 12开始第 2/2 步。新建空的 ndp,准备接收扩散结果;ans 当前 = 2。
- 13看 dp[0][1]=1(紫):它会把这 1 条路径分别推向上下左右四个邻居。
- 14从 (0,1) 往下到界内的 (1,1)(蓝),把 1 条路径累加进 ndp,ndp[1][1] 现为 1。
- 15从 (0,1) 往上走出网格了,这 1 条路径正好在第 2 步出界,计入 ans,现 ans = 3。
- 16从 (0,1) 往右走出网格了,这 1 条路径正好在第 2 步出界,计入 ans,现 ans = 4。
- 17从 (0,1) 往左到界内的 (0,0)(蓝),把 1 条路径累加进 ndp,ndp[0][0] 现为 1。
- 18看 dp[1][0]=1(紫):它会把这 1 条路径分别推向上下左右四个邻居。
- 19从 (1,0) 往下走出网格了,这 1 条路径正好在第 2 步出界,计入 ans,现 ans = 5。
- 20从 (1,0) 往上到界内的 (0,0)(蓝),把 1 条路径累加进 ndp,ndp[0][0] 现为 2。
- 21从 (1,0) 往右到界内的 (1,1)(蓝),把 1 条路径累加进 ndp,ndp[1][1] 现为 2。
- 22从 (1,0) 往左走出网格了,这 1 条路径正好在第 2 步出界,计入 ans,现 ans = 6。
- 23第 2 步扩散完毕,用 ndp 替换 dp(现在网格里的数是「走 2 步停在各格的路径数」)。累计出界 ans = 6。
- 24走满 2 步,累计出界路径 ans = 6。注意 dp 里还剩下的非零格,是「停在界内还没出去」的路径,不计入答案;只有真正跨出边界的才算。
⚠️ 容易写错的地方
✗ 错:原地在 dp 上扩散
✓ 对:用独立的 ndp 接收本步结果
同一步内若直接改 dp,会让刚更新的格子在本步内又被当成源重复扩散;必须用 ndp 隔离「这一步前」和「这一步后」
✗ 错:出界路径漏计或重复计
✓ 对:越界时把源格路径数一次性加进 ans
每条到达 (r,c) 的路径在这一步向某越界方向走,就是一条独立的出界路径,数量正是 dp[r][c],按方向逐次累加
✗ 错:相加不取模导致溢出
✓ 对:每次累加都对 1e9+7 取模,ans 用更宽整型暂存
路径数可指数级增长,C++/Java 的 int 会溢出;ans 与 ndp 都要边加边取模,C++ 用 long long、Java 用 long 暂存
完整代码(Python / C++ / Java)
Python
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 ansC++
#include <vector>
using namespace std;
class Solution {
public:
int findPaths(int m, int n, int maxMove, int startRow, int startColumn) {
const int MOD = 1000000007;
vector<vector<int>> dp(m, vector<int>(n));
dp[startRow][startColumn] = 1;
long long ans = 0;
int dirs[5] = {1,0,-1,0,1};
for (int step = 0; step < maxMove; ++step) {
vector<vector<int>> ndp(m, vector<int>(n));
for (int r = 0; r < m; ++r) for (int c = 0; c < n; ++c) if (dp[r][c]) {
for (int d = 0; d < 4; ++d) {
int nr = r + dirs[d], nc = c + dirs[d+1];
if (nr < 0 || nr >= m || nc < 0 || nc >= n) ans = (ans + dp[r][c]) % MOD;
else ndp[nr][nc] = (ndp[nr][nc] + dp[r][c]) % MOD;
}
}
dp.swap(ndp);
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int findPaths(int m, int n, int maxMove, int startRow, int startColumn) {
int MOD = 1_000_000_007;
int[][] dp = new int[m][n];
dp[startRow][startColumn] = 1;
long ans = 0;
int[] dirs = {1,0,-1,0,1};
for (int step = 0; step < maxMove; step++) {
int[][] ndp = new int[m][n];
for (int r = 0; r < m; r++) for (int c = 0; c < n; c++) if (dp[r][c] != 0) {
for (int d = 0; d < 4; d++) {
int nr = r + dirs[d], nc = c + dirs[d+1];
if (nr < 0 || nr >= m || nc < 0 || nc >= n) ans = (ans + dp[r][c]) % MOD;
else ndp[nr][nc] = (ndp[nr][nc] + dp[r][c]) % MOD;
}
}
dp = ndp;
}
return (int) ans;
}
}复杂度
时间
O(maxMove·m·n)
外层走 maxMove 步,每步遍历整张 m×n 网格、每格看 4 个方向(常数);整体 O(maxMove·m·n)
空间
O(m·n)
只需 dp 和 ndp 两张网格(滚动),O(m·n);不必存每一步的网格
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 出界的路径数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
能不能用记忆化 DFS 从「位置加剩余步数」出发做?+
可以,而且很自然。定义 dfs(r,c,move):从 (r,c) 出发还剩 move 步,能产生多少条出界路径。若 (r,c) 已经在界外返回 1(这条路径出界了);若 move 为 0 返回 0;否则对四个方向求 dfs(nr,nc,move−1) 之和,用 memo[(r,c,move)] 缓存。它和正推分层 dp 完全等价,状态都是「位置加剩余步数」,复杂度同为 O(maxMove·m·n)。
为什么不能直接在 dp 上原地扩散,非得开一张 ndp?+
同一步里若直接改 dp,某个格子刚被这一步的扩散累加过,接着遍历到它时又会被当成这一步的源头再扩一次,等于让一条路径在一步内走了两格,步数就乱了。用独立的 ndp 接收本步结果,dp 始终是「这一步之前」的快照,四个方向都扩完再整体替换,才能保证每步只走一格。
为什么走满步数后 dp 里剩下的非零格不计入答案?+
走满 maxMove 步后,dp[r][c] 表示「恰好停在界内 (r,c)、还没出界」的路径数。题目只数走出边界的路径,这些还停在网格里的路径没出界,自然不算。只有在某一步真正跨出网格的那一刻,才把对应的 dp[r][c] 条路径累加进 ans。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 出界的路径数 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。