网格中的最小路径代价 图解题解
这道题到底在问什么
- 输入
- grid=[[5,3],[4,0],[2,1]], moveCost=[[9,8],[1,5],[10,12],[18,6],[2,4],[14,3]]
- 输出
- 17(路径 5 到 0 到 1)
- 输入
- grid=[[5,1,2],[4,0,3]], moveCost 见题面
- 输出
- 6(路径 2 到 3)
最优解:为什么这么做
一句话答案:LeetCode 2304 网格中的最小路径代价:从第一行走到最后一行,靠逐行滚动的动态规划,每格取上一行所有来路里最省的一条、加本格值与移动代价,末行取最小,时间 O(m·n²)。
从第一行走到最后一行,最小总代价怎么算
给一张 m 行 n 列的网格 grid,格子是互不相同的整数。从第一行出发,每步落到下一行任意一列,直到最后一行。总代价 = 沿途格值之和加每次跨行的移动代价;moveCost[v][j] 是从值 v 的格子落到下一行第 j 列的代价。题面例子 grid=[[5,3],[4,0],[2,1]],最优路径 5 到 0 到 1,答案 17。
把每条路都枚举一遍,为什么行不通
直觉是枚举每条从头到尾的路比总代价。可每一行都能落到 n 列任意一个,m 行连乘是 n 的 (m−1) 次方条路,n 和 m 到 50 就跑不完。但走到某一格后,往下怎么走只跟站在这格、已花多少有关,所以不必记整条来路,只需记住走到每一格的最小代价。
逐行滚动,f[j] 记走到本行第 j 列的最省花费
设 f[j] 为从第一行走到本行第 j 列的最小总代价。第 0 行是起点、出发不付移动代价,f 就是格值。往下逐行推:算本行第 j 列的 f,枚举上一行每列 k 当来路,候选 = f[k] 加移到本列 j 的代价加本格值 grid[i][j],取最小。
移动代价用出发格的数值查 moveCost 的行、不是坐标:上一行第 k 列的值是 grid[i−1][k],就查 moveCost[grid[i−1][k]][j]。推下一行只用得到紧挨的上一行,只留它逐行推进,这就是滚动数组省空间的办法。
一行填满再填下一行,末行取最小收口
逐行往下填,第 0 行是格值,本行 f 算好后成为下一行的地基,一路压到最后一行。终点没指定落在哪列,末行每列的 f 是走到那列的最小代价,扫一遍取最小即答案。
拿 [[5,3],[4,0],[2,1]] 把每格逐步填一遍
moveCost=[[9,8],[1,5],[10,12],[18,6],[2,4],[14,3]]。第 0 行抄格值 f=[5, 3]。
第 1 行取两来路较小者。(1,0):5 加 moveCost[5][0]=14 加 4 得 23,或 3 加 18 加 4 得 25,取 23。(1,1):5 加 moveCost[5][1]=3 加 0 得 8,或 3 加 6 加 0 得 9,取 8。f=[23, 8]。
第 2 行(上一行 f=[23, 8]、格值 4 和 0)。(2,0):23 加 moveCost[4][0]=2 加 2 得 27,或 8 加 moveCost[0][0]=9 加 2 得 19,取 19。(2,1):23 加 4 加 1 得 28,或 8 加 moveCost[0][1]=8 加 1 得 17,取 17。末行 f=[19, 17],最小 17 即答案。
复杂度,再逐个点名填表最常写反的四处
时间 O(m·n²):m 行、每行 n 列、每列再枚举 n 条来路,三层嵌套。空间 O(n),滚动数组只留上一行和本行两份,不存整张 m 乘 n 的表。
写反的四处:moveCost 按数值查行,当坐标查会查错行;本格值 grid[i][j] 要计入总代价,漏了偏小;来路是上一行任意一列而非正上方,少枚举会错过更省解;答案取末行 f 最小值,取第 0 列会漏掉更优解。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记牢这套转移:每一格的最小代价,等于自己的格值,加上「从上一行所有来路里挑一条最省的」。下面就照着这句,从第 0 行开始一格一格填。
- 4这是舞台上的动态规划表,一开始每个格子先写上它在 grid 里的原始值:第 0 行是 5 和 3,第 1 行是 4 和 0,第 2 行是 2 和 1。接下来从第 1 行起,我们会把每格改写成「到达这格的最小总代价」,格值本身则并进代价里。第 0 行没有移动,它的代价就等于格值,可以直接定下来。
- 5先填第 0 行,它是所有路径的起点。因为从第一行出发本身不产生移动代价,所以到第 0 行每一格的最小代价,就等于它自己的格值。列 0 这格值是 5,f 就记 5,标成绿色确定下来。
- 6同理,第 0 行列 1 这格值是 3,到它的最小代价就是 3。第 0 行两格都定了,f 这一行是 [5, 3]。这一行是后面所有计算的地基,往下每一格都要回头看它。
- 7往下走之前,先把 moveCost 怎么查说清楚。它有 m 乘 n 行,行号从 0 到 m 乘 n 减 1,查表用的是「出发那格里的数值 v」当行号,不是坐标。比如上一行列 0 的值是 5,要落到本行列 0,查的就是 moveCost 第 5 行第 0 列。这个「按值查行」是本题最容易记错的地方,盯住它。
- 8现在算格子 (1,0),它的值是 4。先试第一条来路:从上一行 列 0 过来。那格的最小代价 f 是 5,它的格值是 5,要落到本行列 0,查 moveCost 第 5 行第 0 列得移动代价 14。三样加起来:5 加 14 加 4 等于 23。
- 9再试另一条来路:从上一行 列 1 过来。那格 f 是 3,格值 3,移到本列的代价查表得 18。合计 3 加 18 加 4 等于 25。和上一条 23 摆一起比,谁小取谁。
- 10两条来路的代价是 23 和 25,取更小的 23。所以到 (1,0) 的最小总代价就是 23,它是从上一行 列 0 接过来的。把这格填成 23,标绿确定。
- 11现在算格子 (1,1),它的值是 0。先试第一条来路:从上一行 列 0 过来。那格的最小代价 f 是 5,它的格值是 5,要落到本行列 1,查 moveCost 第 5 行第 1 列得移动代价 3。三样加起来:5 加 3 加 0 等于 8。
- 12再试另一条来路:从上一行 列 1 过来。那格 f 是 3,格值 3,移到本列的代价查表得 6。合计 3 加 6 加 0 等于 9。和上一条 8 摆一起比,谁小取谁。
- 13两条来路的代价是 8 和 9,取更小的 8。所以到 (1,1) 的最小总代价就是 8,它是从上一行 列 0 接过来的。把这格填成 8,标绿确定。
- 14第 1 行两格都算完了,f 变成 [23, 8]。注意此刻格子里显示的已经不是原始格值,而是「到这格的最小总代价」。第 2 行往上看的就是这两个数;原始格值 4 和 0 只在查移动代价时还用得上。
- 15现在算格子 (2,0),它的值是 2。先试第一条来路:从上一行 列 0 过来。那格的最小代价 f 是 23,它的格值是 4,要落到本行列 0,查 moveCost 第 4 行第 0 列得移动代价 2。三样加起来:23 加 2 加 2 等于 27。
- 16再试另一条来路:从上一行 列 1 过来。那格 f 是 8,格值 0,移到本列的代价查表得 9。合计 8 加 9 加 2 等于 19。和上一条 27 摆一起比,谁小取谁。
- 17两条来路的代价是 27 和 19,取更小的 19。所以到 (2,0) 的最小总代价就是 19,它是从上一行 列 1 接过来的。把这格填成 19,标绿确定。
- 18现在算格子 (2,1),它的值是 1。先试第一条来路:从上一行 列 0 过来。那格的最小代价 f 是 23,它的格值是 4,要落到本行列 1,查 moveCost 第 4 行第 1 列得移动代价 4。三样加起来:23 加 4 加 1 等于 28。
- 19再试另一条来路:从上一行 列 1 过来。那格 f 是 8,格值 0,移到本列的代价查表得 8。合计 8 加 8 加 1 等于 17。和上一条 28 摆一起比,谁小取谁。
- 20两条来路的代价是 28 和 17,取更小的 17。所以到 (2,1) 的最小总代价就是 17,它是从上一行 列 1 接过来的。把这格填成 17,标绿确定。
- 21最后一行也填完了,f 是 [19, 17]。这两个数分别是「走到最后一行列 0」和「走到最后一行列 1」的最小总代价。因为终点可以是最后一行任意一列,答案就在这两个里挑小的,下一帧收束。
- 22整张表填满了。最后一行的 f 是 [19, 17],因为终点可以是最后一行的任意一列,取里面最小的那个就是答案。17 比 19 小,所以最小总代价是 17,落在列 1。
- 23答案定在最后一行列 1 那个 17。顺着填表时记下的「从哪来」往回倒:这格是从第 1 行列 1 接来的;第 1 行列 1 这格现在显示的是 DP 代价 8,它的原始格值是 0,这个 8 又是从第 0 行列 0 的 5 接来的。这样一路往上,就把最省的那条路摸出来了。
- 24整条最优路径亮出来:第 0 行列 0 的 5,到第 1 行列 1 的 0,再到第 2 行列 1 的 1。三个格值 5 加 0 加 1 等于 6;第一步从值 5 移到列 1 代价 3,第二步从值 0 移到列 1 代价 8,两步合 11。6 加 11,正好是 17。和开头说的路径对上了。
⚠️ 容易写错的地方
✗ 错:用「出发格的坐标」去 moveCost 查行
✓ 对:用「出发格里的数值」查行
moveCost 的行号是格子的值 v,不是行列坐标;查错行会算出完全错误的移动代价
✗ 错:忘了把本格值 grid[i][j] 计入
✓ 对:每格代价必须加上落格自身的值
总代价 = 沿途格值之和 加 移动代价之和,落格的值也是路径的一部分,漏加会偏小
✗ 错:只从上一行同列或相邻列转移
✓ 对:上一行任意一列都能作为来路
题目允许移动到下一行的任意列,所以要枚举上一行全部 n 个列取最小,不能只看正上方
✗ 错:答案取最后一行第 0 列
✓ 对:答案取最后一行 f 的最小值
终点可以是最后一行任意一列,必须在整行里取最小,不是固定某一列
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def minPathCost(self, grid: List[List[int]], moveCost: List[List[int]]) -> int:
m, n = len(grid), len(grid[0])
f = grid[0]
for i in range(1, m):
g = [inf] * n
for j in range(n):
for k in range(n):
g[j] = min(g[j], f[k] + moveCost[grid[i - 1][k]][j] + grid[i][j])
f = g
return min(f)C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int minPathCost(vector<vector<int>>& grid, vector<vector<int>>& moveCost) {
int m = grid.size(), n = grid[0].size();
const int inf = 1 << 30;
vector<int> f = grid[0];
for (int i = 1; i < m; ++i) {
vector<int> g(n, inf);
for (int j = 0; j < n; ++j) {
for (int k = 0; k < n; ++k) {
g[j] = min(g[j], f[k] + moveCost[grid[i - 1][k]][j] + grid[i][j]);
}
}
f = move(g);
}
return *min_element(f.begin(), f.end());
}
};Java
import java.util.*;
class Solution {
public int minPathCost(int[][] grid, int[][] moveCost) {
int m = grid.length, n = grid[0].length;
int[] f = grid[0];
final int inf = 1 << 30;
for (int i = 1; i < m; ++i) {
int[] g = new int[n];
Arrays.fill(g, inf);
for (int j = 0; j < n; ++j) {
for (int k = 0; k < n; ++k) {
g[j] = Math.min(g[j], f[k] + moveCost[grid[i - 1][k]][j] + grid[i][j]);
}
}
f = g;
}
// return Arrays.stream(f).min().getAsInt();
int ans = inf;
for (int v : f) {
ans = Math.min(ans, v);
}
return ans;
}
}复杂度
时间
O(m·n²)
m 行,每行 n 个目标列,每个目标列又要枚举上一行的 n 个来路列,三层嵌套。行数 m 乘上每行 n 乘 n 的转移,总量随 m 乘 n 的平方增长
空间
O(n)
按峰值算。滚动数组只保留上一行的 f 和当前行的 g,各长 n;不需要存整张 m 乘 n 的表。若为回溯路径而保留全表则是 O(m·n),本解只求最小值故 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 网格中的最小路径代价 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
状态和转移到底该怎么设?+
状态设 f[i][j] 为从第一行走到第 i 行第 j 列的最小总代价。第 0 行 f 等于格值本身,因为出发不付移动代价。转移是 f[i][j] = grid[i][j] 加上「上一行每一列 k 的 f[i−1][k] 加 moveCost[grid[i−1][k]][j]」里的最小值——上一行任意一列都能作来路,要全枚举取小。答案是最后一行 f 的最小值。最关键的一点是移动代价按出发格的值查行,别拿坐标去查。
为什么只保留上一行就够,不用存整张表?+
因为算第 i 行任何一格,只回头看第 i−1 行的 f 和格值,第 i−2 行往上的信息都用不到了。所以维护一份长 n 的数组存上一行、再开一份存正在填的本行,填完把本行赋给「上一行」继续下压即可,空间从 O(m·n) 压到 O(n)。只有当你还想回溯出具体走了哪条路时,才需要留整张表记每格从哪一列接来。
复杂度 O(m·n²) 还能再降吗?+
一般降不了。三层循环是 m 行、每行 n 个目标列、每列枚举 n 条来路,转移本身是稠密的全枚举,没有可跳过的来路。只有当 moveCost 具备某种单调性时,才可能用决策单调或数据结构把内层那一维优化掉;本题 n 通常到 50,直接三层循环就足够快,没必要上花活。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 网格中的最小路径代价 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。