← 題庫 / Archive
2026-04-29 Daily Hard ArrayDynamic ProgrammingMatrixPrefix Sum

3225. Maximum Score From Grid Operations

題目 / Problem

中文:給定一個 n × n 的方陣 grid,初始所有格子皆為白色。一次操作可選定 (i, j),把第 j 欄從第 0 列一路塗黑到第 i 列。最終分數為:所有「自身仍是白色,且左或右水平相鄰有黑色格」的格子 grid[i][j] 之總和。回傳能達到的最大分數。

English: You are given an n × n grid grid. All cells start white. One operation picks (i, j) and paints column j black from row 0 down to row i. The score is Σ grid[i][j] over all cells that are still white and have at least one horizontally adjacent black cell. Return the maximum score achievable.

Constraints: 1 ≤ n ≤ 100, 0 ≤ grid[i][j] ≤ 10^9.

Example 1 - Input: grid = [[0,0,0,0,0],[0,0,3,0,0],[0,1,0,0,0],[5,0,0,3,0],[0,0,0,0,2]] - Output: 11 - Paint column 1 down to row 3 (height 4) and column 4 down to row 4 (height 5). Score = grid[3][0] + grid[1][2] + grid[3][3] = 5 + 3 + 3 = 11.

名詞解釋 / Glossary

  • 欄高 / Column height h[j]:第 j 欄被塗黑的格數。若 h[j] = k,則第 0 ~ k-1 列為黑、第 k ~ n-1 列為白。把每欄獨立用一個 0..n 的整數描述,是壓縮狀態的關鍵。
  • 動態規劃 / Dynamic Programming (DP):把大問題拆成許多小子問題,記錄每個子問題的最佳值,避免重算。本題子問題以「目前處理到第幾欄、相鄰兩欄的高度」描述。
  • 狀態 / State:DP 中區分子問題的索引。本題狀態為 (j, h[j-1], h[j])
  • 轉移 / Transition:從一個狀態算出下一個狀態的過程。本題對應「再決定下一欄的高度」。
  • 前綴和 / Prefix Sum:預先把陣列累加起來,使得任意連續區間的和能在 O(1) 取得。本題對每一欄各做一次。
  • 滾動陣列 / Rolling Array:DP 只用上一層推下一層時,只保留兩層而不是 n 層,節省記憶體。
  • long long:64 位整數型態。本題 grid[i][j] ≤ 10^9、n=100,總和最多約 10^4 × 10^9 = 10^13,遠超 32 位 int 的 ~2.1×10^9 上限,必須用 long long 防止 overflow。

思路

我們先觀察操作的結構:每次操作只能把「某一欄從頂端往下」連續塗黑,因此最終局面完全等價於一組欄高陣列 h[0..n-1],每個 h[j] ∈ [0, n]。對於白色格 (i, j)(即 i ≥ h[j]),它要計入分數的條件是「左或右鄰是黑色」,也就是 i < h[j-1]i < h[j+1]。把兩條合併成一條,等同 i < max(h[j-1], h[j+1])。所以第 j 欄的貢獻就是 grid[i][j]i ∈ [h[j], max(h[j-1], h[j+1])) 求和。直接列舉所有 h(n+1)^n,完全爆炸,但這個式子告訴我們:每一欄的貢獻只跟自己跟左右兩鄰有關,於是可以用 DP 由左往右一欄欄定。設 dp[j][a][b] 為「處理到第 j 欄、h[j-1] = ah[j] = b,且第 0 ~ j-1 欄的貢獻都已經加完」的最大分數;第 j 欄的貢獻先延後,因為它要等 h[j+1] 才能算。轉移時枚舉下一欄高度 c = h[j+1],把第 j 欄的貢獻 S(j, b, max(a, c)) 加進去更新 dp[j+1][b][c],其中 S(j, lo, hi) 是第 j 欄第 lo ~ hi-1 列的數值和(用前綴和 O(1))。最後到第 n-1 欄時,把假想 h[n] = 0 的貢獻加上即可。狀態 O(n³)、轉移 O(n),總複雜度 O(n⁴) ≈ 10⁸,n ≤ 100 可過。

