題目的難度顏色使用 Luogu 上的分級,由簡單到困難分別為 🔴🟠🟡🟢🔵🟣⚫。

🔗 ABC388F Dangerous Sugoroku

Problem Statement

題目簡述

NN 個方格排成一直線,起點為 11,終點為 NN
另外有 MM 個壞區間 [Li,Ri][L_i, R_i],表示方格 LiL_iRiR_i 都是壞格,不能落腳。
每次可以向右移動 AABB 格,但落點不能位於任何給定的壞區間內。
判斷是否存在一條合法移動路徑,能從方格 11 抵達方格 NN

Constraints

約束條件

  • 2N10122 \leq N \leq 10^{12}
  • 0M2×1040 \leq M \leq 2 \times 10^4
  • 1AB201 \leq A \leq B \leq 20
  • 1<LiRi<N1 < L_i \leq R_i < N
  • Ri<Li+1R_i < L_{i+1},也就是壞區間互不重疊且已排序
  • 所有輸入皆為整數

思路:矩陣快速冪優化 DP

如果只看能不能抵達,其實可以用布林 DP;但為了更自然地推導矩陣形式,可以先把它想成「方案數」問題。

fif_i 表示抵達位置 ii 的方案數。若位置 iigood,則可以從距離 AABB 的前方位置轉移而來:

fi=j=ABfijf_i = \sum_{j=A}^{B} f_{i-j}

若位置 iibad,則不能落腳,因此不存在任何轉移來源,也就是 fi=0f_i = 0

關鍵觀察

每次轉移只會用到最近 BB 個狀態,而 B20B \leq 20 很小。因此可以把最近 BB 個狀態包成一個向量,將「往右推進一格」寫成固定矩陣轉移。

以下以 A=3,B=5A = 3, B = 5 為例。若目前位置是 good,新的最上方狀態會由前方距離 3355 的位置加總而來,其餘狀態則整體往後平移:

[fifi1fi2fi3fi4]=[0011110000010000010000010][fi1fi2fi3fi4fi5]\begin{bmatrix} f_i \\ f_{i-1} \\ f_{i-2} \\ f_{i-3} \\ f_{i-4} \end{bmatrix} = \begin{bmatrix} 0 & 0 & 1 & 1 & 1 \\ 1 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 \end{bmatrix} \begin{bmatrix} f_{i-1} \\ f_{i-2} \\ f_{i-3} \\ f_{i-4} \\ f_{i-5} \end{bmatrix}

若目前位置是 bad,新的最上方狀態必定是 00,但歷史狀態仍然要往後平移:

[fifi1fi2fi3fi4]=[0000010000010000010000010][fi1fi2fi3fi4fi5]\begin{bmatrix} f_i \\ f_{i-1} \\ f_{i-2} \\ f_{i-3} \\ f_{i-4} \end{bmatrix} = \begin{bmatrix} 0 & 0 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 \end{bmatrix} \begin{bmatrix} f_{i-1} \\ f_{i-2} \\ f_{i-3} \\ f_{i-4} \\ f_{i-5} \end{bmatrix}

初始狀態是 f1=1f_1 = 1,並定義 f0=f1==fB+2=0f_0 = f_{-1} = \cdots = f_{-B+2} = 0。也就是說,一開始只有起點可達:

x=[f1f0f1f2f3]=[10000]\mathbf{x} = \begin{bmatrix} f_1 \\ f_0 \\ f_{-1} \\ f_{-2} \\ f_{-3} \end{bmatrix} = \begin{bmatrix} 1 \\ 0 \\ 0 \\ 0 \\ 0 \end{bmatrix}

區間分段與矩陣快速冪

題目給出的壞區間互不重疊且已排序,因此整條路可以被切成若干段 goodbad。每次從某個已處理位置推進到另一個位置時,若中間這段性質相同,就等同於重複套用同一個轉移矩陣。

左開右閉的邊界

若目前已處理到位置 stst,並要推進到位置 eded,需要轉移 edsted - st 次,對應的是區間 (st,ed](st, ed]。因此第一段好格是 (1,L11](1, L_1 - 1],第一段壞格是 (L11,R1](L_1 - 1, R_1],依此類推,最後還要處理 (RM,N](R_M, N]

