題目的難度顏色使用 Luogu 上的分級,由簡單到困難分別為 🔴🟠🟡🟢🔵🟣⚫。
Problem Statement
題目簡述
給定一個大小為 R×C 的整數矩陣,其中 Nij 代表第 i 橫列(row) 第 j 直行(column) 的分數。
你需要對這個矩陣進行切割:先水平切 A−1 刀,再對這 A 個切分出來的矩陣分別獨立地垂直切 B−1 刀,將矩陣分成 A×B 個小塊。每一刀必須切在格子之間(即切出完整行或列)。定義每一塊的「分數」為該子矩陣內所有數字的總和。
定義每一塊的「分數」為該子矩陣內所有數字的總和。目標是讓這些 A×B 塊中分數最小的一塊儘量大。輸出這個最大可能的最小值。
Constraints
約束條件
- 1≤R,C≤500
- 1≤A≤R
- 1≤B≤C
- 0≤Nij≤4⋅104
思路:二分答案 + 貪心 + 二維前綴和
這是一個典型的「最大化最小值」問題,這類問題通常可以透過二分搜尋答案來解決。
我們將問題轉化為判定性問題:給定一個目標分數 mid,判斷是否能將矩陣切成 A×B 塊,且每一塊的分數都大於等於 mid。
貪心判定策略
為了判斷是否能切出符合條件的區塊,我們可以採用貪心策略,由上而下、由左至右進行嘗試:
- 尋找合法的水平區塊:我們由上往下逐列擴展當前的水平範圍。對於每一個水平範圍,我們嘗試在其中進行垂直切割。
- 尋找合法的垂直區塊:在固定的水平範圍內,我們由左往右逐行擴展當前的垂直範圍。一旦發現當前累積的子矩陣分數大於等於 mid,我們就貪心地切一刀,並重新開始計算下一個垂直區塊。
- 確認水平切割:如果在當前的水平範圍內,我們能夠成功切出至少 B 個分數大於等於 mid 的區塊,這意味著我們找到了一個合法的水平切分。此時,我們就可以確定這一刀水平切割,並繼續往下尋找下一個水平區塊。
- 擴展水平範圍:如果當前的水平範圍無法切出 B 個合法的區塊,我們就必須繼續往下包含更多的橫列,直到滿足條件為止。
如果在掃描完所有橫列後,我們能夠成功切出至少 A 個合法的水平區塊,就代表當前的目標分數 mid 是可行的。
為什麼可以貪心?因為矩陣中的數字都是非負整數,子矩陣的範圍越大,分數只會增加不會減少。因此,一旦滿足條件就立刻切割,可以為後面的區塊保留最多的剩餘空間,這是最優的策略。
快速計算區塊分數
在貪心判定的過程中,我們需要頻繁地計算某個子矩陣的分數總和。為了避免每次都重新遍歷子矩陣,我們可以預先計算二維前綴和。
透過二維前綴和,我們可以在 O(1) 的時間內查詢任意子矩陣的分數總和,這能大幅提升判定函數的執行效率。
複雜度分析
- 時間複雜度:O(R⋅Clog(A⋅BS)),其中 S 為整個矩陣的總分數。
- 預處理二維前綴和需要 O(R⋅C) 的時間。
- 因為最小塊的最大值不可能超過平均分數,否則總和會超過 S,因此二分搜尋的上界為平均分數的下取整 ⌊S/(A⋅B)⌋,二分搜尋的次數約為 log(A⋅BS) 次。
- 每次判定時,雖然有巢狀迴圈,但水平和垂直的邊界指標都是單調遞增的(類似雙指標的概念),因此單次判定的時間複雜度為 O(R⋅C)。
- 空間複雜度:O(R⋅C),主要用於儲存二維前綴和陣列。
在固定水平區間內尋找垂直切割點時,也可以採用二分搜尋的方式:對於給定的左邊界,二分查詢滿足區塊和 ≥mid 的最左側右邊界位置。由於共需切出 B 個區塊,內層需要進行 B 次二分,每次二分需 O(logC) 時間計算前綴和,因此單次 check 的時間複雜度為 O(R⋅BlogC)。
當 B 遠小於 C 時,此方法可能比雙指標的 O(R⋅C) 更快。但本題中 R,C≤500,且 B 最大可達 C,因此反而是種負優化。
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| def solve(): R, C, A, B = map(int, input().split()) grid = [list(map(int, input().split())) for _ in range(R)]
s = [[0] * (C + 1) for _ in range(R + 1)] for i, row in enumerate(grid, start=1): for j, val in enumerate(row, start=1): s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + val
def area(x1, x2, y1, y2): return s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]
tot = sum(sum(row) for row in grid)
def check(mid): x1 = x2 = 1 cntx = 0 while x2 <= R: y1 = y2 = 1 cnty = 0 while y1 <= C: while y2 <= C and area(x1, x2, y1, y2) < mid: y2 += 1 if y2 > C: break cnty += 1 if cnty == B: break y1 = y2 = y2 + 1 if cnty == B: cntx += 1 if cntx == A: return True x1 = x2 = x2 + 1 else: x2 += 1 return False
left, right = 0, tot // (A * B) while left <= right: mid = (left + right) // 2 if check(mid): left = mid + 1 else: right = mid - 1 print(right)
if __name__ == "__main__": solve()
|