The first observation is that any final configuration is fully described by the black height of each column: a single number h[j] ∈ [0, n] saying that rows 0…h[j]−1 of column j are black. A white cell (i, j) (i.e. i ≥ h[j]) earns points exactly when at least one horizontal neighbor is black, which collapses to i < max(h[j-1], h[j+1]). So column j's total contribution is the sum of grid[i][j] for i ∈ [h[j], max(h[j-1], h[j+1])). Brute-forcing all height vectors costs (n+1)^n, but the formula reveals each column only depends on itself and its two neighbors — perfect for left-to-right DP. Define dp[j][a][b] = best score after deciding heights up through column j, where h[j-1] = a and h[j] = b, and we have already booked the contributions from columns 0..j-1. Column j's contribution is deferred because it needs h[j+1]. Transitioning to column j+1 with chosen height c adds column j's contribution S(j, b, max(a, c)), where S(j, lo, hi) is column j's row-range sum (O(1) via per-column prefix sums). At the final column we add its deferred contribution under the convention h[n] = 0. The state space is O(n³), each transition O(n), so total O(n⁴) ≈ 10⁸ for n = 100 — comfortably within limits.

逐步走查 / Walkthrough

Take Example 1 with n = 5 and the optimal heights h = [0, 4, 0, 0, 5]. We trace the per-column contributions, which is exactly what the DP totals up across all candidate heights and then maximizes.

Step 1 — column prefix sums (colSum[j][k] = sum of grid[0..k-1][j]):

j colSum[j]
0 [0, 0, 0, 0, 5, 5]
1 [0, 0, 0, 1, 1, 1]
2 [0, 0, 3, 3, 3, 3]
3 [0, 0, 0, 0, 3, 3]
4 [0, 0, 0, 0, 0, 2]

Step 2 — for the chosen h, compute each column's contribution using top = max(h[j-1], h[j+1]) (treat h[-1] = h[n] = 0):

j h[j-1] h[j] h[j+1] top contrib = colSum[j][top] − colSum[j][h[j]]
0 0 0 4 4 colSum[0][4] − colSum[0][0] = 5 − 0 = 5
1 0 4 0 0 top ≤ h[j], contribution = 0
2 4 0 0 4 colSum[2][4] − colSum[2][0] = 3 − 0 = 3
3 0 0 5 5 colSum[3][5] − colSum[3][0] = 3 − 0 = 3
4 0 5 0 0 top ≤ h[j], contribution = 0

Total = 5 + 0 + 3 + 3 + 0 = 11. ✓

Step 3 — how the DP finds this. The DP considers every triple (h[j-1], h[j], h[j+1]). For example, when transitioning from dp[1][a=0][b=4] (after column 1 with h[0]=0, h[1]=4) into column 2 with c = h[2] = 0, it adds column 1's deferred contribution S(1, b=4, max(0,0)) = 0, recording dp[2][b=4][c=0] = 5 (the 5 came from booking column 0's contribution earlier in the previous transition). Then transitioning into column 3 with h[3]=0 adds column 2's contribution S(2, 0, max(4,0)) = 3, then into column 4 with h[4]=5 adds column 3's contribution S(3, 0, max(0,5)) = 3, and the final step adds column 4's contribution S(4, 5, 0) = 0. Sum = 11.

Solution — C

// 演算法 / Algorithm: 由左往右掃描,維護 dp[h[j-1]][h[j]] = 已完成第 0..j-1 欄貢獻的最大分數。
// 轉移時枚舉下一欄高度 c,把第 j 欄延後的貢獻 S(j, h[j], max(h[j-1], c)) 加進去。
// Sweep columns left-to-right; dp[a][b] = best score with h[j-1]=a, h[j]=b and columns 0..j-1
// fully booked. Each transition picks h[j+1]=c and adds column j's deferred contribution.

#include <stdlib.h>     // 提供基本型態 / basic types
#include <string.h>     // 提供 memcpy / for memcpy
#include <limits.h>     // 提供 LLONG_MIN / for LLONG_MIN

