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] = a、h[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-bitint(~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[*][*])會少算最後一欄的右邊界貢獻 — 這是最容易掉的洞。