初級圖論。
CHANGE LOG
- 2022.5.26:修改文章。
- 2022.6.8:添加 SAT 的定義。
- 2022.6.10:添加 DAG 的支配樹。
- 2022.9.30:添加 DAG 鏈剖分。
1. 同余最短路
說難也不算難,但是挺有意思的一個知識點。應用不廣泛。
前置知識:SPFA / Dijkstra 求最短路。
1.1 算法簡介
同余最短路用於求解在某個范圍內有多少重量可以由若干物品做完全背包湊出,或者說,有多少數值可由給定的一些數進行 系數非負 的線性組合得到。
我們嘗試具體描述這樣的問題。給出 \(n\ (n\leq 50)\) 個數 \(a_i\ (1\leq a_i\leq 10 ^ 5)\),求 \([L, R](1\leq L\leq R\leq 10 ^ {18})\) 以內滿足能表示成 \(\sum\limits_{c_i\geq 0} a_ic_i\) 的正整數個數。
同余最短路的核心思想在於觀察到:如果一個數 \(r\) 可以被表出,那么任何 \(r + xa_i(x > 0)\) 也可以被表出。因此只需選出任意一個 \(a_i\),求出每個模 \(a_i\) 同余的同余類 \(K_j\) 當中最小的能被表出的數 \(f_j\),即可快速判斷一個數 \(S\) 能否被表出:當且僅當 \(S \geq f(S\bmod a_i)\)。
這個思想非常巧妙,感覺不是凡人能夠想到的思路。一種可能的理解方式是,考慮每個數能否被表出的狀態 \(g(r)\),我們發現對於某個 \(a_i\),如果 \(r_1\equiv r_2\pmod {a_i}\) 且 \(r_1 < r_2\),那么必然有 \(g(r_1) \leq g(r_2)\)。這樣一來,根據單調性下的定義域值域互換的技巧,設 \(f_j\) 表示使得 \(g(r) = 1\) 且 \(r\bmod {a_i} = j\) 的最小的 \(r\)。
\(a\) 本身很小,所以對每個同余類求出最小數是可行的。為了減小常數,我們一般會選擇使得同余類個數最少的 \(a_i\),即 \(a\) 的最小值。不妨設其為 \(a_1\)。
得到模 \(a_1\) 余 \(j\) 的數中最小的能被表示出來的數 \(f_j\) 后,可以通過加上 \(a_2 \sim a_n\) 轉移到其它同余類 \(f_k(k\equiv j + a_i \pmod {a_1})\)。
注意到上述過程非常像一個最短路:對於每個點 \(j\),它向 \((j + a_i) \bmod a_1\) 連了一條長度為 \(a_i\) 的邊,需要求從源點 \(0\) 開始到每個點的最短路。使用 dijkstra 或 SPFA 求解。
初始值 \(f_0=0\),\(f_i = +\infty(1\leq i < a_1)\)。
求出 \(f_j\) 后得到答案是容易的。首先進行一步差分轉化為求 \([0, R]\) 有多少個數能被表出,此時答案為
用 \([0, R]\) 的答案減去 \([0, L - 1]\) 的答案即可。
時間復雜度為 dijkstra 的 \(\mathcal{O}(na_1\log a_1)\) 或 SPFA 的 \(\mathcal{O}(n a_1 k)\)。SPFA 跑不滿,因為圖的形態和邊權相對固定,不會出現把 SPFA 卡死的情況,所以一般比 dijkstra 快。代碼見例 I.
1.2 例題
*I. P2371 [國家集訓隊] 墨墨的等式
同余最短路模板題。
設 \(m = n \times a_i\)。使用 \(\mathcal{O}(mk)\) 的 SPFA 會比 \(\mathcal{O}(m\log m)\) 的 dijkstra 快不少。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5;
int n, a[N], in[N];
long long f[N], l, r, ans;
int main() {
cin >> n >> l >> r;
for(int i = 1; i <= n; i++) cin >> a[i];
memset(f, 0x3f, sizeof(f));
sort(a + 1, a + n + 1);
queue<int> q;
q.push(0), f[0] = 0;
while(!q.empty()) {
int t = q.front();
q.pop(), in[t] = 0;
for(int i = 2; i <= n; i++) {
int it = (t + a[i]) % a[1];
long long d = f[t] + a[i];
if(d < f[it]) {
f[it] = d;
if(!in[it]) in[it] = 1, q.push(it);
}
}
}
for(int i = 0; i < a[1]; i++) {
if(r >= f[i]) ans += max(0ll, (r - f[i]) / a[1] + 1);
if(l > f[i]) ans -= max(0ll, (l - 1 - f[i]) / a[1] + 1);
}
cout << ans << endl;
return cerr << "Time: " << clock() << endl, 0;
}
II. P3403 跳樓機
同余最短路的板子題。
*III. AT3621 [ARC084B] Small Multiple
一道神仙題。
注意到所有數都可以從 \(1\) 開始經過若干次 \(+1\) 或 \(\times 10\) 得到,因此每個點 \(i\) 向 \((i + 1) \bmod K\) 連一條權值為 \(1\) 的邊,向 \(10i \bmod n\) 連一條權值為 \(0\) 的邊,跑 01 BFS 即可。
注意,不可以以 \(0\) 作為源點,因為題目要求倍數是正整數,且我們希望求的就是 \(f_0\),所以 \(0\) 不能算。對於本題而言,\(f_i\) 定義為模 \(K\) 等於 \(i\) 的 正整數 的各位數字之和的最小值。
時間復雜度 \(\mathcal{O}(K)\),代碼。寫了 SPFA,跑得也很快。
IV. P2662 牛場圍欄
同余最短路板題。
SPFA 的時間復雜度為 \(\mathcal{O}(L ^ 2k)\),比 dijkstra 的 \(\mathcal{O}(L ^ 2\log L)\) 跑得快一些。
V. 模擬賽題 夢回 2021
給定 \(n\) 個值域 \([2, m]\) 的 隨機數,求最大的不能被這些數表示出的數。\(50\leq n\leq 10 ^ 7\),\(2\leq m\leq 10 ^ 8\)。
和 IV. 一樣,無限背包問題考慮同余最短路。因為數據隨機,所以最小值的期望為 \(\dfrac m {n + 1}\)。
直接跑 SPFA 的期望復雜度為 \(\mathcal{O}(\frac m {n + 1}\times n k)\approx \mathcal{O}(mk)\),可以通過。
[P4156 WC2016]論戰捆竹竿 - 洛谷 | 計算機科學教育新生態 (luogu.com.cn)
2. 2-SAT
2-SAT 具有很強的邏輯性,因為 SAT 本身由邏輯學術語定義。
2.1 SAT 的定義
該部分與 OI 沒有太大關系,不感興趣的讀者可以跳過。
為方便說明,首先給出相關術語。
- 合取:用符號 \(\land\) 表示,是自然語言聯結詞 “且” 的抽象。命題 \(p\land q\) 表示 \(p, q\) 的合取,稱為合取式,讀作 \(p\) 且 \(q\),其為真當且僅當 \(p, q\) 均為真。簡單地說,合取就是邏輯與
&&
,可以類比計算機科學中的按位與&
,相信這個概念大家並不陌生。 - 析取:用符號 \(\lor\) 表示,是自然語言聯結詞 “或” 的抽象。命題 \(p\lor q\) 表示 \(p, q\) 的析取,稱為析取式,讀作 \(p\) 或 \(q\),其為真當且僅當 \(p, q\) 至少有一個為真。同樣的,合取是邏輯或
||
,類比按位或|
。 - 否定:用符號 \(\lnot\) 表示,\(\lnot p\) 表示命題 \(p\) 的否定,其真假性與 \(p\) 相反。否定是邏輯非
!
,類比按位取反~
。
上述三條概念均為基本命題聯結詞,大概可以看作給常見的 “與或非” 三種運算起了高大上的名字。將命題變元(可真可假的布爾變量)用合取 \(\land\),析取 \(\lor\) 和否定 \(\lnot\) 聯結在一起,得到布爾邏輯式(叫法不唯一)。
布爾邏輯式可滿足,指存在一個對所有命題變元的真假賦值,使得該布爾邏輯式為真。
布爾可滿足性問題(Boolean Satisfiability Problem)簡稱 SAT,它定義為檢查一個布爾邏輯式是否可滿足,是第一個被證明的 NPC 問題。
- 命題變元或其否定稱為文字。
- 若干個文字的析取稱為簡單析取式,形如 \(P = x_1 \lor x_2 \lor \cdots \lor x_k\),其中 \(x_i\) 表示命題 \(p_i\) 或其否定 \(\lnot p_i\)。
- 若干簡單析取式的合取稱為合取范式(Conjunctive Normal Form,CNF),形如 \(P_1 \land P_2 \land \cdots \land P_n\),其中 \(P_i\) 表示一個簡單析取式。
考慮 SAT 的簡單版本:命題公式為合取范式,且組成它的每個簡單析取式至多含有 \(k\) 個文字。這一問題稱為 k-SAT。
當 \(k\geq 3\) 時 k-SAT 是 NPC 問題,但 2-SAT 存在多項式復雜度的解法。接下來介紹這一算法。
2.2 算法介紹
首先我們將抽象的 2-SAT 描述得更具體一些。將簡單析取式看成條件,我們希望每個條件均被滿足,而每個簡單析取式的形態是固定的:\(p_i\),\(\lnot p_i\),\(p_i\lor p_j\),\(p_i \lor \lnot p_j\),\(\lnot p_i \lor\lnot p_j(i\neq j)\)。
注意到每個條件僅和至多兩個文字(變量)有關,這啟發我們思考圖論解法。
嘗試將限制寫成 “若,則” 的形式,因為可達性具有傳遞性,而蘊含性同樣具有傳遞性(若 \(A\) 則 \(B\),若 \(B\) 則 \(C\),說明若 \(A\) 則 \(C\))。根據這些限制,我們可以建出一張有向圖,它自然地表示出了當每個文字為真時,有哪些文字必須為真。
- \(p_i\):\(p_i\) 必須為真。可以用若 \(\lnot p_i\) 則 \(p_i\) 來限制 \(p_i\) 為真。
- \(\lnot p_i\):\(p_i\) 必須為假。同理,若 \(p_i\) 則 \(\lnot p_i\)。
- \(p_i \lor p_j(i\neq j)\):\(p_i\) 和 \(p_j\) 不能同時為假,即若 \(\lnot p_i\) 則 \(p_j\),若 \(\lnot p_j\) 則 \(p_i\)。
- \(p_i \lor \lnot p_j\):若 \(\lnot p_i\) 則 \(\lnot p_j\),若 \(p_j\) 則 \(p_i\)。
- \(\lnot p_i\lor \lnot p_j\):若 \(p_i\) 則 \(\lnot p_j\),若 \(p_j\) 則 \(\lnot p_i\)。
注意第三,四,五中簡單析取式所產生的連邊具有 對稱性。因為若一個命題成立,則其逆否命題仍然成立:若 \(A\Rightarrow B\),則 \(\lnot B\Rightarrow \lnot A\)。
這樣,若一個命題變元 \(p_i\) 為真可推導出它的否定為真,即 \(p_i\rightsquigarrow \lnot p_i\),則該命題變元必須為假。反之亦然。進一步地,若 \(p_i\) 和 \(\lnot p_i\) 強連通,則 2-SAT 無解。
因此,對有向圖進行強連通分量縮點,若 \(p_i\) 和 \(\lnot p_i\) 在同一強連通分量說明 \(p_i\) 非真非假,矛盾,問題無解。
除此以外,是否一定有解呢?
首先確定解的形態。對於與命題變元 \(p_i\) 相關的兩個文字 \(p_i\) 和 \(\lnot p_i\),有且僅有一個文字為真,恰對應命題變元 \(p_i\) 為真或假。所有選出為真的共 \(n\) 個文字(若一個命題變元為假,則它的否定對應的文字為真)對應的點集 \(P\) 在有向圖上的閉合子圖(即所有這些點能到達的點構成的點導出子圖)不同時包含一個命題變元及其否定。這說明閉合子圖內部恰有 \(n\) 個點,繼而得到 \(P\) 的點導出子圖等於其閉合子圖,等價表述是不存在一個為假的文字被一個為真的文字到達。
接下來嘗試構造一組解。注意到若 \(\lnot p_i\rightsquigarrow p_i\),則 \(p_i\) 的拓撲序一定大於 \(\lnot p_i\),所以對每個命題變元及其否定,最直接的想法是選擇拓撲序更大的那一個。
先猜后證,考慮證明這樣做的正確性。反證法,假設存在 \(p_i\) 和 \(p_j\),滿足 \(p_i\) 的拓撲序大於 \(\lnot p_i\),\(p_j\) 的拓撲序大於 \(\lnot p_j\),但是 \(p_i\) 可達 \(\lnot p_j\)。根據命題與其逆否命題的等價性,\(p_j\) 可達 \(\lnot p_i\)。因為 \(p_i\) 的拓撲序大於 \(\lnot p_i\),所以 \(p_j\) 的拓撲序小於 \(\lnot p_i\) 小於 \(p_i\)。因為 \(p_j\) 的拓撲序大於 \(\lnot p_j\),所以 \(p_i\) 的拓撲序小於 \(\lnot p_j\) 小於 \(p_j\),矛盾。
因此此時一定有解。
綜上,若只需求出任意一組解,那么對所點后的圖拓撲排序,然后對於每個命題變元相關的兩個文字,使得拓撲序更大的那個為真。注意到我們在 tarjan 時已經得到了縮點后 DAG 的反向拓撲序:若 \(u\rightsquigarrow v\),則 \(v\) 在 \(u\) 之前被彈出,即 col[v] <= col[u]
,因此只需選擇 col
較小的文字賦為真。時間復雜度 \(\mathcal{O}(n + m)\),其中 \(n\) 是命題變元數量,\(m\) 是簡單析取式數量。
如果要求字典序最小解,可以按位貪心,每次盡量選字典序小的點,遍歷所有其能夠到達的點並檢查是否使當前決策出現矛盾,貪心的局部決策不影響全局合法性留給讀者自證。時間復雜度 \(\mathcal{O}(n(n + m))\)。Bitset 優化求傳遞閉包可做到 \(\mathcal{O}\left(\dfrac{n ^ 3} w\right)\)。
2.3 例題
P4782 【模板】2-SAT 問題
#include <bits/stdc++.h>
using namespace std;
constexpr int N = 2e6 + 5; // 兩倍空間
int cnt, hd[N], nxt[N], to[N];
void add(int u, int v) {nxt[++cnt] = hd[u], hd[u] = cnt, to[cnt] = v;}
int n, m, dn, dfn[N], low[N], top, stc[N], vis[N], cn, col[N];
void tarjan(int id) {
dfn[id] = low[id] = ++dn, vis[id] = 1, stc[++top] = id;
for(int i = hd[id]; i; i = nxt[i]) {
int it = to[i];
if(!dfn[it]) tarjan(it), low[id] = min(low[id], low[it]);
else if(vis[it]) low[id] = min(low[id], dfn[it]);
}
if(low[id] == dfn[id]) {
col[id] = ++cn;
while(stc[top] != id) col[stc[top]] = cn, vis[stc[top--]] = 0;
vis[id] = 0, top--;
}
}
int main() {
cin >> n >> m;
for(int i = 1; i <= m; i++) {
int u, a, v, b;
scanf("%d%d%d%d", &u, &a, &v, &b);
add(u + (!a) * n, v + b * n); // 當 u 等於 !a 時,v 必須等於 b
add(v + (!b) * n, u + a * n); // 同理
}
for(int i = 1; i <= n << 1; i++) if(!dfn[i]) tarjan(i);
for(int i = 1; i <= n; i++) if(col[i] == col[i + n]) puts("IMPOSSIBLE"), exit(0);
puts("POSSIBLE");
for(int i = 1; i <= n; i++) putchar('0' + (col[i + n] < col[i])), putchar(' '); // 選 col 更小的
return 0;
}
P3825 [NOI2017] 游戲
若沒有 x
就是裸的 2-SAT。
注意到 \(d\) 非常小,所以 \(2 ^ d\) 枚舉每個 x
的狀態:a
或 c
,這保證了任何一種合法解都被考慮到。
時間復雜度 \(\mathcal{O}(2 ^ d(n + m))\)。代碼。
*P6965 [NEERC2016] Binary Code
一個字符串至多含有一個問號,所以狀態至多有兩種,考慮 2-SAT,設 \(x_i\) 表示第 \(i\) 個字符串的問號填 \(0\),\(\lnot x_i\) 表示第 \(i\) 個字符串的問號填 \(1\)。現在我們有 \(2n\) 個字符串和 \(2n\) 個文字,它們之間一一對應。
容易發現,若字符串 \(s\) 是 \(t\) 的前綴,則若 \(s\) 對應文字為真,則 \(t\) 對應文字為假;若 \(t\) 對應文字為真,則 \(s\) 對應文字為假。這說明若 \(s\) 則非 \(t\),若 \(t\) 則非 \(s\)。
刻畫前綴關系的結構是字典樹。對於若 \(s\) 則非 \(t\) 的限制,我們需要從 \(s\) 向它的子樹內所有字符串的 否定 連邊。對於若 \(t\) 則非 \(s\) 的限制,我們需要從 \(t\) 向它的祖先對應的所有字符串的 否定 連邊。因此,建出根向字典樹和葉向字典樹。在兩棵字典樹上,每個字符串對應的狀態向它對應文字的否定連邊。為防止出現 \(s\) 和 \(t\) 對應同一字符串的情況,在葉向字典樹上,\(s\) 對應文字只能向它對應狀態的兩個兒子連邊,否則 \(s\) 會向 \(s\) 的否定連邊,導致必然無解。同理,在根向字典樹上,\(s\) 對應文字向它對應節點的父親(而非它本身)連邊。
我們還要處理 \(s = t\) 但對應不同字符串的情況。容易發現,將相等的字符串排成一行,每個字符串會向所有除了它本身的其它字符串的 否定 連邊,可以通過前綴后綴優化建圖做到。
綜上,點數和邊數關於 \(n\) 和字典樹大小 \(m\) 線性。點數不超過 \(4n + 2m\),而 \(m \leq 2n\),所以點數不超過 \(8n\)。代碼。
3. 廣義圓方樹
前置知識:tarjan 求割點和邊雙的縮點的正確性證明(懶得再證一遍正確性了),詳見 初級圖論。
廣義圓方樹是刻畫圖上點的必經性的強力工具。它可以描述原圖任意兩點之間的所有割點,即 \(u\to v\) 的所有必經點。
點雙和邊雙的縮點方法類似,都是借助 tarjan 算法求出所有連通分量的具體形態。但是它們縮點后得到的結構形態卻截然不同。這是因為一個節點最多只會出現在一個邊雙當中(相對應的,一條邊最多屬於一個點雙),所以邊雙縮點時可以將一個邊雙內部所有點看成一個點。但是一個點可能出現在若干點雙當中,而且為了刻畫必經點,我們希望在縮點時保留這些割點(邊雙縮點時也保留了割邊對吧)。
3.1 點雙相關性質
咕咕咕,可以看例題 V. 題解,有時間再補充在這里。
3.2 算法介紹
廣義圓方樹是定義在 一般無向圖 上的一種樹型結構。它是點雙縮點后的產物,可以有效解決必經性相關的題目。
點雙的縮點本質上可以看成 “縮邊”,就是把原圖上所有對刻畫必經性無用的邊全部丟掉。
考察一個點雙。如果我們希望將它縮成一棵樹,並且樹上任意兩點之間的簡單路徑恰包含且僅包含它們之間的必經點,那么任意兩點之間都必須直接相連,因為單獨考察一個點雙,其內部不存在必經點。但這和 “縮成一棵樹” 矛盾。
為此,我們嘗試建出點雙的 “代表點” 並向點雙內部所有點連邊。容易發現這樣一種菊花滿足條件,因為任意兩點通過中心點間接地直接相連。
這樣就可以自然地引出圓方樹的定義了:將原圖的點視為圓點。對於原圖中的每一個點雙,新建 代表該點雙 的方點連向點雙內部所有圓點。
每個點雙縮成一張菊花圖,多個菊花圖通過原圖中的割點相連,因為 點雙的分隔點是割點。類比邊雙縮點時,每個邊雙縮成一個點,多個點通過原圖中的割邊相連。
廣義圓方樹的建法只需在 tarjan 算法的基礎上稍作修改即可。節點 \(v\) dfs 完畢回溯到其父節點 \(u\) 時,若 low[v] >= dfn[u]
(回憶割點的判定法則),說明 \(v\) 及其子樹在棧內(也是棧頂)的部分與 \(u\) 共同形成了一個點雙。新建代表點雙的方點並對應連邊,彈出棧頂直到 \(v\) 被彈出即可。注意這里 不能彈出 \(u\),因為 \(u\) 可能和它別的兒子形成另外的點雙(但 \(v\) 不會了,因為 \(v\) 及其子樹已經被處理掉了)。
正確性(彈出點集形成的連通分量的唯一性和極大性)可以類似邊雙縮點證,這里不再贅述。以下是一些注意點。
- 當原圖不連通時,每個連通塊處理完之后棧內會剩下一個點,即進入該連通塊的點。
- 每個點雙均會新建一個方點,所以需要開兩倍空間存儲圓方樹。當圖是一張菊花時,點雙數量為 \(n - 1\)。
廣義圓方樹提供了原圖上所有割點的信息,是解決必經性問題的得力助手,也是很有用的一類樹形結構。代碼見例題 I.
3.3 性質
我們先給出一些與點雙連通性相關的引理:
引理 1:除了 \(x, y\) 本身,\(x, y\) 在原圖上的必經點為割點。
證明:考慮一個不等於 \(x\) 或 \(y\) 的非割點,刪去后整張圖仍然連通。
引理 2:\(z\) 是 \(x, y\) 的必經點當且僅當刪去 \(z\) 后 \(x, y\) 不連通。
證明:若刪去 \(z\) 后 \(x, y\) 連通說明存在不經過 \(z\) 的連接 \(x, y\) 的簡單路徑;假設存在連接 \(x, y\) 的簡單路徑不經過 \(z\),那么刪去 \(z\) 后 \(x, y\) 可以通過該簡單路徑連通。因此,\(z\) 不是 \(x, y\) 的必經點當且僅當刪去 \(z\) 后 \(x, y\) 連通,原命題繼而得證。
引理 3:若 \(x\) 與 \(y, z\) 均點雙連通,但 \(y, z\) 不點雙連通,則 \(x\) 是 \(y, z\) 的必經點。
證明:考慮 \(y\to x\) 的兩條僅在端點處相交的路徑 \(P_1, P_2\) 以及 \(x\to z\) 的兩條僅在端點處相交的路徑 \(P_3, P_4\)。\(P_1\to P_3\) 和 \(P_2\to P_4\) 使得 \(y, z\) 之間只有 \(x\) 可能是必經點,再根據 \(y, z\) 不點雙連通得證。
上述引理容易感性理解,讀者應當認為它們的成立非常自然。
接下來是一些由淺到深,層層遞進的圓方樹相關性質,最重要且常見的性質在最后給出。
性質 1:圓點 \(x\) 的度數等於包含它的點雙個數。
證明:根據圓方樹的構建方式,顯然。
性質 2:圓方樹上圓方點相間。
證明:任何兩個在同一點雙內的點由該點雙對應的方點間接相連。
性質 3:原圖上直接相連的 \(x, y\) 包含於同一點雙。
證明:\(x, y\) 點雙連通。
性質 4:圓點 \(x\) 是葉子當且僅當它在原圖上是非割點。
證明:
若 \(x\) 是割點,則刪去 \(x\) 后存在與 \(x\) 直接相鄰的兩點 \(y, z\) 不連通(顯然),所以 \(x, y, z\) 不包含於同一點雙 \(S\),又因為 \(x, y\) 點雙連通,\(x, z\) 點雙連通,所以 \(x\) 至少包含於兩個點雙。
若 \(x\) 至少包含於兩個點雙,則根據性質 2,存在 \(y, z\) 使得 \(x, y\) 點雙連通,\(x, z\) 點雙連通,但 \(y, z\) 不點雙連通,根據引理 3,\(x\) 是 \(y, z\) 的必經點,即 \(x\) 是割點,得證。
性質 5:在廣義圓方樹上刪去圓點 \(x\) 后剩余節點的連通性與在原圖上刪去 \(x\) 相等。
證明:因為刪去 \(x\) 時只刪去 \(x\) 的鄰邊,所以只需證明與 \(x\) 直接相鄰的任意兩點 \(y, z\) 連通性相等。
若 \(x, y, z\) 在同一點雙,則據點雙定義原圖刪去 \(x\) 后 \(y, z\) 連通,同時圓方樹上刪去 \(x\) 后 \(y, z\) 通過方點連通。
若 \(x, y\) 和 \(x, z\) 在不同點雙 \(S_1, S_2\),據引理 3 原圖刪去 \(x\) 后 \(y, z\) 不連通,同時圓方樹上刪去 \(x\) 后方點 \(S_1\) 和方點 \(S_2\) 不連通(\(x\) 和 \(S_1, S_2\) 直接相連,且圓方樹的形態是樹),得出圓方樹上刪去 \(x\) 后 \(y, z\) 不連通,得證。
性質 6:\(x, y\) 簡單路徑上的所有圓點恰好是原圖 \(x, y\) 之間的所有必經點。
證明:若圓點 \(z\) 在圓方樹上 \(x, y\) 的簡單路徑上,則圓方樹上刪去 \(z\) 后 \(x, y\) 不連通,根據性質 6 原圖刪去 \(z\) 后 \(x, y\) 不連通,根據引理 2 \(z\) 是 \(x, y\) 的必經點。反之可證 \(z\) 不是 \(x, y\) 的必經點,得證。
總結一下,這些性質無非就是在描述一個核心結論:若在圓方樹上 \(z\) 是 \(x, y\) 的必經點,則原圖 \(z\) 是 \(x, y\) 的必經點。換言之,圓方樹完整地保留了原圖的必經性。
3.4 例題
I. P5058 [ZJOI2004] 嗅探器
對原圖建出廣義圓方樹,那么 \(a, b\) 兩點之間的所有圓點(也是割點)的編號最小值即為所求。注意不能包含 \(a, b\) 本身。
如果不存在這樣的圓點則無解,此時 \(a, b\) 處在同一點雙內部。
時間復雜度 \(\mathcal{O}(n + m)\)。
#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 5;
int n, a, b, node;
int dn, dfn[N], low[N], top, stc[N];
vector<int> e[N], g[N];
void tarjan(int id) {
dfn[id] = low[id] = ++dn, stc[++top] = id;
for(int it : e[id]) {
if(!dfn[it]) {
tarjan(it), low[id] = min(low[id], low[it]);
if(low[it] == dfn[id]) {
g[++node].push_back(id), g[id].push_back(node);
for(int x = 0; x != it; )
g[node].push_back(x = stc[top--]), g[x].push_back(node);
}
}
else low[id] = min(low[id], dfn[it]);
}
}
int fa[N], dep[N];
void dfs(int id, int ff) {
fa[id] = ff, dep[id] = dep[ff] + 1;
for(int it : g[id]) if(it != ff) dfs(it, id);
}
int main() {
cin >> n, node = n;
scanf("%d%d", &a, &b);
while(a && b) {
e[a].push_back(b), e[b].push_back(a);
scanf("%d%d", &a, &b);
}
tarjan(1), dfs(1, 0);
scanf("%d%d", &a, &b);
int ans = N;
if(dep[a] < dep[b]) swap(a, b);
while(dep[a] > dep[b]) if((a = fa[a]) != b) ans = min(ans, a);
while(a != b) ans = min(ans, min(a = fa[a], b = fa[b]));
if(ans > n) puts("No solution");
else cout << ans << "\n";
return 0;
}
II. P4630 [APIO2018] Duathlon 鐵人兩項
廣義圓方樹基礎練習題。
首先進行一步轉化,對所有點對 \((u, v)\) 求它們之間簡單路徑的並去掉 \(u, v\) 之后的點集大小之和。
由於本題簡單路徑定義為不經過重復點的路徑,且題目考察連通性相關,不難想到建出廣義圓方樹。因此相當於求 \((u, v)\) 之間所有方點所表示的點雙大小之和。
注意,路徑上每個除了 \(u, v\) 以外的割點會被算到兩次,而 \(u, v\) 本身也會被路徑上它們所在的點雙算入一次。這說明路徑上每個圓點都被多算了一次。因此,每個方點的貢獻是其對應的點雙大小,而每個圓點的貢獻是 \(-1\)。
如上圖,從最左邊的 \(u\) 到最右邊的 \(v\),路徑上共有兩個方點 \(x, y\)。單純將它們的權值加起來會得到 \(6 + 5 = 11\)。但我們發現正確的答案應該是 \(8\),因為只有所有紅點和 \(w\) 才會作為中轉點。錯誤原因是 \(u, v, w\) 均被多算了一次,真正的答案應由 \(-1 + 6 -1 + 5 - 1\) 求得。
設每個點的權值為 \(a\),問題等價於求 \(\sum\limits_{u \neq v \land u, v\leq n} \sum\limits_{p \in \mathrm{path}(u, v)} a_p\)。一般而言,我們用大於 \(n\) 的數值給方點標號(具體見上一題代碼),所以有限制 \(u, v\leq n\)。
轉換貢獻方式,考察圓方樹上每個節點對答案的貢獻。容易通過一遍 dfs 求出子樹大小的同時求解該問題。
注意原圖可能不連通。
時間復雜度線性。代碼。
III. P4606 [SDOI2018] 戰略游戲
扣掉一個節點后 \(u\) 不能到達 \(v\),說明該節點是 \(u, v\) 之間的割點。因此,建出廣義圓方樹,我們發現題目相當於求點集 \(S\) 在樹上的虛樹所包含的不屬於 \(S\) 的圓點數量。
對每個點是否為圓點做樹上前綴和,虛樹上一條邊的貢獻(不含兩端)容易計算。再加上所有不在 \(S\) 內部但是是圓點的虛樹節點的貢獻,時間復雜度線性對數。
代碼。
IV. P4334 [COI2007] Policija
建出廣義圓方樹。
對於類型 2 的詢問,直接判 \(C\) 是否在 \(A, B\) 的簡單路徑上。
對於類型 1 的詢問,首先判 \((G_1, G_2)\) 是否為割邊(即點雙大小為 \(2\)),若是,則判 \(G_1 \to C(G_1, G_2) \to G_2\) 這條路徑是否在 \(A, B\) 的簡單路徑上,其中 \(C(G_1, G_2)\) 是這兩個點形成的方點。因為點雙大小為 2,所以該方點只與 \(G_1, G_2\) 相連,因此只需判 \(C(G_1, G_2)\) 是否在 \(A, B\) 的簡單路徑上。
時間復雜度線性對數,瓶頸在於求 LCA。代碼。
*V. P3225 [HNOI2012] 礦場搭建
不那么套路的連通性相關題目。
題目希望我們給出一種選擇關鍵點的方式,滿足刪去任何一個點后形成的每個連通塊內都存在至少一個關鍵點。
我們發現,由於刪去非割點后整張圖仍連通,所以刪去非割點后連通塊存在關鍵點蘊含於刪去割點后連通塊存在關鍵點。
唯一的特例是整張圖點雙連通。此時從圖上任意選擇兩點作為關鍵點均合法,方案數為 \(\dbinom{|V|}{2}\)。選擇兩個是因為防止其中一個關鍵點被刪去。
進行特判后,每個點雙至少有一個原圖上的割點。
為了解題,我們需要深入剖析廣義圓方樹的結構。剔除廣義圓方樹上所有葉子,即原圖的非割點,我們得到了廣義圓方樹的由原圖割點和點雙方點構成的骨架,稱為主干樹。
定義一個點雙的度數等於它對應的方點在主干樹上的度數,等價於該點雙包含的原圖割點數量。
考慮一個大小為 \(s\) 的點雙。若其度數為 \(1\),那么它是主干樹的葉子,其內部需要有一個關鍵點,方案數 \(s - 1\)。否則它有兩個或以上的割點,我們發現若其包含的任意一個割點被刪去,我們總可以走其它割點到達某個主干樹的葉子,因此其內部不需要割點。
進一步地,我們發現刪去主干樹上任何一個割點,形成每個連通塊必然包含至少一個主干樹的葉子。因此方案合法。
綜上,令主干樹的葉子對應點雙大小分別為 \(s_1, s_2, \cdots, s_k\),則第一問的答案為 \(k\),第二問的答案為 \(\prod\limits_{i = 1} ^ k(s_i - 1)\)。
時間復雜度線性。代碼。
VI. CF487E Tourists
一道圓方樹經典題。
\(c\) 在 \(a\to b\) 的簡單路徑上當且僅當 \(c\) 與 \(a\to b\) 某個方點相鄰。因此,令方點的權值為對應點雙所有節點權值最小值,將圓方樹樹剖即可回答詢問。
問題在於修改點權時可能影響到很多方點權值,無法承受。考慮使用 只維護兒子信息 的技巧,將方點權值改為所有兒子的權值最小值,用 multiset
維護。修改圓點權值時修改其父節點的 multiset
並更新其父節點權值。
查詢同樣求出 \(a\to b\) 點權最小值。當 LCA 為方點時,答案還要與其父節點(若存在)取最小值。
時間復雜度 \(\mathcal{O}((n + q\log n)\log n)\),代碼。
*VII. P8456 「SWTR-8」地地鐵鐵
題解。
[P8331 ZJOI2022] 簡單題 - 洛谷 | 計算機科學教育新生態 (luogu.com.cn)
CF1763F Edge Queries
這題是不是有點裸?題目給的性質亂七八糟,完全沒用。
建出圓方樹,則 \(a, b\) 之間涉及到的所有點雙即 \(a, b\) 在圓方樹簡單路徑上的所有方點。
對於 \(a, b\) 路徑上的一個點雙,如果其不為兩點一邊即割邊,則刪去其中任意一條邊 \(a, b\) 仍相互可達。因此,對於所有點雙,若其為兩點一邊,則給其對應方點賦值 \(0\),否則賦點雙內部邊的條數作為權值,則詢問 \(a, b\) 的答案即 \(a, b\) 在圓方樹上的簡單路徑上的所有方點的權值之和。
視 \(n, m, q\) 同級,時間復雜度 \(\mathcal{O}(n\log n)\)。代碼。
4. 支配樹
由於 NOI 不可能考一般有向圖的支配樹(考了也寫不動啊),所以這玩意以后再學。
4.1 定義
對有向圖 \(G = (V, E)\),選定一個起點 \(s\)(若有多個,可以新建虛點向所有起點連邊轉化為單個起點)。若從 \(s\) 到 \(y\) 的所有路徑均經過 \(x\),即 \(x\) 是 \(y\) 從 \(s\) 出發的必經點,我們稱 \(x\) 支配 \(y\)。
容易證明支配關系具有
- 傳遞性:若 \(x\) 支配 \(y\),\(y\) 支配 \(z\),則 \(x\) 支配 \(z\)。
- 自反性:\(x\) 支配 \(x\)。
- 反對稱性:若 \(x\) 支配 \(y\),\(y\) 支配 \(x\),則 \(x = y\)。
這保證了支配是偏序關系,但偏序關系只能建出 DAG,不夠優秀。
支配關系有一條特殊性質:若 \(x, y\) 均支配 \(z\),則 \(x, y\) 之間也有支配關系。反證法,考慮 \(s\to z\) 的任意一條路徑,據條件 \(x, y\) 必然在路徑上。若 \(x\) 在 \(y\) 之前,因為 \(x\) 不支配 \(y\),所以 \(1\to y\to z\) 可以不經過 \(x\),與 \(x\) 支配 \(z\) 矛盾;對於 \(y\) 在 \(x\) 之前的情況同理。
不妨稱這種性質為半完全性。其重要之處在於,任何具有半完全性的系統均可以建樹:考慮支配 \(y\) 的所有點 \(D_y = \{x_1, x_2, \cdots, x_k\}\),對其中任意兩個元素 \(x_i, x_j (i\neq j)\) 應用半完全性,可知 \(D_y\) 內的所有元素形成了 全序 關系:任意兩個元素均可比較,則所有 \(x_i\) 的關系可以用一條鏈描述。
因此,令 \(idom_i\) 表示 \(D_i = \{x_1, x_2, \cdots, x_k\}\) 當中被其它所有點支配的 \(x_j\)。\(idom_i\) 稱為 \(i\) 的直接支配點(immediate dominator),可以理解為 \(i\) 的所有支配點當中除 \(i\) 本身以外距離 \(i\) 最近的那一個。特殊地,\(idom_s\) 不存在(類似樹根不存在父節點)。
在 \(i\) 和 \(idom_i\) 之間連邊,我們就得到了支配樹。根據半完全性,一個節點在支配樹上的祖先集合就是支配它的所有節點。
支配樹用於描述有向圖在給定起點時節點的必經性,類似廣義圓方樹完整地描述了無向圖的必經性。
4.2 DAG 支配樹
有向無環圖具有特殊性質,使得我們能夠方便地求出其支配樹。
對 DAG 拓撲排序,設當前節點為 \(x\)。根據拓撲排序的性質,\(x\) 對拓撲序在它之前的節點的必經性沒有影響,因為 \(x\) 甚至不可達它們。因此,考慮到 \(x\) 時,我們只需求出 \(idom_x\),不需要更改其它節點的 \(idom\)。
當 \(x\) 只有一條入邊 \(y\to x\) 時,\(idom_x\) 很顯然是 \(y\)。到達 \(x\) 必然要走 \(y\to x\) 這條邊,所以 \(y\) 支配 \(x\),推出 \(y\) 及其在支配樹上的所有祖先 \(anc(y)\) 都是 \(x\) 的支配點(支配的傳遞性),而 \(y\) 則是距離 \(x\) 最近的那一個。
當 \(x\) 有兩條入邊 \(y\to x\) 和 \(z\to x\) 時,若到達 \(x\) 走 \(y\to x\) 這條邊,則集合 \(anc(y)\) 是必經點;同理,若走 \(z\to x\) 這條邊,則集合 \(anc(z)\) 是必經點。據定義,\(x\) 的支配點是 \(anc(y)\) 與 \(anc(z)\) 的交集(不在交集內的某個節點均可以通過走 \(y\to x\) 或 \(z\to x\) 繞過去)加上 \(x\),前者即 \(anc(lca(y, z))\)。因此,\(idom_x = lca(y, z)\)。
通過上述推理,我們容易發現,若 \(x\) 有入邊 \(y_i \to x\),則 \(anc(x) \backslash x\) 等於 \(\bigcap\limits_{i = 1} ^ k anc(y_i)\),所以
因為拓撲序在 \(x\) 之前的節點均已經確定了其 \(idom\),所以在確定每個節點的 \(idom\) 后預處理倍增數組,倍增求 LCA 即可做到 \(\mathcal{O}((n + m)\log n)\)。代碼見例 I.
4.3 例題
I. P2597 [ZJOI2012] 災難
一道經典的 DAG 支配樹問題,代碼。
5. DAG 鏈剖分
前置知識:樹鏈剖分。
DAG 鏈剖分在國內信息學算法競賽界第一次被系統闡述是在 2022 年戴江齊學長的集訓隊論文中。它看上去是比較新的科技,但實際上很早就有 DAG 鏈剖分相關的題目了(GRE Words Once More)。
5.1 算法介紹
DAG 鏈剖分的核心思想和樹鏈剖分類似,都是通過 設置重兒子 的方法將圖剖分成若干條鏈,使得圖上任意路徑經過的鏈的條數在可接受的范圍內。
若原圖存在若干無入度的節點,則新建源點向它們連邊。因此,可以規范 DAG 的形態僅有節點 \(1\) 無入度。設 \(f_i\) 表示從 \(i\) 出發的路徑條數。
從樹剖出發,考慮定義重兒子為 \(f\) 值最大的后繼。形式化地,\(son_i = \mathrm{argmax}_{j\in suc(i)} f_j\),並稱 \(i\to son_i\) 為重邊。將所有重邊建圖得到內向森林。容易發現對於任意輕邊 \(i\to j\),\(2f_j \leq f_i\),因為 \(f_i = 1 +\sum_{j\in suc(i)} f_j\) 且存在 \(j'\in suc(i)\) 使得 \(f_{j'}\geq f_j\)。這種結構配合倍增可以解決部分題目,但不夠優秀,我們希望 \(i\to son_i\) 形成鏈而不是樹。為此,每個節點的入度也不應大於 \(1\)。
因此,設 \(g_i\) 表示從 \(1\) 到 \(i\) 的路徑條數。顯然 \(g_j = [j = 1] + \sum_{j\in suc(i)} g_i\)。設 \(i\to j\) 為重邊當且僅當 \(j\) 為 \(i\) 的 \(f\) 值最大的后繼,且 \(i\) 為 \(j\) 的 \(g\) 值最大的前驅。這樣每個點的出度和入度均不大於 \(1\)。容易發現對於任意輕邊 \(i\to j\),要么 \(2f_j \leq f_i\),要么 \(2g_i \leq g_j\),因此每走一條輕邊,要么 \(f\) 值減半,要么 \(g\) 值翻倍。因此一條路徑上的輕邊條數為 \(\mathcal{O}(\log V)\) 級別,其中 \(V\) 為路徑條數。這說明 只有當路徑條數不大時 DAG 鏈剖分才適用。
DAG 鏈剖分常與后綴自動機結合,因為字符串 \(|s|\) 的后綴自動機的 DAWG 的路徑條數為 \(s\) 的本質不同子串數,且剖分 DAWG 后可以快速定位字符串查詢相關信息。更重要的是可以用 \(\log\) 條時間戳連續的鏈描述從 \(s[l, r_1]\) 到 \(s[l, r_2]\) 的路徑,我們將在例題中看到這非常有用。
5.2 例題
*I. UOJ752 Border 的第五種求法
感覺思想挺簡單的,但是是未曾設想的道路。
對 \(s\) 建出 SAM,則 \(B\) 是 \(s[l, r]\) 的 border 當且僅當:
- \(B\) 是 \(s[l, r]\) 的前綴。
- \(B\) 在 link 樹上是 \(s[1, r]\) 的祖先。
因為 \(s[l, r]\) 的所有前綴形成 DAWG 上一條路徑,考慮 DAG 剖分將路徑拆成 \(\log n\) 條時間戳連續的重鏈,則問題轉化成 \(\mathcal{O}(q\log n)\) 次詢問 \(s[1, r]\) 到根的路徑上時間戳落在某區間的節點的權值和,而 \(i\) 的權值即 \(f_{|\mathrm{endpos}(i)|}\)。
- 如何拆重鏈:容易求出當前節點 \(i\) 到重鏈末尾形成的字符串在原串上的任意對應下標 \([l, r]\)。不妨設當前要匹配 \(s[x, y]\),則當前重鏈匹配長度即 \(|lcp(s[l, r], s[x, y])|\),容易 SA 或者 SAM 求出。
二維數點,離線后在樹上掃一遍,用 BIT 維護單點修改區間查詢。時間復雜度 \(\mathcal{O}(n\log n + q\log ^ 2 n)\),代碼。
*II. CF1098F Ж-function
提供一個比較好想的思路。
一句話題解:DAG 鏈剖分 + P4211 + 處理算錯的貢獻。
翻轉 \(s\) 將問題轉化為求 \(s[l, r]\) 的每個前綴與 \(s[l, r]\) 的最長公共后綴。眾所周知兩串 LCS 可以用 SAM 的 link 樹的 LCA 的 \(len\) 值大致描述,僅有當兩串存在祖先后代關系時需要特殊考慮。因此,不妨先求
考慮上式,發現形式很像經典老題 P4211 [LNOI2014] LCA。問題在於 \(s[l, p]\) 隨着 \(p\) 從 \(l\) 增大到 \(r\),其取值和 \(l\) 有關,我們不能對每個 \(l\) 都做一遍離線掃描線。注意到它是 DAWG 的路徑,因此考慮 DAG 鏈剖分。將其剖成 \(\mathcal{O}(\log n)\) 條 DAWG 上時間戳連續的鏈,這樣只需按 DAWG 時間戳的順序掃描線。也就是說,我們將問題轉化為了 \(\mathcal{O}(n)\) 次加入,\(\mathcal{O}(q\log n)\) 次查詢的 P4211。將 link 樹樹剖,用 BIT 維護區間給 \(c_i\) 加 \(1\),區間查詢 \(c_iv_i\) 之和,其中 \(v_i\) 即時間戳為 \(i\) 的節點 \(u\) 的 \(len\) 值減去其父親的 \(len\) 值,這樣 \(x\) 處的修改對 \(y\) 處的查詢的貢獻即 \(len(lca(x, y))\)。這部分時間復雜度 \(\mathcal{O}(q\log ^ 3 n)\),但三個 \(\log\) 分別是 DAG 鏈剖分,樹剖,BIT,而難以構造使得 link 樹卡滿前兩個 \(\log\) 的字符串 \(s\),所以常數非常小。
然后我們處理算錯的貢獻。因為 \(s[l, p]\) 不可能是 \(s[l, r]\) 的后代,所以只需考慮 \(s[l, p]\) 是 \(s[l, r]\) 的祖先的情況。此時減去 \(len(s[l, p])\),加上 \(p - l + 1\)。每一條 DAWG 上時間戳連續的鏈 \([ql, qr]\) 相當於查詢 \(s[l, r]\) 到根的路徑上,在 DAWG 上時間戳 \(t\) 落在 \([ql, qr]\) 范圍內的節點 \(x\) 對應的 \(s[l, p]\) 的 \(p - l + 1 - len(x)\) 之和。\(-len(x)\) 容易樹上線段樹前綴和預處理,但 \(p - l + 1\) 該怎么辦?每個節點可能對應多個長度 \([len(fa(x)) + 1, len(x)]\),不能和 \(-len(x)\) 放在一起預處理。但注意到,我們容易求出 \([ql, qr]\) 對應的 \(p\) 的范圍 \([pl, pr]\),而時間戳隨着 \(p\) 增大同樣連續,因此時間戳 \(t\) 對應的 \(p - l + 1\) 又可以寫為 \(pl + (t - ql)\)。該式 \(pl, ql\) 都是定值,只有 \(t\) 和每個節點有關,而顯然每個節點只有一個時間戳,因此線段樹額外維護落在當前范圍內的時間戳之和以及個數即可。這部分時間復雜度 \(\mathcal{O}((n + q)\log ^ 2 n)\)。
注意第一部分因為離線,空間復雜度必須 \(q\log n\)。為減小空間開銷,第二部分需要離線 dfs 解除祖先限制做到空間復雜度線性。總時間復雜度 \(\mathcal{O}((n + q\log n) \log ^ 2 n)\),擦時限通過。代碼。
6.1 Dilworth 定理
CF1738G Anti-Increasing Addicts
果然還是 Anton 出的題最智慧。
初步感知問題:
-
當沒有限制一些位置必須保留時,直接刪去 \((n - k + 1) \times (n - k + 1)\) 的正方形即可滿足條件。
-
題目要求不能出現保留位置不出現長度為 \(k\) 的鏈,根據 dilworth 定理,必須用不超過 \(k - 1\) 個反鏈覆蓋所有保留位置。
-
\(k - 1\) 條反鏈至多覆蓋 \(n ^ 2 - (n - k + 1) ^ 2\) 個位置,因為第一條反鏈至多覆蓋 \(2n - 1\) 個位置,第二條反鏈至多覆蓋 \(2n - 3\) 個位置。想象每次剝去正方形的一行一列。因此,反鏈覆蓋位置是極大的,限制已經最嚴格。
設 \(f_{x, y}\) 表示從 \((x, y)\) 開始的只經過保留位置的最長鏈長度。顯然若存在 \(f_{x, y} = k\) 則無解,因為我們被強制保留長度為 \(k\) 的鏈。否則我們嘗試用 \(k - 1\) 條反鏈在最大化覆蓋位置的同時,覆蓋到所有強制保留的位置。
根據 \(f_{x, y}\) 的值將位置分層,值相同的位置必然形成一條反鏈,否則 \(p\to q\) 形成鏈,\(f_p\) 可以更大。
為了讓最大化覆蓋位置,不妨設第 \(i\) 條反鏈從 \((n, i)\) 開始,每次向上或向右走,經過所有未被覆蓋的 \(f\) 值為 \(k - i\) 的位置並將經過的點全部覆蓋,最終走到 \((i, n)\)。若反鏈互不相交,則覆蓋位置之和為 \(\sum_{i = 1} ^ {k - 1} 2(n - i) + 1\),達到最大值。
為此,我們規定反鏈 能向上走就向上走。也就是說,當且僅當上方已經被覆蓋或再往上走就無法覆蓋某個 \(f\) 值為 \(k - i\) 的位置時,我們才會向右走。為此,設 \(lim_{v, y}\) 表示第 \(y\) 列及其右側 \(f\) 值為 \(v\) 的位置的行數最大值,若當前 \(x = f_{k - i, y + 1}\) 則無法繼續往上走,否則無法覆蓋 \(y + 1\) 及其右側 \(f\) 值為 \(v\) 的行數最大的位置。
如上圖。
只需證明第 \(i\) 條反鏈必然經過 \((n - k + i, i)\) 和 \((i, n - k + i)\) 即可證明合法方案一定存在,而這是容易的。對於第 \(i\) 條反鏈,在第 \(i + 1\) 列及其右側不會出現行數大於 \(n - k + i\) 的 \(f\) 值為 \(k - i\) 的位置 \((x, y)\),其中 \(x > n - k + i\),因為否則其對應鏈末端的行數 \(x' \geq x + (k - i) > n\),不合法。因此,根據 盡量向上走 原則,從 \((n, i)\) 出發一定可以走到 \((n - k + i)\)。同理可證最終必然經過 \((i, n - k + i)\)。
\(f\) 可以直接二維前綴最大值求出,時間復雜度 \(\mathcal{O}(n ^ 2)\)。實際上將 \(f\) 直接當成二維前綴最大值數組也是對的,因為它不影響任何東西,證明略。代碼。
參考資料
第二章:
- 算法學習筆記(71):2-SAT - Pecco。
- 【計算理論】計算復雜性(NP 完全問題 - 布爾可滿足性問題 | 布爾可滿足性問題是 NP 完全問題證明思路)- 韓曙亮。
- 合取 - 百度百科。
- 2-SAT 適定性 (Satisfiability) 問題知識點詳解 - zeng_jun_yv。
第三章:
第四章:
第五章: