← 題庫 / Archive
2026-04-30 Daily Medium ArrayDynamic ProgrammingMatrix

3742. Maximum Path Score in a Grid

題目 / Problem

給定 m x n 的網格 grid,每個格子的值為 012,以及一個整數 k。從左上角 (0, 0) 出發,每步只能向或向走,目標是抵達右下角 (m-1, n-1)。每個格子貢獻的分數與成本如下:

  • 0:分數 +0,成本 +0
  • 1:分數 +1,成本 +1
  • 2:分數 +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 <= 200
  • 0 <= k <= 10^3
  • grid[0][0] == 0
  • 0 <= grid[i][j] <= 2

Example: grid = [[0, 1], [2, 0]], k = 12. 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 [] and size().

思路

最直覺的暴力作法是枚舉所有「只往右或往下」的路徑,計算每條路徑的分數與成本。網格大小是 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]=00+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]=00+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]=11+0=1; from-left dp[0][1]=22+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 ≤ 200k ≤ 1000,總共約 4 × 10⁷ 次操作,可接受。 / Each of the m·n cells runs an inner loop of size k+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 with dp[...] >= 0 before adding score; otherwise -1 propagates as a false "valid" state.
  • 滾動更新順序 / Rolling-array orderj 必須由左到右,且要用暫存 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, not 0 — 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.