利用矩陣快速冪,可以把同一種矩陣的 kk 次轉移拆成若干個 2j2^j 次轉移的乘積。

但如果對每一段都直接做一次矩陣快速冪,會超時。原因是每段都會重新計算 M2jM^{2^j},而兩個 B×BB \times B 矩陣相乘需要 O(B3)\mathcal{O}(B^3),因此總時間會是:

O(MB3logN)\mathcal{O}(M B^3 \log N)

根據本題資料範圍,粗略估算為:

2×104×203×log210126.4×1092 \times 10^4 \times 20^3 \times \log_2 10^{12} \approx 6.4 \times 10^9

這個量級明顯無法通過。

第一層優化:預處理矩陣冪

本題只會出現兩種轉移矩陣:好格矩陣與壞格矩陣。因此可以先預處理兩者的 20,21,22,2^0, 2^1, 2^2, \ldots 次冪,避免每段重新計算相同矩陣冪。

預處理只需要對兩種矩陣各做 O(logN)\mathcal{O}(\log N) 次平方,每次平方是一次 B×BB \times B 矩陣乘法,因此成本為:

O(B3logN)\mathcal{O}(B^3 \log N)

第二層優化:由後往前乘到狀態向量

實際上我們最後只需要目前的狀態向量。若先把所有矩陣冪乘成完整矩陣,再乘上狀態向量,會做很多次 B×BB \times B 矩陣乘法;但若遇到某個需要的矩陣冪時,直接乘到目前的 B×1B \times 1 狀態向量上,每次只需要 O(B2)\mathcal{O}(B^2)

在經過兩種優化後,這樣每段轉移就能降到 O(B2logN)\mathcal{O}(B^2 \log N)

Warning

若只做預處理,但每段仍先把需要的矩陣冪互相乘成完整轉移矩陣,單段仍要做 O(logN)\mathcal{O}(\log N)B×BB \times B 矩陣乘法,總時間仍是 O(MB3logN)\mathcal{O}(M B^3 \log N)。真正把每段降到 O(B2logN)\mathcal{O}(B^2 \log N) 的關鍵,是「每個被選到的矩陣冪直接乘到狀態向量」。

從方案數改成可行性

前面的推導用方案數比較直觀,但本題只需要判斷是否存在一條路徑。若真的計算方案數,數字會非常大;即使取模,也可能出現「實際有方案,但取模後剛好是 00」的問題。

所以程式把矩陣乘法中的「加法」改成或運算,把「乘法」改成與運算。如此一來,狀態值只表示可不可達:只要存在任一個合法來源,新位置就是可達。

複雜度分析

  • 時間複雜度:O(B3logN+MB2logN)\mathcal{O}(B^3 \log N + M B^2 \log N)
    • 預處理好格矩陣與壞格矩陣的二次冪:兩種矩陣各做 O(logN)\mathcal{O}(\log N)B×BB \times B 矩陣乘法,所以是 O(B3logN)\mathcal{O}(B^3 \log N)
    • 區間轉移:壞區間會切出前方好格與自身壞格,最後還有一段尾端好格,段數是 O(M)\mathcal{O}(M)
    • 每段長度拆成二進位後,最多取 O(logN)\mathcal{O}(\log N) 個矩陣冪;每次是 B×BB \times B 矩陣乘上 B×1B \times 1 狀態向量,需要 O(B2)\mathcal{O}(B^2),所以每段是 O(B2logN)\mathcal{O}(B^2 \log N)
    • 合併後為 O(B3logN+MB2logN)\mathcal{O}(B^3 \log N + M B^2 \log N)
  • 空間複雜度:O(B2logN)\mathcal{O}(B^2 \log N)
    • 需要保存兩種轉移矩陣的二次冪,每個矩陣大小為 B2B^2,共有 O(logN)\mathcal{O}(\log N) 層。
    • 目前狀態向量只需要 O(B)\mathcal{O}(B),被矩陣冪表吸收。

進一步的優化:狀態壓縮

