題目的難度顏色使用 Luogu 上的分級,由簡單到困難分別為 🔴🟠🟡🟢🔵🟣⚫。
Problem Statement
題目簡述
給定長度為 n n n 的整數序列與目標高度 h h h 。
一次操作可以選擇一段連續區間,將區間內所有元素加 1 1 1 。
所有被選區間的左端點必須兩兩不同,右端點也必須兩兩不同。
求有多少種操作方案可以讓所有元素最後都等於 h h h ,答案對 1 0 9 + 7 10^9+7 1 0 9 + 7 取模。
Constraints
約束條件
1 ≤ n ≤ 2000 1 \le n \le 2000 1 ≤ n ≤ 2 0 0 0
1 ≤ h ≤ 2000 1 \le h \le 2000 1 ≤ h ≤ 2 0 0 0
0 ≤ A i ≤ 2000 0 \le A_i \le 2000 0 ≤ A i ≤ 2 0 0 0
所有輸入皆為整數
思路:把補高度轉成區間覆蓋
先把「加區間」換個角度看。令
B i = h − A i B_i = h - A_i
B i = h − A i
則 B i B_i B i 代表第 i i i 個位置還需要被多少個操作區間覆蓋。換句話說,當我們從左到右掃描到位置 i i i 時,必須剛好有 B i B_i B i 個區間覆蓋住位置 i i i 。
如果存在 B i < 0 B_i < 0 B i < 0 ,表示原本高度已經超過 h h h ,無論如何都不能靠加法降回來,答案直接是 0 0 0 。否則,問題就變成:構造若干區間,使第 i i i 個位置剛好被覆蓋 B i B_i B i 次,且每個位置最多當一次左端點、最多當一次右端點。
這裡雖然可以用左右括號想像區間開閉,但結束一個區間時,不需要和最近開啟的區間配對。
只要右端點位置不同,就能從所有尚未結束的區間中任選一個結束,因此減少覆蓋數時的貢獻是目前開啟數量,而不是固定 1 1 1 。
方法一:差分陣列與組合數學
為了處理邊界,令 B 0 = B n + 1 = 0 B_0 = B_{n+1} = 0 B 0 = B n + 1 = 0 。從左到右觀察相鄰兩個位置的需求差異
B i − B i − 1 B_i - B_{i-1}
B i − B i − 1
由於每個位置最多只能作為一次左端點、一次右端點,所以從 i − 1 i-1 i − 1 走到 i i i 時,覆蓋數最多增加 1 1 1 ;從 i i i 走到下一個位置時,覆蓋數最多減少 1 1 1 。也就是說,相鄰兩項的差值只能是 − 1 , 0 , 1 -1,0,1 − 1 , 0 , 1 ,否則不可能用合法區間構造出來。
當我們從 i − 1 i-1 i − 1 移動到 i i i 時,可以分成以下四種情況討論:
B i = B i − 1 + 1 B_i = B_{i-1}+1 B i = B i − 1 + 1
位置 i i i 比前一個位置多需要一層覆蓋。由於前一個位置不需要這一層,而目前位置需要,這一層只能來自一個從 i i i 開始的新區間。
又因為每個位置最多只能作為一次左端點,所以這裡必須開啟新區間,方案數貢獻為 1 1 1 。
B i = B i − 1 − 1 B_i = B_{i-1}-1 B i = B i − 1 − 1
位置 i i i 比前一個位置少需要一層覆蓋,表示有一個區間覆蓋了 i − 1 i-1 i − 1 ,但不再覆蓋 i i i ,也就是它在 i − 1 i-1 i − 1 結束。
在位置 i − 1 i-1 i − 1 時共有 B i − 1 B_{i-1} B i − 1 個區間仍然開啟,可以任選其中一個結束,因此方案數貢獻為 B i − 1 B_{i-1} B i − 1 。
B i = B i − 1 B_i = B_{i-1} B i = B i − 1
覆蓋數不變,有兩種可能:
什麼都不做,沒有區間在這裡開始或結束,貢獻 1 1 1 。
關閉一個舊區間,並在位置 i i i 開啟一個新區間。此時可從 B i − 1 B_{i-1} B i − 1 個已開啟區間中任選一個結束,貢獻 B i − 1 B_{i-1} B i − 1 。
所以總貢獻為 B i − 1 + 1 B_{i-1}+1 B i − 1 + 1 。
∣ B i − B i − 1 ∣ > 1 |B_i-B_{i-1}|>1 ∣ B i − B i − 1 ∣ > 1
變化量超過 1 1 1 ,但單一位置最多只能新增一個左端點、移除一個右端點,無法讓覆蓋數一次跳這麼多。
因此這種情況不合法,答案為 0 0 0 。
把每一步的合法選擇數相乘,就是總方案數。
複雜度分析
時間複雜度:O ( n ) \mathcal{O}(n) O ( n ) ,只需掃描一次覆蓋需求。
空間複雜度:O ( n ) \mathcal{O}(n) O ( n ) 。
方法二:Open and Close Interval Trick
另一種寫法是把區間看成從左往右逐步「開啟」與「關閉」。掃描到位置 i i i 時,必須剛好有 B i B_i B i 個區間覆蓋住它;掃描完位置 i i i 後,我們只需要知道還有多少區間會繼續覆蓋下一個位置。
狀態定義
令 f [ i ] [ j ] f[i][j] f [ i ] [ j ] 表示處理完前 i i i 個位置後,仍然有 j j j 個區間處於開啟狀態,也就是會繼續覆蓋位置 i + 1 i+1 i + 1 的方案數。
初始狀態為 f [ 0 ] [ 0 ] = 1 f[0][0]=1 f [ 0 ] [ 0 ] = 1 ,表示還沒處理任何位置時,沒有任何區間開啟。
狀態轉移
考慮位置 i i i 。假設處理位置 i i i 之前,已經有 j j j 個區間處於開啟狀態,也就是這些區間原本就會覆蓋位置 i i i 。接著從這個舊狀態往下一層更新,可以分成五種操作形態:
不開不關
已有的 j j j 個區間直接通過目前位置,處理完後仍留下 j j j 個開啟區間,覆蓋數為 j j j 。
因此需要 j = B i j=B_i j = B i ,轉移為:
f [ i ] [ j ] ← f [ i ] [ j ] + f [ i − 1 ] [ j ] f[i][j] \leftarrow f[i][j] + f[i-1][j]
f [ i ] [ j ] ← f [ i ] [ j ] + f [ i − 1 ] [ j ]
開啟一個新區間
新區間從位置 i i i 開始,並會繼續傳到下一位。原本有 j j j 個區間開啟,處理完後留下 j + 1 j+1 j + 1 個開啟區間。
目前位置被 j + 1 j+1 j + 1 個區間覆蓋,因此需要 j + 1 = B i j+1=B_i j + 1 = B i ,轉移為:
f [ i ] [ j + 1 ] ← f [ i ] [ j + 1 ] + f [ i − 1 ] [ j ] f[i][j+1] \leftarrow f[i][j+1] + f[i-1][j]
f [ i ] [ j + 1 ] ← f [ i ] [ j + 1 ] + f [ i − 1 ] [ j ]
關閉一個舊區間
有一個舊區間在位置 i i i 結束。它仍然覆蓋目前位置,但不會傳到下一位。
原本有 j j j 個區間開啟,目前位置被 j j j 個區間覆蓋,所以需要 j = B i j=B_i j = B i 。結束時要從這 j j j 個舊區間中選一個,處理完後留下 j − 1 j-1 j − 1 個開啟區間,轉移為:
f [ i ] [ j − 1 ] ← f [ i ] [ j − 1 ] + f [ i − 1 ] [ j ] ⋅ j f[i][j-1] \leftarrow f[i][j-1] + f[i-1][j]\cdot j
f [ i ] [ j − 1 ] ← f [ i ] [ j − 1 ] + f [ i − 1 ] [ j ] ⋅ j
開啟並立即關閉
在位置 i i i 放一個長度為 1 1 1 的區間。這個新區間覆蓋目前位置,但不會留下來。
處理前後的開啟區間數都為 j j j ,目前位置實際被 j + 1 j+1 j + 1 個區間覆蓋,所以需要 j + 1 = B i j+1=B_i j + 1 = B i ,轉移為:
f [ i ] [ j ] ← f [ i ] [ j ] + f [ i − 1 ] [ j ] f[i][j] \leftarrow f[i][j] + f[i-1][j]
f [ i ] [ j ] ← f [ i ] [ j ] + f [ i − 1 ] [ j ]
關閉一個舊區間,同時開啟一個新區間
有一個舊區間在位置 i i i 結束,同時有一個新區間從位置 i i i 開始。處理前後留下的開啟區間數都為 j j j 。
目前位置被「原本 j j j 個舊區間」加上「新開的一個區間」覆蓋,所以需要 j + 1 = B i j+1=B_i j + 1 = B i 。關閉的舊區間可以從原本 j j j 個開啟區間中選一個,轉移為:
f [ i ] [ j ] ← f [ i ] [ j ] + f [ i − 1 ] [ j ] ⋅ j f[i][j] \leftarrow f[i][j] + f[i-1][j]\cdot j
f [ i ] [ j ] ← f [ i ] [ j ] + f [ i − 1 ] [ j ] ⋅ j
為什麼每步只有常數個狀態
固定目前位置後,合法的留下數只有兩種:
若目前位置沒有額外結束的區間,留下的開啟數必須是 B i B_i B i 。
若目前位置有一個區間結束,或有一個長度為 1 1 1 的區間,留下的開啟數必須是 B i − 1 B_i-1 B i − 1 。
因此不需要真的維護整張二維 DP 表,只需要保留可能出現的狀態即可。
複雜度分析
時間複雜度:O ( n ) \mathcal{O}(n) O ( n ) ,每個位置只會更新常數個可能狀態。
空間複雜度:O ( 1 ) \mathcal{O}(1) O ( 1 ) ,滾動字典中有效狀態數為常數級。
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 from itertools import pairwiseMOD = int (1e9 + 7 ) def solve (): n, h = map (int , input ().split()) A = list (map (int , input ().split())) assert len (A) == n B = [0 ] + [h - x for x in A] + [0 ] if any (x < 0 for x in B): print (0 ) return ans = 1 for x, y in pairwise(B): diff = y - x if diff == 1 : ans = (ans * 1 ) elif diff == -1 : ans = (ans * x) % MOD elif diff == 0 : ans = (ans * (x + 1 )) % MOD else : print (0 ) return print (ans) if __name__ == "__main__" : solve()
方法二:動態規劃
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 from collections import defaultdictMOD = int (1e9 + 7 ) def solve (): n, h = map (int , input ().split()) A = list (map (int , input ().split())) assert len (A) == n if any (x > h for x in A): print (0 ) return f = defaultdict(int ) f[0 ] = 1 for x in A: nf = defaultdict(int ) need = h - x for j, ways in f.items(): if ways == 0 : continue if j == need: nf[j] = (nf[j] + ways) % MOD if j + 1 == need: nf[j + 1 ] = (nf[j + 1 ] + ways) % MOD if j == need and j > 0 : nf[j - 1 ] = (nf[j - 1 ] + ways * j) % MOD if j + 1 == need: nf[j] = (nf[j] + ways) % MOD if j + 1 == need and j > 0 : nf[j] = (nf[j] + ways * j) % MOD f = nf print (f[0 ]) if __name__ == "__main__" : solve()