long long maximumScore(int** grid, int gridSize, int* gridColSize) {
    int n = gridSize;                                   // n 為網格邊長 / n is the grid side length

    // static 讓陣列在 BSS 區,避免爆掉函式呼叫堆疊 / static keeps arrays off the call stack
    static long long colSum[101][102];                  // colSum[j][k]=第 j 欄前 k 列之和 / column-j prefix sum
    static long long dp[102][102], ndp[102][102];       // 滾動兩層 / rolling DP layers
    const long long NEG = LLONG_MIN / 4;                // 標記不可達狀態,除以 4 避免相加溢位 / sentinel for unreachable, /4 avoids overflow when added

    // 預計算每欄的前綴和,讓任意列區間和能 O(1) 取得 / precompute per-column prefix sums for O(1) range queries
    for (int j = 0; j < n; j++) {                       // 對每一欄 / for each column
        colSum[j][0] = 0;                               // 空前綴和為 0 / empty prefix sums to 0
        for (int i = 0; i < n; i++) {                   // 從上到下累加 / accumulate top-down
            // 強制轉成 long long,避免在 int 範圍相加時溢位 / cast to long long to avoid int overflow
            colSum[j][i + 1] = colSum[j][i] + (long long)grid[i][j];
        }
    }

    // 初始化 j=0 的狀態 / initialize states for column j=0
    for (int a = 0; a <= n; a++)                        // a = h[-1] (虛擬左鄰) / a = h[-1] (virtual left neighbor)
        for (int b = 0; b <= n; b++)                    // b = h[0]
            dp[a][b] = NEG;                             // 預設不可達 / default unreachable
    for (int b = 0; b <= n; b++)                        // 規定 h[-1]=0,所以只有 a=0 合法 / convention h[-1]=0, only a=0 is valid
        dp[0][b] = 0;                                   // 還沒加任何貢獻 / no contributions added yet

    // 主迴圈:由 j 轉移到 j+1 / main loop: transition from column j to column j+1
    for (int j = 0; j + 1 < n; j++) {
        // 重置 ndp / reset ndp
        for (int b = 0; b <= n; b++)
            for (int c = 0; c <= n; c++)
                ndp[b][c] = NEG;

        for (int b = 0; b <= n; b++) {                  // b = h[j] = 上一狀態的 b / b is current column's height
            for (int c = 0; c <= n; c++) {              // c = h[j+1] = 想決定的下一欄高度 / c is next column's height
                long long best = NEG;                   // 在所有 a 中取最大 / take max over all a
                for (int a = 0; a <= n; a++) {          // a = h[j-1] = 上一狀態的 a / a is previous-previous column's height
                    if (dp[a][b] <= NEG) continue;      // 跳過不可達狀態 / skip unreachable
                    int top = (a > c) ? a : c;          // top = max(h[j-1], h[j+1]) 決定第 j 欄白色貢獻邊界 / top sets the upper bound for column j's white contributions
                    // 第 j 欄的貢獻:第 b 列到第 top-1 列的數值和,若 b ≥ top 則為 0
                    // Column j's contribution: sum of rows [b, top); if b ≥ top, no white cell qualifies
                    long long contrib = (b < top) ? (colSum[j][top] - colSum[j][b]) : 0;
                    long long val = dp[a][b] + contrib; // 加上延後的貢獻 / add deferred contribution
                    if (val > best) best = val;         // 更新最大值 / update max
                }
                ndp[b][c] = best;                       // 寫入新狀態 / store new state
            }
        }
        memcpy(dp, ndp, sizeof(dp));                    // 滾動:把 ndp 變成下一輪的 dp / roll layers: ndp becomes dp
    }

    // 最後一欄(j = n-1)還欠一次貢獻,用 h[n]=0 套公式 / last column still has a deferred contribution; apply with h[n]=0
    long long ans = 0;                                  // 答案至少為 0(全不操作) / answer is at least 0 (no operations)
    int j = n - 1;
    for (int a = 0; a <= n; a++) {
        for (int b = 0; b <= n; b++) {
            if (dp[a][b] <= NEG) continue;              // 不可達跳過 / skip unreachable
            // h[n]=0,top = max(a, 0) = a,故貢獻為 [b, a) 之和
            // h[n]=0 makes top = a, so contribution is sum over rows [b, a)
            long long contrib = (b < a) ? (colSum[j][a] - colSum[j][b]) : 0;
            long long val = dp[a][b] + contrib;
            if (val > ans) ans = val;                   // 取最大 / track max
        }
    }
    return ans;                                         // 回傳最大分數 / return the max score
}

Solution — C++

// 演算法 / Algorithm: 同 C 版本 — 由左往右 DP,dp[a][b] 表示已完成 0..j-1 欄貢獻、且 h[j-1]=a、h[j]=b 的最大分數。
// Same as the C version: left-to-right DP where dp[a][b] is the best score with columns 0..j-1
// fully booked, h[j-1]=a, h[j]=b. Transitions choose h[j+1]=c and pay column j's deferred contribution.

#include <vector>       // std::vector 動態陣列 / std::vector dynamic array
#include <climits>      // LLONG_MIN
#include <algorithm>    // std::max
using namespace std;

