From 4ee48ecc8246e12e7e35cff690d8008939f93c85 Mon Sep 17 00:00:00 2001 From: AC_Oier Date: Wed, 22 Nov 2023 10:19:47 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8feat:=20add=202304?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...2 \346\234\200\347\237\255\350\267\257.md" | 1 + "Index/\345\272\217\345\210\227 DP.md" | 1 + "Index/\346\250\241\346\213\237.md" | 1 + ...10\344\270\255\347\255\211\357\274\211.md" | 2 +- ...10\344\270\255\347\255\211\357\274\211.md" | 469 ++++++++++++++++++ 5 files changed, 473 insertions(+), 1 deletion(-) create mode 100644 "LeetCode/2301-2310/2304. \347\275\221\346\240\274\344\270\255\347\232\204\346\234\200\345\260\217\350\267\257\345\276\204\344\273\243\344\273\267\357\274\210\344\270\255\347\255\211\357\274\211.md" diff --git "a/Index/\345\233\276\350\256\272 \346\234\200\347\237\255\350\267\257.md" "b/Index/\345\233\276\350\256\272 \346\234\200\347\237\255\350\267\257.md" index 631618f8..dead7ce1 100644 --- "a/Index/\345\233\276\350\256\272 \346\234\200\347\237\255\350\267\257.md" +++ "b/Index/\345\233\276\350\256\272 \346\234\200\347\237\255\350\267\257.md" @@ -9,4 +9,5 @@ | [1786. 从第一个节点出发到最后一个节点的受限路径数](https://leetcode-cn.com/problems/number-of-restricted-paths-from-first-to-last-node/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/number-of-restricted-paths-from-first-to-last-node/solution/xiang-jie-dui-you-hua-dijkstra-dong-tai-i6j0d/) | 中等 | 🤩🤩🤩 | | [1976. 到达目的地的方案数](https://leetcode.cn/problems/number-of-ways-to-arrive-at-destination/) | [LeetCode 题解链接](https://leetcode.cn/problems/number-of-ways-to-arrive-at-destination/solution/by-ac_oier-4ule/) | 中等 | 🤩🤩🤩🤩 | | [2045. 到达目的地的第二短时间](https://leetcode-cn.com/problems/second-minimum-time-to-reach-destination/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/second-minimum-time-to-reach-destination/solution/gong-shui-san-xie-yi-ti-shuang-jie-dui-y-88np/) | 困难 | 🤩🤩🤩🤩 | +| [2304. 网格中的最小路径代价](https://leetcode.cn/problems/minimum-path-cost-in-a-grid/) | [LeetCode 题解链接](https://leetcode.cn/problems/minimum-path-cost-in-a-grid/solutions/2536854/gong-shui-san-xie-cong-tu-lun-zui-duan-l-emxv/) | 中等 | 🤩🤩🤩🤩🤩 | diff --git "a/Index/\345\272\217\345\210\227 DP.md" "b/Index/\345\272\217\345\210\227 DP.md" index 2266add3..b1e9f4a6 100644 --- "a/Index/\345\272\217\345\210\227 DP.md" +++ "b/Index/\345\272\217\345\210\227 DP.md" @@ -29,4 +29,5 @@ | [1691. 堆叠长方体的最大高度](https://leetcode.cn/problems/maximum-height-by-stacking-cuboids/) | [LeetCode 题解链接](https://acoier.com/2022/12/10/1691.%20%E5%A0%86%E5%8F%A0%E9%95%BF%E6%96%B9%E4%BD%93%E7%9A%84%E6%9C%80%E5%A4%A7%E9%AB%98%E5%BA%A6%EF%BC%88%E5%9B%B0%E9%9A%BE%EF%BC%89/) | 困难 | 🤩🤩🤩🤩 | | [1713. 得到子序列的最少操作次数](https://leetcode-cn.com/problems/minimum-operations-to-make-a-subsequence/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/minimum-operations-to-make-a-subsequence/solution/gong-shui-san-xie-noxiang-xin-ke-xue-xi-oj7yu/) | 困难 | 🤩🤩🤩🤩🤩 | | [1751. 最多可以参加的会议数目 II](https://leetcode-cn.com/problems/maximum-number-of-events-that-can-be-attended-ii/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/maximum-number-of-events-that-can-be-attended-ii/solution/po-su-dp-er-fen-dp-jie-fa-by-ac_oier-88du/) | 困难 | 🤩🤩🤩🤩 | +| [2304. 网格中的最小路径代价](https://leetcode.cn/problems/minimum-path-cost-in-a-grid/) | [LeetCode 题解链接](https://leetcode.cn/problems/minimum-path-cost-in-a-grid/solutions/2536854/gong-shui-san-xie-cong-tu-lun-zui-duan-l-emxv/) | 中等 | 🤩🤩🤩🤩 | diff --git "a/Index/\346\250\241\346\213\237.md" "b/Index/\346\250\241\346\213\237.md" index eb024355..99472079 100644 --- "a/Index/\346\250\241\346\213\237.md" +++ "b/Index/\346\250\241\346\213\237.md" @@ -246,6 +246,7 @@ | [2069. 模拟行走机器人 II](https://leetcode-cn.com/problems/walking-robot-simulation-ii/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/walking-robot-simulation-ii/solution/by-ac_oier-6zib/) | 中等 | 🤩🤩🤩🤩 | | [2103. 环和杆](https://leetcode.cn/problems/rings-and-rods/) | [LeetCode 题解链接](https://leetcode.cn/problems/rings-and-rods/solutions/2509056/gong-shui-san-xie-liang-ge-jiao-du-jin-x-r1v1/) | 简单 | 🤩🤩🤩🤩 | | [2216. 美化数组的最少删除数](https://leetcode.cn/problems/minimum-deletions-to-make-array-beautiful/) | [LeetCode 题解链接](https://leetcode.cn/problems/minimum-deletions-to-make-array-beautiful/solutions/2535327/gong-shui-san-xie-zhi-ji-ben-zhi-de-ji-j-dk05/) | 中等 | 🤩🤩🤩🤩 | +| [2304. 网格中的最小路径代价](https://leetcode.cn/problems/minimum-path-cost-in-a-grid/) | [LeetCode 题解链接](https://leetcode.cn/problems/minimum-path-cost-in-a-grid/solutions/2536854/gong-shui-san-xie-cong-tu-lun-zui-duan-l-emxv/) | 中等 | 🤩🤩🤩🤩 | | [2335. 装满杯子需要的最短总时长](https://leetcode.cn/problems/minimum-amount-of-time-to-fill-cups/) | [LeetCode 题解链接](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247495870&idx=1&sn=a15b87852faaa33fc9d976b575ef1099) | 简单 | 🤩🤩🤩🤩🤩 | | [2342. 数位和相等数对的最大和](https://leetcode.cn/problems/max-sum-of-a-pair-with-equal-sum-of-digits/) | [LeetCode 题解链接](https://leetcode.cn/problems/max-sum-of-a-pair-with-equal-sum-of-digits/solutions/2531511/gong-shui-san-xie-yong-bian-li-guo-cheng-kt3f/) | 中等 | 🤩🤩🤩🤩 | | [2520. 统计能整除数字的位数](https://leetcode.cn/problems/count-the-digits-that-divide-a-number/) | [LeetCode 题解链接](https://leetcode.cn/problems/count-the-digits-that-divide-a-number/solutions/2498966/gong-shui-san-xie-jian-dan-mo-ni-ti-shi-0ad2c/) | 简单 | 🤩🤩🤩🤩🤩 | diff --git "a/LeetCode/1331-1340/1334. \351\230\210\345\200\274\350\267\235\347\246\273\345\206\205\351\202\273\345\261\205\346\234\200\345\260\221\347\232\204\345\237\216\345\270\202\357\274\210\344\270\255\347\255\211\357\274\211.md" "b/LeetCode/1331-1340/1334. \351\230\210\345\200\274\350\267\235\347\246\273\345\206\205\351\202\273\345\261\205\346\234\200\345\260\221\347\232\204\345\237\216\345\270\202\357\274\210\344\270\255\347\255\211\357\274\211.md" index 8910cb01..e1783f78 100644 --- "a/LeetCode/1331-1340/1334. \351\230\210\345\200\274\350\267\235\347\246\273\345\206\205\351\202\273\345\261\205\346\234\200\345\260\221\347\232\204\345\237\216\345\270\202\357\274\210\344\270\255\347\255\211\357\274\211.md" +++ "b/LeetCode/1331-1340/1334. \351\230\210\345\200\274\350\267\235\347\246\273\345\206\205\351\202\273\345\261\205\346\234\200\345\260\221\347\232\204\345\237\216\345\270\202\357\274\210\344\270\255\347\255\211\357\274\211.md" @@ -639,7 +639,7 @@ public: vector vis(n, false); dist[x] = 0; // 使用「优先队列」存储所有可用于更新的点 - // 以 (点编号, 到起点的距离) 进行存储,优先弹出「最短距离」较小的点 + // 以 (到起点的距离, 点编号) 进行存储,优先弹出「最短距离」较小的点 priority_queue, vector>, greater>> q; q.push({0, x}); while (!q.empty()) { diff --git "a/LeetCode/2301-2310/2304. \347\275\221\346\240\274\344\270\255\347\232\204\346\234\200\345\260\217\350\267\257\345\276\204\344\273\243\344\273\267\357\274\210\344\270\255\347\255\211\357\274\211.md" "b/LeetCode/2301-2310/2304. \347\275\221\346\240\274\344\270\255\347\232\204\346\234\200\345\260\217\350\267\257\345\276\204\344\273\243\344\273\267\357\274\210\344\270\255\347\255\211\357\274\211.md" new file mode 100644 index 00000000..5024696b --- /dev/null +++ "b/LeetCode/2301-2310/2304. \347\275\221\346\240\274\344\270\255\347\232\204\346\234\200\345\260\217\350\267\257\345\276\204\344\273\243\344\273\267\357\274\210\344\270\255\347\255\211\357\274\211.md" @@ -0,0 +1,469 @@ +### 题目描述 + +这是 LeetCode 上的 **[2304. 网格中的最小路径代价](https://leetcode.cn/problems/minimum-path-cost-in-a-grid/solutions/2536854/gong-shui-san-xie-cong-tu-lun-zui-duan-l-emxv/)** ,难度为 **中等**。 + +Tag : 「最短路」、「图」、「模拟」、「序列 DP」、「动态规划」 + + + +给你一个下标从 `0` 开始的整数矩阵 `grid`,矩阵大小为 `m x n`,由从 `0` 到 $m \times n - 1$ 的不同整数组成。 + +你可以在此矩阵中,从一个单元格移动到下一行的任何其他单元格。 + +如果你位于单元格 $(x, y)$ ,且满足 $x < m - 1$,你可以移动到 $(x + 1, 0)$, $(x + 1, 1)$, ..., $(x + 1, n - 1)$ 中的任何一个单元格。注意: 在最后一行中的单元格不能触发移动。 + +每次可能的移动都需要付出对应的代价,代价用一个下标从 `0` 开始的二维数组 `moveCost` 表示,该数组大小为 $(m \times n) \times n$ ,其中 `moveCost[i][j]` 是从值为 `i` 的单元格移动到下一行第 `j` 列单元格的代价。从 `grid` 最后一行的单元格移动的代价可以忽略。 + +`grid` 一条路径的代价是:所有路径经过的单元格的值之和加上所有移动的代价之和 。从第一行任意单元格出发,返回到达最后一行任意单元格的最小路径代价。 + +示例 1: +![](https://assets.leetcode.com/uploads/2022/04/28/griddrawio-2.png) + +``` +输入:grid = [[5,3],[4,0],[2,1]], moveCost = [[9,8],[1,5],[10,12],[18,6],[2,4],[14,3]] + +输出:17 + +解释:最小代价的路径是 5 -> 0 -> 1 。 +- 路径途经单元格值之和 5 + 0 + 1 = 6 。 +- 从 5 移动到 0 的代价为 3 。 +- 从 0 移动到 1 的代价为 8 。 +路径总代价为 6 + 3 + 8 = 17 。 +``` +示例 2: +``` +输入:grid = [[5,1,2],[4,0,3]], moveCost = [[12,10,15],[20,23,8],[21,7,1],[8,1,13],[9,10,25],[5,3,2]] + +输出:6 + +解释: +最小代价的路径是 2 -> 3 。 +- 路径途经单元格值之和 2 + 3 = 5 。 +- 从 2 移动到 3 的代价为 1 。 +路径总代价为 5 + 1 = 6 。 +``` + +提示: +* $m = grid.length$ +* $n = grid[i].length$ +* $2 <= m, n <= 50$ +* `grid` 由从 `0` 到 `m * n - 1` 的不同整数组成 +* $moveCost.length = m \times n$ +* $moveCost[i].length = n$ +* $1 <= moveCost[i][j] <= 100$ + +--- + +### 建新图 + 建虚拟点 + 堆优化 Dijkstra + +> 注意:可以直接使用解法二的方法,但先认真看完本做法,再去看解法二,会有相当丝滑的体验。 + +每次移动,**实际路径权值 = 经过边的权值 + 目的地的权值**。 + +利用原图,构建新图:**每个单元格视为一个点,除最后一行外,每个点对下一行的所有点连一条有向边,边权 = 原图中该边的权值 + 原图中该目的地的权值**。 + +分析新图中的点边数量: + +* 点:共 $m \times n$ 个点,数量为 $2.5 \times 10^3$ +* 边:不算最后一行,共 $(m - 1) \times n$ 个点,这些点与下一行的每个点均有一条有向边,合计 $(m - 1) \times n^2$ 条边,数量为 $1.25 \times 10^5$ + +原问题转换为:求点 $i$ 到 $j$ 的最短路,其中点 $i$ 所在位置为第 $0$ 行,点 $j$ 所在位置为第 $m - 1$ 行。 + +这似乎是一个「多源汇最短路」问题?但求解多源汇最短路的 `Floyd` 算法是 $O(n^3)$ 的,会超时。 + +实际上,我们也并不真的关心图中任意点之间的最短路,仅仅关心第一行到最后一行的最短路。 + +因此,**我们可通过建立“虚拟源点”和“虚拟汇点”的方式,来将“多源汇最短路”问题转换为“单源最短路”问题。** + +具体的,我们创建一个“虚拟源点”,该点向所有第一行的点连权值为 $grid[0][i]$ 的有向边;同时创建一个“虚拟汇点”,最后一行的所有点向该点连权值为 $0$ 的有向边。 + +问题进一步转化为:求“虚拟源点”到“虚拟汇点”的最短路。 + +至此,我们通过 **建新图 -> 创建虚拟源汇点(转换为单源最短路)-> 套用单源最短路算法** 解决本题。 + +将新图中点的数量记为 $n$,边数记为 $m$,朴素 `Dijkstra` 复杂度为 $O(n^2)$,堆优化的 `Dijkstra` 的复杂度为 $O(m\log{n})$,当 $m < n^2$ (相对稀疏)时,优先使用堆优化 `Dijkstra`。 + +Java 代码: +```Java +class Solution { + int N = 50 * 50 + 2, M = 50 * 50 * 50, idx = 0, n; + int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M]; + void add(int a, int b, int c) { + e[idx] = b; + ne[idx] = he[a]; + w[idx] = c; + he[a] = idx++; + } + public int minPathCost(int[][] grid, int[][] moveCost) { + int N = grid.length, M = grid[0].length; + int S = N * M, T = S + 1; + n = N * M + 2; + Arrays.fill(he, -1); + //「虚拟源点」向「第一行」进行连边 + for (int i = 0; i < M; i++) add(S, grid[0][i], grid[0][i]); + // 转换原图 + for (int i = 0; i < N - 1; i++) { + for (int j = 0; j < M; j++) { + int a = grid[i][j]; + for (int k = 0; k < M; k++) { + int b = grid[i + 1][k]; + add(a, b, moveCost[a][k] + b); + } + } + } + //「最后一行」向「虚拟汇点」进行连边 + for (int i = 0; i < M; i++) add(grid[N - 1][i], T, 0); + // 最短路 + int[] dist = dijkstra(S); + return dist[T]; + } + int[] dijkstra(int x) { + // 起始先将所有的点标记为「未更新」和「距离为正无穷」 + int[] dist = new int[n]; + Arrays.fill(dist, 0x3f3f3f3f); + boolean[] vis = new boolean[n]; + dist[x] = 0; + // 使用「优先队列」存储所有可用于更新的点 + // 以 (点编号, 到起点的距离) 进行存储,优先弹出「最短距离」较小的点 + PriorityQueue q = new PriorityQueue<>((a,b)->a[1]-b[1]); + q.add(new int[]{x, 0}); + while (!q.isEmpty()) { + // 每次从「优先队列」中弹出 + int[] poll = q.poll(); + int u = poll[0], step = poll[1]; + // 如果弹出的点被标记「已更新」,则跳过 + if (vis[u]) continue; + // 标记该点「已更新」,并使用该点更新其他点的「最短距离」 + vis[u] = true; + for (int i = he[u]; i != -1; i = ne[i]) { + int j = e[i]; + if (dist[j] <= dist[u] + w[i]) continue; + dist[j] = dist[u] + w[i]; + q.add(new int[]{j, dist[j]}); + } + } + return dist; + } +} +``` +C++ 代码: +```C++ +class Solution { +public: + static const int N = 50 * 50 + 2, M = 50 * 50 * 50; + int he[N], e[M], ne[M], w[M], idx, n, INF = 0x3f3f3f3f; + void add(int a, int b, int c) { + e[idx] = b; + ne[idx] = he[a]; + w[idx] = c; + he[a] = idx++; + } + int minPathCost(vector>& grid, vector>& moveCost) { + int N = grid.size(), M = grid[0].size(); + int S = N * M, T = S + 1; + n = N * M + 2; + fill(he, he + n, -1); + //「虚拟源点」向「第一行」进行连边 + for (int i = 0; i < M; i++) add(S, grid[0][i], grid[0][i]); + // 转换原图 + for (int i = 0; i < N - 1; i++) { + for (int j = 0; j < M; j++) { + int a = grid[i][j]; + for (int k = 0; k < M; k++) { + int b = grid[i + 1][k]; + add(a, b, moveCost[a][k] + b); + } + } + } + //「最后一行」向「虚拟汇点」进行连边 + for (int i = 0; i < M; i++) add(grid[N - 1][i], T, 0); + // 最短路 + vector dist = dijkstra(S); + return dist[T]; + } + vector dijkstra(int x) { + vector dist(n, 0x3f3f3f3f); + vector vis(n, false); + dist[x] = 0; + // 使用「优先队列」存储所有可用于更新的点 + // 以 (到起点的距离, 点编号) 进行存储,优先弹出「最短距离」较小的点 + priority_queue, vector>, greater>> q; + q.push({0, x}); + while (!q.empty()) { + // 每次从「优先队列」中弹出 + auto [step, u] = q.top(); + q.pop(); + // 如果弹出的点被标记「已更新」,则跳过 + if (vis[u]) continue; + // 标记该点「已更新」,并使用该点更新其他点的「最短距离」 + vis[u] = true; + for (int i = he[u]; i != -1; i = ne[i]) { + int j = e[i]; + if (dist[j] <= dist[u] + w[i]) continue; + dist[j] = dist[u] + w[i]; + q.push({dist[j], j}); + } + } + return dist; + } +}; +``` +Python 代码: +```Python +import heapq + +class Solution: + def minPathCost(self, grid, moveCost): + N, M = len(grid), len(grid[0]) + S, T = N * M, N * M + 1 + n = N * M + 2 + he = [-1] * n + e, ne, w = [-1] * (50 * 50 * 50), [-1] * (50 * 50 * 50), [-1] * (50 * 50 * 50) + idx = 0 + + def add(a, b, c): + nonlocal idx + e[idx] = b + ne[idx] = he[a] + w[idx] = c + he[a] = idx + idx += 1 + + def dijkstra(x): + dist = [float('inf')] * n + vis = [False] * n + dist[x] = 0 + # 使用「优先队列」存储所有可用于更新的点 + # 以 (到起点的距离, 点编号) 进行存储,优先弹出「最短距离」较小的点 + q = [(0, x)] + heapq.heapify(q) + while q: + # 每次从「优先队列」中弹出 + step, u = heapq.heappop(q) + # 如果弹出的点被标记「已更新」,则跳过 + if vis[u]: continue + # 标记该点「已更新」,并使用该点更新其他点的「最短距离」 + vis[u] = True + i = he[u] + while i != -1: + j, c = e[i], w[i] + i = ne[i] + if dist[j] <= dist[u] + c: continue + dist[j] = dist[u] + c + heapq.heappush(q, (dist[j], j)) + return dist + + #「虚拟源点」向「第一行」进行连边 + for i in range(M): + add(S, grid[0][i], grid[0][i]) + # 转换原图 + for i in range(N - 1): + for j in range(M): + a = grid[i][j] + for k in range(M): + b = grid[i + 1][k] + add(a, b, moveCost[a][k] + b) + #「最后一行」向「虚拟汇点」进行连边 + for i in range(M): + add(grid[N - 1][i], T, 0) + # 最短路 + dist = dijkstra(S) + return dist[T] +``` +* 时间复杂度:$O(m\log{n})$,其中 $n$ 为新图中的点数 $N \times M = 2.5 \times 10^3$,$m$ 为新图中的边数 $(M - 1) \times N^2 = 1.25 \times 10^5$ +* 空间复杂度:$O(n + m)$ + +--- + +### 堆优化 Dijkstra + +什么?你说你实在不想建新图,也不想搞什么虚拟点,就想用你心爱的 `BFS` 来做?! + +我懂你意思,但那不叫 `BFS`。 + +只是将「建新图」和「建虚拟点」的过程省掉,仍需要使用优先队列(堆)来每次取出当前“路径代价最小”的点来进行扩充,执行过程仍为堆优化 `Dijkstra` 的核心操作。 + +尤其所谓“省掉” 建新图 和 建虚拟点,真就字面上的“省掉”,并非不存在,因为两种做法思路是完全一致的。可简单列举「本解法」与「解法一」的对应关系: + +* 起始往队列放入首行元素,对应了解法一的“建立虚拟源点”过程; +* 从队列中取元素出来扩充时,若当前元素所在行是最后一行时,用当前路径代价来更新答案,对应了解法一的“建立虚拟汇点”过程; +* 扩充时直接遍历列(即下一行的所有点),对应的解法一的“用原图边建新图”的过程。 + +Java 代码: + +```Java +class Solution { + public int minPathCost(int[][] grid, int[][] moveCost) { + int m = grid.length, n = grid[0].length, INF = 0x3f3f3f3f, ans = INF; + int[][] dist = new int[m][n]; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) dist[i][j] = INF; + } + PriorityQueue d = new PriorityQueue<>((a,b)->a[2]-b[2]); + for (int i = 0; i < n; i++) { + d.add(new int[]{0, i, grid[0][i]}); + dist[0][i] = grid[0][i]; + } + while (!d.isEmpty()) { + int[] info = d.poll(); + int x = info[0], y = info[1], cur = info[2]; + if (x == m - 1) { + ans = Math.min(ans, cur); + continue; + } + for (int i = 0; i < n; i++) { + int step = moveCost[grid[x][y]][i], ne = grid[x + 1][i]; + int tot = cur + step + ne; + if (tot >= ans || dist[x + 1][i] <= tot) continue; + dist[x + 1][i] = tot; + d.add(new int[]{x + 1, i, tot}); + } + } + return ans; + } +} +``` +C++ 代码: +```C++ +class Solution { +public: + int minPathCost(vector>& grid, vector>& moveCost) { + int m = grid.size(), n = grid[0].size(), INF = 0x3f3f3f3f, ans = INF; + vector> dist(m, vector(n, INF)); + priority_queue, vector>, greater>> pq; + for (int i = 0; i < n; i++) { + pq.push({0, i, grid[0][i]}); + dist[0][i] = grid[0][i]; + } + while (!pq.empty()) { + vector info = pq.top(); + pq.pop(); + int x = info[0], y = info[1], cur = info[2]; + if (x == m - 1) { + ans = min(ans, cur); + continue; + } + for (int i = 0; i < n; i++) { + int step = moveCost[grid[x][y]][i], ne = grid[x + 1][i]; + int tot = cur + step + ne; + if (tot >= ans || dist[x + 1][i] <= tot) continue; + dist[x + 1][i] = tot; + pq.push({x + 1, i, tot}); + } + } + return ans; + } +}; +``` +Python 代码: +```Python +class Solution: + def minPathCost(self, grid, moveCost): + m, n, INF = len(grid), len(grid[0]), float('inf') + ans = INF + dist = [[INF] * n for _ in range(m)] + for i in range(n): + dist[0][i] = grid[0][i] + pq = [(0, i, grid[0][i]) for i in range(n)] + while pq: + x, y, cur = heapq.heappop(pq) + if x == m - 1: + ans = min(ans, cur) + continue + for i in range(n): + step, ne = moveCost[grid[x][y]][i], grid[x + 1][i] + tot = cur + step + ne + if tot >= ans or dist[x + 1][i] <= tot: continue + dist[x + 1][i] = tot + heapq.heappush(pq, (x + 1, i, tot)) + return ans +``` +* 时间复杂度:$O(m\log{n})$,其中 $n$ 为新图中的点数 $N \times M = 2.5 \times 10^3$,$m$ 为新图中的边数 $(M - 1) \times N^2 = 1.25 \times 10^5$ +* 空间复杂度:$O(n + m)$ + +--- + +### 原地模拟 + +什么?你说你连图论的方法都不想用,想就着题意做一遍? + +可以。甚至当你调整更新方向,还能利用已有的 `grid`,实现原地模拟。 + +具体的,我们将“从上往下走”调整为“从下往上走”,这样可以确保当我们使用底下一行 $grid[i + 1][j]$ 来更新当前行 $grid[i][j]$ 时,所用到的 $grid[i + 1][j]$ 不会被覆盖。 + + +Java 代码: +```Java +class Solution { + public int minPathCost(int[][] grid, int[][] moveCost) { + int m = grid.length, n = grid[0].length, INF = 0x3f3f3f3f, ans = INF; + for (int i = m - 2; i >= 0; i--) { + for (int j = 0; j < n; j++) { + int cur = INF; + for (int k = 0; k < n; k++) cur = Math.min(cur, grid[i + 1][k] + moveCost[grid[i][j]][k]); + grid[i][j] += cur; + } + } + for (int i = 0; i < n; i++) ans = Math.min(ans, grid[0][i]); + return ans; + } +} +``` +C++ 代码: +```C++ +class Solution { +public: + int minPathCost(vector>& grid, vector>& moveCost) { + int m = grid.size(), n = grid[0].size(), INF = INT_MAX, ans = INF; + for (int i = m - 2; i >= 0; i--) { + for (int j = 0; j < n; j++) { + int cur = INF; + for (int k = 0; k < n; k++) cur = min(cur, grid[i + 1][k] + moveCost[grid[i][j]][k]); + grid[i][j] += cur; + } + } + for (int i = 0; i < n; i++) ans = min(ans, grid[0][i]); + return ans; + } +}; +``` +Python 代码: +```Python +class Solution: + def minPathCost(self, grid, moveCost): + m, n = len(grid), len(grid[0]) + for i in range(m - 2, -1, -1): + for j in range(n): + grid[i][j] += min([grid[i + 1][k] + moveCost[grid[i][j]][k] for k in range(n)]) + return min([grid[0][i] for i in range(n)]) +``` +TypeScript 代码: +```TypeScript +function minPathCost(grid: number[][], moveCost: number[][]): number { + let m = grid.length, n = grid[0].length, INF = 0x3f3f3f3f, ans = INF; + for (let i = m - 2; i >= 0; i--) { + for (let j = 0; j < n; j++) { + let cur = INF; + for (let k = 0; k < n; k++) cur = Math.min(cur, grid[i + 1][k] + moveCost[grid[i][j]][k]); + grid[i][j] += cur; + } + } + for (let i = 0; i < n; i++) ans = Math.min(ans, grid[0][i]); + return ans; +}; +``` +* 时间复杂度:$O(m \times n^2)$,其中 $m$ 和 $n$ 分别代表给定 `grid` 的长宽 +* 空间复杂度:$O(1)$ + +--- + +### 最后 + +这是我们「刷穿 LeetCode」系列文章的第 `No.2304` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 + +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 + +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 + +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 +