3742. Maximum Path Score in a Grid
題目 / Problem
給定 m x n 的網格 grid,每個格子的值為 0、1 或 2,以及一個整數 k。從左上角 (0, 0) 出發,每步只能向右或向下走,目標是抵達右下角 (m-1, n-1)。每個格子貢獻的分數與成本如下:
0:分數 +0,成本 +01:分數 +1,成本 +12:分數 +2,成本 +1
請回傳在總成本不超過 k 的條件下,可獲得的最大分數;若不存在合法路徑,回傳 -1。
You are given an m x n grid where each cell holds 0, 1, or 2, plus an integer k. Starting from (0, 0) you may move only right or down, ending at (m-1, n-1). Each cell contributes a score and a cost: 0 adds (0, 0), 1 adds (1, 1), 2 adds (2, 1). Return the maximum score reachable while keeping total cost ≤ k, or -1 if no path satisfies the constraint.
Constraints
1 <= m, n <= 2000 <= k <= 10^3grid[0][0] == 00 <= grid[i][j] <= 2
Example: grid = [[0, 1], [2, 0]], k = 1 → 2. The path (0,0) → (1,0) → (1,1) collects scores 0 + 2 + 0 = 2 with cost 0 + 1 + 0 = 1 ≤ k.
名詞解釋 / Glossary
- 動態規劃 / Dynamic Programming (DP) — 把大問題拆成有重疊的子問題,把每個子問題的答案存起來,避免重複計算。A technique that solves a big problem by combining cached answers to overlapping subproblems.
- 狀態 / DP state — 描述一個子問題所需的所有資訊。本題的狀態是
(行, 列, 已花成本)。The set of variables that uniquely identifies a subproblem; here it is(row, column, cost-spent-so-far). - 狀態轉移 / Transition — 從較小的狀態推到較大的狀態的公式。The recurrence that derives a state's value from previously computed states.
- 滾動陣列 / Rolling array — 觀察到只需要前一行的結果,就不必開三維陣列,改用兩維陣列覆寫舊值,節省記憶體。Reusing a smaller array (here 2D instead of 3D) by overwriting old entries we no longer need; saves memory dramatically.
- 哨兵值 / Sentinel value — 用一個特殊值(本題用
-1)代表「不可達」,讓邏輯判斷更清晰。A special marker (here-1) meaning "this state is unreachable", simplifies branching. malloc/free— C 語言中向堆 (heap) 申請與釋放記憶體的函式;比放在 stack 上的大陣列更安全。C functions to allocate / release heap memory; safer than huge stack arrays for big DP tables.std::vector— C++ 動態陣列;會自動管理記憶體,可用[]取值、size()取長度。C++'s dynamic array; manages its own memory and supports[]andsize().
思路
最直覺的暴力作法是枚舉所有「只往右或往下」的路徑,計算每條路徑的分數與成本。網格大小是 200×200,路徑數量是 C(398, 199),遠超過 10^100,完全不可行。觀察結構:從 (0,0) 走到 (i,j) 的路徑,最後一步一定來自 (i-1,j) 或 (i,j-1),於是子問題具備明顯的「重疊」與「最佳子結構」,適合動態規劃。我們無法只用「最大分數到 (i,j)」當狀態,因為一條目前分數較高但成本也較高的部分路徑,未必能在 k 之內走到終點;因此必須把「目前累計成本」也當狀態,設 dp[i][j][c] 為走到 (i,j) 且恰好花了成本 c 時的最大分數。轉移為 dp[i][j][c] = max(dp[i-1][j][c-Δc], dp[i][j-1][c-Δc]) + score(i,j),其中 Δc = (grid[i][j] != 0)。最後在 dp[m-1][n-1][0..k] 取最大值即為答案,若全部不可達則回傳 -1。直接開 200×200×1001 的 int 陣列要約 160 MB,記憶體吃不消;但注意第 i 行只依賴第 i-1 行,因此只需保留目前這行的 2D 表 dp[j][c],逐列就地更新即可,空間降到 O(n·k) ≈ 800 KB。讀取 dp[j-1] 時需要當前行(已更新),讀取 dp[j] 時需要上一行(尚未覆寫),處理順序天然滿足這個要求。
The brute-force approach enumerates every right/down path — there are C(398, 199) of them, hopelessly many. The structure of the problem is classic DP: the last step into (i, j) came from (i-1, j) or (i, j-1), so subproblems overlap. We cannot collapse "best score to (i, j)" into a single number because a higher-scoring partial path may also be more expensive and fail to finish under budget k; we must keep the spent cost as part of the state. Define dp[i][j][c] = the maximum score achievable at (i, j) having spent exactly cost c, with transition dp[i][j][c] = max(dp[i-1][j][c-Δc], dp[i][j-1][c-Δc]) + grid[i][j] where Δc = 1 if grid[i][j] != 0 else 0. The answer is max(dp[m-1][n-1][c]) over c = 0..k, or -1 if every entry is unreachable. A naive 3D table costs ~160 MB for the worst-case constraints, so we use a rolling 2D array indexed by (column, cost): row i only depends on row i-1, and within row i the value at column j reads dp[j-1] (already updated, this row) and dp[j] (still last row, not yet overwritten). Iterating columns left-to-right keeps both reads consistent, and memory drops to O(n·k).
逐步走查 / Walkthrough
Trace for grid = [[0, 1], [2, 0]], k = 1. Use dp[j][c], where -1 = unreachable. Initial state: dp[0] = [0, -1], dp[1] = [-1, -1] (only dp[0][0] = 0 is set).
| 步驟 / Step | Cell (i, j) |
v, cost, score |
Updates | dp[0] after |
dp[1] after |
|---|---|---|---|---|---|
| 1 | (0, 0) |
skip — base case | — | [0, -1] |
[-1, -1] |
| 2 | (0, 1) |
v=1, cost=1, score=1 |
c=1: from-left dp[0][0]=0 → 0+1=1; from-above blocked (i=0) |
[0, -1] |
[-1, 1] |
| 3 | (1, 0) |
v=2, cost=1, score=2 |
c=1: from-above dp[0][0]=0 → 0+2=2; from-left blocked (j=0) |
[-1, 2] |
[-1, 1] |
| 4 | (1, 1) |
v=0, cost=0, score=0 |
c=0: both reads are -1, stays -1. c=1: from-above dp[1][1]=1 → 1+0=1; from-left dp[0][1]=2 → 2+0=2; take 2 |
[-1, 2] |
[-1, 2] |
Final row dp[n-1] = dp[1] = [-1, 2]. Take the max → 2. ✅
Solution — C
// 演算法 / Algorithm:
// dp[j][c] = 走到目前列、第 j 行,且恰好花成本 c 時的最大分數
// dp[j][c] = max score upon reaching column j of the current row with exact cost c.
// 逐行 (row-by-row) 滾動更新,讀 dp[j] 拿上一行,讀 dp[j-1] 拿這一行已更新值。
// We roll row-by-row; dp[j] still holds the previous row, dp[j-1] holds the current row.
// 時間 O(m*n*k),空間 O(n*k) / Time O(m*n*k), space O(n*k).
#include <stdlib.h> // 提供 malloc / free / provides malloc, free
int maximumPathScore(int** grid, int gridSize, int* gridColSize, int k) {
int m = gridSize; // 行數 / number of rows
int n = gridColSize[0]; // 列數 / number of columns
// 配置 dp[n][k+1];使用 -1 代表「不可達」/ allocate dp[n][k+1]; -1 means "unreachable"
int** dp = (int**)malloc(n * sizeof(int*)); // 指標陣列 / array of row pointers
for (int j = 0; j < n; j++) { // 為每一列配置 / allocate each column slice
dp[j] = (int*)malloc((k + 1) * sizeof(int)); // k+1 個成本桶 / k+1 cost buckets
for (int c = 0; c <= k; c++) dp[j][c] = -1; // 初值設為不可達 / mark all unreachable
}
dp[0][0] = 0; // 起點:第 0 列、成本 0、分數 0 / start at column 0, cost 0, score 0
// 暫存陣列,避免就地寫壞同一行內後面的讀取 / scratch row to avoid clobbering reads
int* tmp = (int*)malloc((k + 1) * sizeof(int));
for (int i = 0; i < m; i++) { // 由上到下逐行 / iterate rows top-down
for (int j = 0; j < n; j++) { // 由左到右逐列 / iterate columns left-to-right
if (i == 0 && j == 0) continue; // 起點已初始化過 / base cell already set
int v = grid[i][j]; // 此格的值 0/1/2 / cell value 0, 1, or 2
int cost = (v != 0) ? 1 : 0; // 1/2 花費 1,0 不花費 / nonzero cells cost 1
int score = v; // 分數恰好等於 v / score equals the cell value
for (int c = 0; c <= k; c++) tmp[c] = -1; // 清空暫存 / clear scratch row
// c 從 cost 起跳:不到 cost 進不了這格 / start at c=cost: smaller budgets can't enter this cell
for (int c = cost; c <= k; c++) {
int prev = c - cost; // 進此格前已花的成本 / cost spent before entering
// 從上面來:dp[j][prev] 還是上一行的值 / "from above" reads previous row at dp[j]
int from_above = (i > 0 && dp[j][prev] >= 0)
? dp[j][prev] + score : -1;
// 從左邊來:dp[j-1][prev] 已是這行更新後的值 / "from left" reads current row at dp[j-1]
int from_left = (j > 0 && dp[j-1][prev] >= 0)
? dp[j-1][prev] + score : -1;
int best = (from_above > from_left) ? from_above : from_left; // 取較大 / take the larger
tmp[c] = best; // 寫入暫存 / write into scratch
}
for (int c = 0; c <= k; c++) dp[j][c] = tmp[c]; // 把暫存搬回 dp[j] / commit scratch to dp[j]
}
}
int ans = -1; // 預設無解 / default to "no valid path"
for (int c = 0; c <= k; c++) // 在終點掃所有成本 / scan all costs at the goal
if (dp[n - 1][c] > ans) ans = dp[n - 1][c]; // 取最大分數 / keep the maximum score
for (int j = 0; j < n; j++) free(dp[j]); // 釋放每列 / free each column slice
free(dp); // 釋放指標陣列 / free pointer array
free(tmp); // 釋放暫存 / free scratch row
return ans; // 回傳答案;全不可達就是 -1 / return -1 if all unreachable
}
Solution — C++
// 演算法同 C 版本:dp[j][c] 為「目前列、第 j 行、成本恰好 c」的最大分數,逐行就地滾動。
// Same algorithm as the C version: dp[j][c] = max score at column j with exact cost c
// in the current row, rolled in place row-by-row.
// Time O(m*n*k), space O(n*k).
#include <vector> // std::vector 動態陣列 / dynamic array
#include <algorithm> // std::max, std::max_element, std::fill
class Solution {
public:
int maximumPathScore(std::vector<std::vector<int>>& grid, int k) {
int m = grid.size(); // 行數 / row count; vector::size() 回傳元素數 / number of elements
int n = grid[0].size(); // 列數 / column count
// 用 vector 配置 n × (k+1) 的 2D 表,初值 -1 / 2D vector of n × (k+1), filled with -1
std::vector<std::vector<int>> dp(n, std::vector<int>(k + 1, -1));
dp[0][0] = 0; // 起點 / starting cell
std::vector<int> tmp(k + 1); // 暫存陣列 / scratch row
for (int i = 0; i < m; ++i) { // ++i 是慣用前置遞增 / idiomatic pre-increment
for (int j = 0; j < n; ++j) {
if (i == 0 && j == 0) continue; // 跳過起點 / skip base cell
int v = grid[i][j]; // 取值 / read cell value
int cost = (v != 0) ? 1 : 0; // 1/2 成本 1,0 成本 0 / nonzero cells cost 1
int score = v; // 分數即值本身 / score equals value
std::fill(tmp.begin(), tmp.end(), -1);// 清空暫存 / wipe scratch with -1
for (int c = cost; c <= k; ++c) {
int prev = c - cost; // 進此格前的成本 / cost before entering
int from_above = (i > 0 && dp[j][prev] >= 0)
? dp[j][prev] + score : -1; // 上一行同列 / previous row, same column
int from_left = (j > 0 && dp[j-1][prev] >= 0)
? dp[j-1][prev] + score : -1; // 同行左鄰 / current row, left neighbor
tmp[c] = std::max(from_above, from_left); // 取較大者 / take the larger
}
dp[j] = tmp; // 整行覆寫;vector 賦值會深拷貝 / assign copies the row
}
}
// max_element 回傳指向最大元素的迭代器,*it 解引用取值
// max_element returns an iterator to the largest element; * dereferences it
return *std::max_element(dp[n - 1].begin(), dp[n - 1].end());
}
};
複雜度 / Complexity
- Time: O(m · n · k) — 對每個
(i, j)都掃一次成本0..k進行轉移;m, n ≤ 200、k ≤ 1000,總共約 4 × 10⁷ 次操作,可接受。 / Each of them·ncells runs an inner loop of sizek+1. With the given constraints this is ~4·10⁷ basic ops. - Space: O(n · k) — 滾動之後只保留兩維表
dp[n][k+1]加上 O(k) 的暫存;若不滾動是 O(m·n·k) ≈ 160 MB,會 MLE。 / The rolling 2D table dominates; the full 3D version would blow past memory limits.
Pitfalls & Edge Cases
- 「不超過 k」 vs 「恰好 k」 / "at most k" vs "exactly k" — 題目要求成本 ≤ k,所以最後要對
c = 0..k全部取 max,而不是只看dp[m-1][n-1][k]。/ Final answer scans every cost bucket because any cost ≤ k is allowed. - 不可達狀態 / Unreachable states — 用
-1當哨兵,轉移時必須先判>= 0,否則會把無效路徑當成 0 分傳下去。/ Always guard withdp[...] >= 0before adding score; otherwise-1propagates as a false "valid" state. - 滾動更新順序 / Rolling-array order —
j必須由左到右,且要用暫存tmp寫入,讓dp[j-1]先被覆蓋成「本行新值」、dp[j]等最後才被覆蓋,否則上下/左右兩個方向會互相污染。/ Iterate columns left-to-right and stage updates into a scratch row so each cell's reads still match the intended row. - 起點是 0 / Start cell is
grid[0][0] == 0— 題目保證起點花費 0、分數 0,所以可以把dp[0][0]直接設為 0。/ Guaranteed by constraints; the base case is clean. - 無解情況 / No-valid-path case — 若所有
dp[m-1][n-1][c]都是-1,代表整段路徑沒有方法在預算內走到終點,需回傳-1,而不是 0。/ If every cost bucket at the goal is unreachable, return-1, not0— they have very different meanings. - 記憶體 / Memory — 直接開
int dp[200][200][1001]約 160 MB,會 MLE;一定要滾動。/ The naive 3D array overflows the memory limit; rolling is mandatory.