題目的難度顏色使用 Luogu 上的分級,由簡單到困難分別為 🔴🟠🟡🟢🔵🟣⚫。
Problem Statement
題目簡述
給定整數 n , m , x n,m,x n , m , x 。需要寫出一個長度為 n n n 的有序區間序列,每個區間都是 [ l i , r i ] [l_i,r_i] [ l i , r i ] ,滿足 1 ≤ l i ≤ r i ≤ m 1 \le l_i \le r_i \le m 1 ≤ l i ≤ r i ≤ m 。
序列中不能存在某個區間被另一個區間包含,也就是不能有兩個區間 [ l 1 , r 1 ] [l_1,r_1] [ l 1 , r 1 ] 、[ l 2 , r 2 ] [l_2,r_2] [ l 2 , r 2 ] 滿足 l 2 ≤ l 1 ≤ r 1 ≤ r 2 l_2 \le l_1 \le r_1 \le r_2 l 2 ≤ l 1 ≤ r 1 ≤ r 2 。
此外,至少要有一個區間的左端點等於 x x x 。
求合法有序區間序列的數量,答案對 1 0 9 + 7 10^9+7 1 0 9 + 7 取模。
Constraints
約束條件
1 ≤ n ⋅ m ≤ 100000 1 \le n \cdot m \le 100000 1 ≤ n ⋅ m ≤ 1 0 0 0 0 0
1 ≤ x ≤ m 1 \le x \le m 1 ≤ x ≤ m
所有輸入皆為整數
思路:Open and Close Interval Trick
Open and Close Interval Trick
核心技巧是把「選 n n n 個互不包含的區間」改寫成「在值域上放置 n n n 個左端點與 n n n 個右端點」。這樣就不需要直接枚舉區間,而是從左到右掃描每個座標,維護目前已經放了多少左端點、多少右端點。
為什麼可以只看端點數量
先把所有區間按照左端點從小到大排序。若有兩個區間滿足左端點較小、右端點卻不小於另一個區間的右端點,那麼後者就會被前者包含,違反條件。
因此,在合法方案中,左端點排序後,右端點也必須按照同樣順序遞增。也就是說,一旦選好了所有左端點位置與所有右端點位置,合法配對方式其實是唯一的:第 k k k 小的左端點配第 k k k 小的右端點。
問題等價於在 1 1 1 到 m m m 的座標上放端點,使得任意前綴中右端點數量不超過左端點數量,最後左、右端點都各有 n n n 個,且座標 x x x 必須放一個左端點。
這個前綴限制和括號序列很像:掃描到某個位置時,若右端點已經比左端點多,就表示有某個區間必須在尚未開始前就結束,這不可能形成合法配對。
若兩個區間左端點相同,右端點較小的那個一定會被右端點較大的那個包含。因此每個座標最多放一個左端點;這也解釋了為什麼當 n > m n>m n > m 時答案必定為 0 0 0 。
掃描時的四種選擇
對每個座標,只需要關心目前已放置的左端點數量與右端點數量。處理一個普通座標時,有四種選擇:
不放任何端點。
放一個左端點。
放一個右端點,前提是放完後右端點數量不超過左端點數量。
同時放一個左端點與一個右端點。
第四種看起來像是同一個座標上同時出現開始與結束。這在端點模型中是必要的,因為合法區間允許 l = r l=r l = r ,也允許某個區間在此結束、另一個區間在此開始。由於最後會按照排序後的位置唯一配對,DP 不需要在當下判斷這兩個端點屬於哪個區間。
題目要求至少一個區間的左端點等於 x x x 。由於左端點不能重複,所以這等價於「座標 x x x 必須放左端點」。因此掃描到 x x x 時,不能選擇「什麼都不放」或「只放右端點」。
狀態與轉移
狀態定義
設 f [ i ] [ a ] [ b ] f[i][a][b] f [ i ] [ a ] [ b ] 表示處理完座標 1 ∼ i 1\sim i 1 ∼ i 後,已經放置 a a a 個左端點、b b b 個右端點的方案數。其中必須滿足:
0 ≤ b ≤ a ≤ n 0 \le b \le a \le n 0 ≤ b ≤ a ≤ n :任意前綴中,右端點數量不能超過左端點數量。
每個座標最多新增一個左端點,因為左端點不能重複。
初始狀態為:
f [ 0 ] [ 0 ] [ 0 ] = 1 f[0][0][0]=1
f [ 0 ] [ 0 ] [ 0 ] = 1
其餘狀態皆為 0 0 0 。程式中用二維表保存目前掃描前綴的狀態,並用滾動陣列省掉座標維度。
轉移方程:刷表法
下面使用的是「刷表法」:枚舉上一層已經存在的狀態,然後把它的方案數貢獻到下一層能到達的狀態。也就是從 f [ i − 1 ] [ a ] [ b ] f[i-1][a][b] f [ i − 1 ] [ a ] [ b ] 往外推到若干個 f [ i ] [ ⋅ ] [ ⋅ ] f[i][\cdot][\cdot] f [ i ] [ ⋅ ] [ ⋅ ] ,而不是固定某個新狀態再反過來枚舉它有哪些來源。
考慮目前正在處理的座標 i i i ,從狀態 ( a , b ) (a,b) ( a , b ) 出發,記目前方案數為 f [ i − 1 ] [ a ] [ b ] f[i-1][a][b] f [ i − 1 ] [ a ] [ b ] 。
若 i ≠ x i\ne x i = x ,可以不放任何端點:
f [ i ] [ a ] [ b ] + = f [ i − 1 ] [ a ] [ b ] f[i][a][b] \mathrel{+}= f[i-1][a][b]
f [ i ] [ a ] [ b ] + = f [ i − 1 ] [ a ] [ b ]
若 a + 1 ≤ n a+1\le n a + 1 ≤ n ,可以放一個左端點:
f [ i ] [ a + 1 ] [ b ] + = f [ i − 1 ] [ a ] [ b ] f[i][a+1][b] \mathrel{+}= f[i-1][a][b]
f [ i ] [ a + 1 ] [ b ] + = f [ i − 1 ] [ a ] [ b ]
若 i ≠ x i\ne x i = x 且 b + 1 ≤ a b+1\le a b + 1 ≤ a ,可以放一個右端點:
f [ i ] [ a ] [ b + 1 ] + = f [ i − 1 ] [ a ] [ b ] f[i][a][b+1] \mathrel{+}= f[i-1][a][b]
f [ i ] [ a ] [ b + 1 ] + = f [ i − 1 ] [ a ] [ b ]
若 a + 1 ≤ n a+1\le n a + 1 ≤ n ,可以同時放一個左端點與一個右端點:
f [ i ] [ a + 1 ] [ b + 1 ] + = f [ i − 1 ] [ a ] [ b ] f[i][a+1][b+1] \mathrel{+}= f[i-1][a][b]
f [ i ] [ a + 1 ] [ b + 1 ] + = f [ i − 1 ] [ a ] [ b ]
在座標 x x x ,因為必須放左端點,所以「不放任何端點」與「只放右端點」兩種轉移被禁止,只保留包含新增左端點的兩種轉移。
目標答案
最後取左右端點都剛好為 n n n 的方案數。這時 DP 算到的是「無標號區間集合」的端點選法;題目問的是有序序列,n n n 個區間可以任意排列,所以還要乘上 n ! n! n ! 。
a n s = f [ m ] [ n ] [ n ] ⋅ n ! ( m o d 1 0 9 + 7 ) \mathrm{ans}=f[m][n][n]\cdot n! \pmod {10^9+7}
a n s = f [ m ] [ n ] [ n ] ⋅ n ! ( m o d 1 0 9 + 7 )
可復用模型是:當一類物件可以由「開啟事件」與「關閉事件」描述,且合法性只依賴目前開啟數量或前綴平衡時,可以嘗試從左到右掃描事件位置,把原本複雜的配對問題壓成端點數量 DP。
複雜度分析
時間複雜度:O ( m n 2 ) \mathcal{O}(mn^2) O ( m n 2 ) 。外層掃描 m m m 個座標,每個座標枚舉左右端點數量。由於 n ⋅ m ≤ 100000 n \cdot m \le 100000 n ⋅ m ≤ 1 0 0 0 0 0 且 n ≤ m n \le m n ≤ m 才可能有解,實際可通過。
空間複雜度:O ( n 2 ) \mathcal{O}(n^2) O ( n 2 ) ,使用滾動陣列保存目前座標的 DP 狀態。
為什麼
O ( m n 2 ) \mathcal{O}(mn^2) O ( m n 2 ) 能過?
乍看三層枚舉很大,但因為 n > m n>m n > m 答案直接為 0 0 0 ,所以只需要考慮 n ≤ m n\le m n ≤ m 的情況。又因為題目保證 n ⋅ m ≤ 100000 n\cdot m\le 100000 n ⋅ m ≤ 1 0 0 0 0 0 ,可推出 n 2 ≤ n ⋅ m ≤ 100000 n^2\le n\cdot m\le 100000 n 2 ≤ n ⋅ m ≤ 1 0 0 0 0 0 ,也就是 n ≲ 316 n\lesssim 316 n ≲ 3 1 6 。因此狀態表最多約 n 2 n^2 n 2 ,外層總掃描量受 m n 2 = ( n ⋅ m ) n mn^2=(n\cdot m)n m n 2 = ( n ⋅ m ) n 控制,在最壞可行範圍內約為 3.16 × 1 0 7 3.16\times 10^7 3 . 1 6 × 1 0 7 級別,可以通過。
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 from functools import reduceMOD = int (1e9 + 7 ) def solve (): n, m, x = map (int , input ().split()) if n > m: print (0 ) return f = [[0 ] * (n + 1 ) for _ in range (n + 1 )] f[0 ][0 ] = 1 for i in range (1 , m + 1 ): nf = [[0 ] * (n + 1 ) for _ in range (n + 1 )] for j in range (min (i, n) + 1 ): for k in range (j + 1 ): v = f[j][k] % MOD if not v: continue if i != x: nf[j][k] += v if j + 1 <= n: nf[j + 1 ][k] += v if i != x and k + 1 <= j: nf[j][k + 1 ] += v if j + 1 <= n: nf[j + 1 ][k + 1 ] += v f = nf print (f[n][n] * reduce(lambda x, y: x * y % MOD, range (1 , n + 1 )) % MOD) if __name__ == '__main__' : solve()