除此之外,還能利用「只求可行性」這個性質,把矩陣的每一橫列壓成一個整數。

矩陣相乘時,若某一橫列的第 kk 個位置為 11,就把另一個矩陣的第 kk 橫列合併進答案橫列。這等價於布林矩陣乘法中的「對所有可連到的橫列取聯集」,而一整橫列的合併可以用位元或完成。

位元壓縮帶來的差異

一般布林矩陣乘法需要枚舉三個維度;壓成整數後,橫列的合併由位元運算完成,因此矩陣相乘可以壓到 O(B2)\mathcal{O}(B^2)

在這個版本中,快速冪會先累積完整轉移矩陣,最後再乘上目前狀態向量。因為矩陣乘法本身已被位元壓縮優化,這樣寫仍然足夠快,也更接近一般快速冪的形式。

也就是說,第一份程式是避免在每段做 B×BB \times B 矩陣乘法;第二份程式則是讓 B×BB \times B 矩陣乘法本身變快。兩者最後的單段轉移都會落在 O(B2logN)\mathcal{O}(B^2 \log N)

狀態向量的表示

位元壓縮版本中,矩陣與向量共用同一套表示方式。向量雖然只有一直行,但仍可視為每橫列只有最低位可能為 11 的矩陣,因此能直接套用相同的乘法函式。

複雜度分析

  • 時間複雜度:O(MB2logN+B2logN)\mathcal{O}(M B^2 \log N + B^2 \log N)
    • 位元壓縮後,一次 B×BB \times B 矩陣相乘只需要對每橫列枚舉可連到的橫列,合併時用位元或完成,因此是 O(B2)\mathcal{O}(B^2)
    • 預處理好格矩陣與壞格矩陣的二次冪需要 O(B2logN)\mathcal{O}(B^2 \log N)
    • 每段快速冪最多做 O(logN)\mathcal{O}(\log N) 次位元壓縮矩陣乘法,最後再乘一次狀態向量,仍是 O(B2logN)\mathcal{O}(B^2 \log N)
    • 總段數為 O(M)\mathcal{O}(M),因此總時間為 O(MB2logN+B2logN)\mathcal{O}(M B^2 \log N + B^2 \log N)
  • 空間複雜度:O(BlogN)\mathcal{O}(B \log N)
    • 每個矩陣只需保存 BB 個整數,代表 BB 個橫列。
    • 兩種矩陣各保存 O(logN)\mathcal{O}(\log N) 層二次冪,因此為 O(BlogN)\mathcal{O}(B \log N)
    • 狀態向量需要 O(B)\mathcal{O}(B),同樣被矩陣冪表吸收。

Code

寫法一:矩陣快速冪優化DP

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
N, M, A, B = map(int, input().split())
intervals = [tuple(map(int, input().split())) for _ in range(M)]

class Matrix:
def __init__(self, mat):
self.mat = mat

def __mul__(self, other):
p, q, r = len(self.mat), len(other.mat), len(other.mat[0])
res = [[0] * r for _ in range(p)]
for k in range(q):
for i in range(p):
for j in range(r):
res[i][j] |= self.mat[i][k] & other.mat[k][j]
return Matrix(res)

def __getitem__(self, key):
i, j = key
return self.mat[i][j]

def __setitem__(self, key, value):
i, j = key
self.mat[i][j] = value

MG = Matrix([[0] * B for _ in range(B)]) # Good
for i in range(A - 1, B):
MG[0, i] = 1
for i in range(1, B):
MG[i, i - 1] = 1

MB = Matrix([[0] * B for _ in range(B)]) # Bad
for i in range(1, B):
MB[i, i - 1] = 1

pow_MG, pow_MB = [0] * 41, [0] * 41
pow_MG[0], pow_MB[0] = MG, MB
for i in range(1, 41):
pow_MG[i] = pow_MG[i - 1] * pow_MG[i - 1]
pow_MB[i] = pow_MB[i - 1] * pow_MB[i - 1]