class Solution {
public:
    long long maximumScore(vector<vector<int>>& grid) {
        int n = grid.size();                                   // n = 邊長 / n = side length

        // 每欄前綴和:colSum[j][k] = grid[0..k-1][j] 之和 / per-column prefix sum
        // vector<vector<long long>> 為二維動態陣列,用 long long 避免 overflow
        // 2D dynamic array of long long to prevent overflow
        vector<vector<long long>> colSum(n, vector<long long>(n + 1, 0));
        for (int j = 0; j < n; j++) {
            for (int i = 0; i < n; i++) {
                colSum[j][i + 1] = colSum[j][i] + grid[i][j];  // 自動提升為 long long / auto-promotes to long long
            }
        }

        const long long NEG = LLONG_MIN / 4;                   // 不可達狀態哨兵 / unreachable sentinel
        // dp[a][b]:目前欄為 j,a = h[j-1],b = h[j] / dp[a][b]: a is h[j-1], b is h[j] for current j
        vector<vector<long long>> dp(n + 1, vector<long long>(n + 1, NEG));
        for (int b = 0; b <= n; b++) dp[0][b] = 0;             // 初始 j=0:只有 a=0 合法 / init j=0: only a=0 valid

        // 一欄欄推進 / advance column by column
        for (int j = 0; j + 1 < n; j++) {
            // 新一層的 dp,先全填 NEG / fresh layer, default NEG
            vector<vector<long long>> ndp(n + 1, vector<long long>(n + 1, NEG));
            for (int b = 0; b <= n; b++) {                     // b = h[j]
                for (int c = 0; c <= n; c++) {                 // c = h[j+1]
                    long long best = NEG;
                    for (int a = 0; a <= n; a++) {             // a = h[j-1]
                        if (dp[a][b] <= NEG) continue;         // 不可達 / unreachable
                        int top = max(a, c);                   // 第 j 欄白色貢獻的上邊界 / upper bound of column j's white contribution
                        // 用前綴和 O(1) 算第 j 欄列 [b, top) 之和 / O(1) range sum of column j rows [b, top)
                        long long contrib = (b < top) ? (colSum[j][top] - colSum[j][b]) : 0;
                        best = max(best, dp[a][b] + contrib);  // 取最佳 / keep the best
                    }
                    ndp[b][c] = best;
                }
            }
            dp = move(ndp);                                    // 滾動,move 避免複製成本 / roll layers; move avoids deep copy
        }

        // 最後加上 j=n-1 欄的延後貢獻,假設右邊高度為 0 / pay column n-1's deferred contribution with h[n]=0
        long long ans = 0;
        int j = n - 1;
        for (int a = 0; a <= n; a++) {
            for (int b = 0; b <= n; b++) {
                if (dp[a][b] <= NEG) continue;
                // h[n]=0,故 top = max(a, 0) = a,貢獻為列 [b, a) 之和
                // h[n]=0 → top = a, contribution = sum over rows [b, a)
                long long contrib = (b < a) ? (colSum[j][a] - colSum[j][b]) : 0;
                ans = max(ans, dp[a][b] + contrib);            // std::max 比較取大者 / std::max picks the larger
            }
        }
        return ans;                                            // 最終答案 / final answer
    }
};

複雜度 / Complexity

  • Time: O(n⁴) — 狀態數 O(n³)(j、a、b),每次轉移再枚舉一次 O(n)(c 或 a),共 ~ n × (n+1)³ ≈ 10⁸ 次基本運算。n is the grid side length; the dominant cost is the triple-nested (b, c, a) loop inside the column sweep.
  • Space: O(n²) — 滾動只保留兩層 dp,加上 O(n²) 的欄前綴和。Two rolling DP layers plus the per-column prefix-sum table.

Pitfalls & Edge Cases

  • 整數溢位 / Integer overflow:每格最大 10⁹,總和可達 n² × 10⁹ = 10¹³,超過 32-bit int (~2.1 × 10⁹)。整條鏈(前綴和、dp、回傳值)都要用 long long,否則會默默截斷出錯。
  • max(a, c) ≤ b 時不可加負貢獻 / Empty range guard:若 b ≥ top,第 j 欄沒有任何白格滿足條件,貢獻為 0。少這個守護,會把 colSum[j][top] - colSum[j][b] 算成負數而毀壞分數。
  • 邊界欄高 / Boundary heights:第 0 欄沒有左鄰、第 n-1 欄沒有右鄰,等同 h[-1] = h[n] = 0。初始化時只允許 a = 0,最終加貢獻時用 top = a,正是這個慣例的具體實現。
  • 不可達狀態的哨兵 / Sentinel for unreachable:用 LLONG_MIN 直接相加會 overflow,故用 LLONG_MIN / 4 並在加之前 if (dp[a][b] <= NEG) continue; 跳過。
  • n = 1:只有一欄、沒有任何鄰居,所有白格都拿不到分。程式中沒有任何轉移迴圈會跑、最終加貢獻時 a = 0 也得 0,正確回傳 0。
  • 全 0 網格 / All-zero grid:答案為 0,程式以 ans = 0 起始,即使所有狀態都為 0 也不會回傳負值。
  • 不要忘記延後貢獻 / Don't forget the last deferred contribution:DP 結束時還欠最後一欄,如果直接 return max(dp[*][*]) 會少算最後一欄的右邊界貢獻 — 這是最容易掉的洞。