def pow(x, k, pow_mat):
"""
根據矩陣快速冪,可以把 MB^k 表示成 MB^(2^i) 的乘積。
因此在預先處理完 MB^(2^i) 的矩陣後,需要計算若干個 (B x B) 的矩陣相乘後再乘以一個 (B x 1) 的矩陣。

但注意這裡有個小技巧:
兩個 (B x B) 的矩陣相乘需要 O(B^3) 的時間;
而 (B x B) 的矩陣乘以 (B x 1) 的矩陣只需要 O(B^2) 的時間。

因此我們應該由後往前計算,這樣可以減少計算的次數。
如此便可以在 O(B^2 log k) 的時間內計算出 MB^k * x 。
"""
res = x
for i in range(41):
if k & (1 << i):
res = pow_mat[i] * res
return res

cur = 1
m = [[0] for _ in range(B)]
m[0] = [1]
x = Matrix(m)
for (L, R) in intervals:
# (cur, L-1] is good
# (L-1, R] is bad
x = pow(x, L - 1 - cur, pow_MG)
x = pow(x, R - L + 1, pow_MB)
cur = R

# (cur, N] is good
x = pow(x, N - cur, pow_MG)
print("Yes" if x[0, 0] else "No")

寫法二:狀態壓縮優化版本

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
N, M, A, B = map(int, input().split())
intervals = [tuple(map(int, input().split())) for _ in range(M)]

class Matrix:
def __init__(self, r, c, mat=None):
self.r = r
self.c = c
self.mat = mat or [0] * r

def __mul__(self, other):
res = [0] * len(self.mat)
for i in range(len(self.mat)):
row = self.mat[i]
for k in range(len(other.mat)):
if row & (1 << k):
res[i] |= other.mat[k]
return Matrix(self.r, other.c, res)

def __getitem__(self, key):
i, j = key
return (self.mat[i] >> j) & 1

def __setitem__(self, key, value):
i, j = key
if value:
self.mat[i] |= (1 << j)
else:
self.mat[i] &= ~(1 << j)

MG = Matrix(B, B) # Good
for i in range(A - 1, B):
MG[0, i] = 1
for i in range(1, B):
MG[i, i - 1] = 1

MB = Matrix(B, B) # Bad
for i in range(1, B):
MB[i, i - 1] = 1

pow_MG, pow_MB = [0] * 41, [0] * 41
pow_MG[0], pow_MB[0] = MG, MB
for i in range(1, 41):
pow_MG[i] = pow_MG[i - 1] * pow_MG[i - 1]
pow_MB[i] = pow_MB[i - 1] * pow_MB[i - 1]

identity = Matrix(B, B)
for i in range(B):
identity[i, i] = 1

def pow(x, k, pow_mat):
"""
根據矩陣快速冪,可以把 MB^k 表示成 MB^(2^i) 的乘積。
因此在預先處理完 MB^(2^i) 的矩陣後,需要計算若干個 (B x B) 的矩陣相乘後再乘以一個 (B x 1) 的矩陣。

但注意這裡有個小技巧:
兩個 (B x B) 的矩陣相乘需要 O(B^3) 的時間;
而 (B x B) 的矩陣乘以 (B x 1) 的矩陣只需要 O(B^2) 的時間。

因此我們應該由後往前計算,這樣可以減少計算的次數。
如此便可以在 O(B^2 log k) 的時間內計算出 MB^k * x 。

不過如果用狀態壓縮 + 位運算,則計算兩個 (B x B) 的矩陣相乘只需要 O(B^2) 的時間。
"""
# res = x
# for i in range(41):
# if k & (1 << i):
# res = pow_mat[i] * res
# return res
res = identity
for i in range(41):
if k & (1 << i):
res = pow_mat[i] * res
return res * x

cur = 1
# m = [[0] for _ in range(B)]
m = [0] * B
m[0] = 1
x = Matrix(B, 1, m)
for (L, R) in intervals:
# (cur, L-1] is good
# (L-1, R] is bad
x = pow(x, L - 1 - cur, pow_MG)
x = pow(x, R - L + 1, pow_MB)
cur = R

# (cur, N] is good
x = pow(x, N - cur, pow_MG)
print("Yes" if x[0, 0] else "No")