By LFsWang
定義 \(P(v)\) 表示點 \(v\) 在 DFS Tree 的父節點
而 \(P(\text{root})\) 是空節點
Tree edge : DFS 過程中行走的邊
(下一個點是沒走過的)
void dfs(int v, int fa) {
used[v] = true;
for (auto u:V[v]) {
if (!used[u]) {
dfs(u, v); // tree edge (v->u)
}
}
}
Back edge : 接觸到目前祖先的邊
(因此這條邊與 Tree edge 會形成環)
Root
如果 \(G\) 是 連通無向圖,則
\(G\) 的 DFS Tree 只由下兩種邊構成
Root
\(p\)
Proof.
\(v\) 是 \(p\) 的子節點
設 \(p\) 有一條邊走到了 \(a\),而 \(a\) 已被標記已走過,而且 \(a\) 不是 \(v\) 的祖先
根據 DFS 的演算法,表示 \(a\) 為根的子樹已經被遍歷
考慮點 \(v\)
因為有路徑 \(a\to v\),故 \(v\) 也被遍歷了
故 \(v\) 不再參與走訪
因此透過 \(v\) 走訪 \(a\) ,與目前狀態矛盾
\(a\)
\(v\)
Root
Forward edge : 會到目前點先繼點的邊
(這條邊會到目前點的更深處)
Forward edge
Root
Cross edge : 會到目前點無相關子樹的邊
(這條邊必不與 Tree edge 產生迴圈)
Cross edge
無向圖 | 有向圖 |
---|---|
Tree edge | Tree edge |
Back edge | Back edge |
Cross edge | |
Forward edge |
Forward edge (實作限制)
連通分量
設集合 \(C\) 為無向圖 \(G\) 的一個連通分量
若點 \(x,y\in G\)
\(x,y \in C\Leftrightarrow x,y\) 之間有一條路徑
鄰點:若存在邊 \(e(v,w)\),則 \(v\), \(w\) 互為鄰點
因此 \(v\) 的鄰居 \(N(v)\) 是 \(v\) 的邊可以到達的所有點集合。
路徑:若 \(a,b\) 在同一個連通塊
\(\operatorname{path}(a,b)\) 表示 a 到 b 的任意一種路徑。
看起來就像disjoint set
定義函數 \(C(g)\) 表示圖 \(g\) 的連通分量數量,給定一張無向圖 \(G\)
根據連通分量的特性,DFS 任意一個點會走遍該點的連通分量
因此就看用了幾次 DFS 就能知道答案
關節點與橋
有一張無向連通圖\(G\)
若點 \(v\) 是關節點 (AP) \(\Leftrightarrow\) \(G\) 刪除 \(v\) 後,\(C(G)>1\)
有一張無向連通圖
若邊 \(e\) 是橋 \(\Leftrightarrow\) 從 \(G\) 刪除 \(e\) 後,\(C(G)>1\)
給一張無向圖,請找出圖裡面所有的關節點 (與橋)。
枚舉所有的點當關節點,模擬刪掉後,是否依然能遍歷所有的節點
橋亦同
DFS:\(O(E+V)\)
總複雜度:\(O((E+V)^2)\)
圖論大師 Tarjan 在1973年提出了在 \(O(n)\) 時間找出AP的演算法
實際上同方法也可以用來找 Bridge
note. \(O(n)\) 在圖論表示 \(O(E+V)\)
定義 \(T\) 是 \(G\) 的 DFS Tree
對於點 \(v \in G\),定義函數如下
Depth \(D(v)\):表示點 \(v\) 在 \(T\) 的深度
$$D=1$$
$$D=2$$
$$D=3$$
$$D=4$$
定義 \(T\) 是 \(G\) 的 DFS 搜索樹
對於點 \(v \in G\),定義函數如下
Lowpoint \(L(v)\) :
在不通過往 parent 的 tree edge 路徑上,
自己、
所有能透過 tree edge 走到的點,
以及前述所有點透過 back edge 連接的點中,
\(D(x)\) 的最小值
parent
X
Quick Hint : 只有 "自己"、或 "Back edge" 連接點會是 L(x) 的答案
✔
✔
✔
自己
子樹
子樹的臨點
(紅色勾勾的鄰居)
\(L(x)\) 是打勾部分 \(D(x)\) 的最小值
✔
臨點
X
\(D(x)\) 小 <--------------------------------> 大
可以利用子樹的答案來推出自己的 Lowpoint
a 是 v 的臨點,不包含 parent
b 是 v 子樹的根結點
$$L(v) = \min( D(v),D(a),L(b) )$$
定理1. 根節點 \(r\) 是 AP \(\Leftrightarrow \deg(r) > 1\)
Proof
若移除 \(r\) 會使原圖產生 \(\deg(r)\) 個連通分量
根據AP的定義得證
DFS tree 中 根root 通常是特例 !!
定理2. \(v\) 是非 root 的點,w 是 \(v\) 的鄰點。
若 \(v\) 是 AP \(\Leftrightarrow \exists w :L(w)\geq D(v)\)
\(v\)
\(w\)
\(L(w)\)
因為 w 無法走到比v 還要前面的點
移除 v 必然會至少分成 w 與 r 的兩個連通分量
\(r\)
定理3. \(v\) 是非 root 的點,\(w\) 是 \(v\) 的鄰點。
若 \(e(v,w)\) 是 Bridge \(\Leftrightarrow \exists w :L(w) > D(v)\)
\(v\)
\(w\)
\(L(w)\)
因為 w 無法走到比w 還要前面的點
移除 e 必然會至少分成 w 與 r 的兩個連通分量
\(r\)
\(e(v,w)\)
在 Tarjan 的原始論文中,D(x) 是定義為 DFS 遍歷順序的時間戳,也是較為常見的實作方法
實際上只需要保證 DFS 時每一條向下的通路 D(x) 遞增就行
void DFS(int v, int fa) //call DFS(v,v) at first
{
D[v] = L[v] = timestamp++; //timestamp > 0
int childCount = 0; //定理1 for root
bool isAP = false;
for (int w:adj[v])
{
if( w==fa ) continue;
if ( !D[w] ) // D[w] = 0 if not visited
{
DFS(w,v);
childCount++;
if (D[v]<=L[w]) isAP = true; //定理2
if (D[v]< L[w]) edgeBridge.emplace_back(v, w);//定理3
L[v] = min(L[v], L[w]);
}
L[v] = min(L[v], D[w]);
}
if ( v == fa && childCount < 2 ) isAP = false; //定理1, v==fa只是確認root
if ( isAP ) nodeAP.push_back(v);
return ;
}
給圖找Cutting Point
UVa 315
有重邊版本 (注意Lowpoint定義)
hdu4612
橋與橋連通分量
橋
橋連通分量
橋連通分量
橋連通分量
橋
橋連通分量
橋
如果將一張圖同一個 BCC 所有點合併成一個點,會構成一棵樹
(顯然的,這樹上的每一條邊都是橋)
可以利用此性質將圖的問題轉換為樹的問題加以思考!
橋
\(L[v]=D[v]\)
void dfs (int v, int fa) {
st.push(v);
/* 繼續 dfs v 的子樹 */
if (想看子樹有誰) {
int x;
do {
x = st.pop(); // 彈出子樹節點
} while (x!=v);
}
}
橋
\(D[v]=L[v]\)
砍掉
stack 彈出來
橋
\(D[v]=L[v]\)
砍掉
stack 彈出來
橋
\(D[v]=L[v]\)
砍掉
stack 彈出來
void DFS(int v, int fa) { //call DFS(v,v) at first
D[v] = L[v] = timestamp++; //timestamp > 0
st.emplace(v);
for (int w:adj[v]) {
if( w==fa ) continue;
if ( !D[w] ) { // D[w] = 0 if not visited
DFS(w,v);
L[v] = min(L[v], L[w]);
}
L[v] = min(L[v], D[w]);
}
if (L[v]==D[v]) {
bccid++;
int x;
do {
x = st.top(); st.pop();
bcc[x] = bccid;
} while (x!=v);
}
return ;
}
剩下的部分與橋相同
練習題
POJ 3177 = POJ 3352
給一個連通無向圖,問至少加入幾條邊使得無向圖中沒有橋
割點
割點
橋
割點
割點
割點
橋
割點
割點
\(L[u]\geq D[v]\)
割點
stack<tuple<int,int>> st; // 以紀錄邊為主的堆疊 !!
void DFS(int v, int fa) { // call DFS(v,v) at first
D[v] = L[v] = timestamp++; //timestamp > 0
for (int w:adj[v]) {
if( w==fa ) continue;
if ( !D[w] ) { // D[w] = 0 if not visited
st.emplace(v, w); // 堆疊上壓入的是邊 !
DFS(w,v);
L[v] = min(L[v], L[w]);
if (L[w] >= D[v]) { // 找到割點!
// 收縮
}
}
L[v] = min(L[v], D[w]);
}
return ;
} // 用完我 stack 要記得清乾淨!!
stack<tuple<int,int>> st;
void DFS(int v, int fa) { //call DFS(v,v) at first
D[v] = L[v] = timestamp++; //timestamp > 0
for (int w:adj[v]) {
if( w==fa ) continue;
if ( !D[w] ) { // D[w] = 0 if not visited
st.emplace(v, w);
DFS(w,v);
L[v] = min(L[v], L[w]);
if (L[w] >= D[v]) { // 找到割點!
int x, y;
bcc.push_back({});
do {
tie(x, y) = st.top(); st.pop();
bcc.back().emplace_back(y);
} while (tie(x, y) != tie(v, w));
bcc.back().emplace_back(v);
}
}
L[v] = min(L[v], D[w]);
}
return ;
} // 用完我 stack 要記得清乾淨!!
割點
割點
橋
割點
割點
割點
割點
Block Cut Tree 是區塊-割點交錯組成的
練習題
HDU 3749 Financial Crisis
補充 雙連通分量的一些性質
1. 雙連通分量如果是 2 - connnect 的,至少要三個點
特例!
補充 雙連通分量的一些性質
2. 雙連通分量如果是 2 - connnect 的
表示任兩點間存在兩條互斥路徑 (環)
且
任兩邊可以找到一個環,包含該兩邊
補充 雙連通分量的一些性質
3. 雙連通分量如果是 2 - connnect 的
如果該連通分量有一個奇環
雙連通分量的任一點都被至少一個奇環覆蓋
強連通分量
令 G 是有向圖,\(v,w\) 是 G 上的兩點,\(S\) 是 G 的一個 SCC,
$$v,w \in S \Leftrightarrow \exists \operatorname{path}(v,w),\operatorname{path}(w,v)$$
SCC 由數個有向環構成!
縮點操作
將同一個強連通分量內的所有節點合併為一個點,構成一張新圖。
定理4.設 \(G'\) 是 \(G\) 經由縮點操作生成的圖
\(G'\) 是有向無環圖 (DAG)
Proof.
若 \(G'\) 有環,那這一個環構成一個 SCC,根據縮點操作,這個環必須合併成一個點,故 \(G'\) 不存在環
有兩個線性方法可以求 SCC
圖論大師 Tarjan 在連通分量有許多著作,這也是其中一個
方法沿用了前一個演算法的 \(L(x),D(x)\) 函數
定理5. 若點 \(v\) 有 \(L(v)=D(v)\),\(v\) 子樹所有未縮點的點構成 SCC
定理5. 若點 \(v\) 有 \(L(v)=D(v)\),\(v\) 子樹所有未縮點的點構成 SCC
小心再有向圖上算 Low point 時,會有 Cross edge !
void DFS(int v, int fa) { //call DFS(v,v) at first
D[v] = L[v] = timestamp++; //timestamp > 0
st.push(v);
inSt[v] = true;
for (int w:adj[v]) {
if ( !D[w] ) { // D[w] = 0 if not visited
DFS(w,v);
L[v] = min(L[v], L[w]);
} else if (inSt[w]) { // it is different!
L[v] = min(L[v], D[w]);
}
}
if( D[v] == L[v] ) {
int x;
do {
x = st.top();
st.pop();
scc[x] = SCCID;
inSt[x] = false;
} while( x != v );
SCCID++;
}
}
兩次 DFS 的方法
用後序走訪走過整張圖放入 stack
由 stack 一個一個拿出來在反圖 DFS,走到的都是該點的 SCC
void DFS(vector<int> *dG, int v, int k=-1){
visited[v] = true;
scc[v] = k;
for (int w:dG[v])
if (!visited[w])
DFS(dG,w,k);
if(dG==G) st.push(v);
}
int Kosaraju(int N){
memset(visited,0,sizeof(visited));
for(int i=0; i<N; ++i) if(!visited[i]) DFS(G,i);
memset(visited,0,sizeof(visited));
while(!st.empty()){
if (!visited[st.top()]) DFS(GT, st.top(), sccID++);
st.pop();
}
return sccID;
}
給定一些骨牌,且已知推倒每張骨牌會連動哪一些骨牌,問至少需推倒幾張骨牌才能使所有骨牌都倒下。
UVa 11504
https://codeforces.com/contest/1547/problem/G
CF #731 (Div. 3) G. How Many Paths?
CF #545 (Div. 1) C. Museums Tour
滿足性問題
以 AND 運算將許多 OR 運算式合併成的算式
$$(a \lor b \lor c )\land (c \lor \neg b \lor d )\land (e)=1?$$
如果在最多 OR 的子運算式中,最多只有 \(K\) 個變數,
那麼我們稱這一個問題為 K-SAT 問題。
定理 7. 所有 SAT 問題都能轉換成 3-SAT 問題
定理 6. 所有 NPC 問題都能轉換成 SAT 問題
定理 8. 3-SAT 問題是 NPC 問題
如果 3-SAT 問題可以被進一步化簡為 2-SAT 問題
定理 9. 2-SAT 問題是 P 問題
可以在 \(O(n)\) 的時間判定 2-SAT 問題
$$(a \lor b)\land (c \lor \neg b )\land (e)=1?$$
運用 SSC 的概念
考慮布林式 \(a \lor b\),若要讓此式子為 true
若 a 選擇 false,b 一定要選 true
若 b 選擇 false,a 一定要選 true
將 2SAT 依賴關係表示成一張圖 \(G_r\)
有向邊 (A,B) 表示若選擇 A,就要選擇 B
$$B'$$
$$A'$$
$$A$$
$$B$$
\(X'\)表示X使用false的狀態
怎麼連 ?
( not a or b )
( a )
( not a )
( a xor b )
定理10. 若 2-SAT 問題無解,則存在 X 使
\(X,X'\)位在同一個 SCC
反之,可以構造一組可行的解
( 對於每一個變數選擇拓譜排序後順位較後者 )
X = true
X = false
hdu 3062
樹
最多可能有兩個重心
int sz[100001];
int center, csize; // csize = INT_MAX; for init
int _N; // ensure _N is how many node of the tree
void dfs(int v, int fa) {
int maxsub = 0;
sz[v] = 1;
for (int u:E[v]) {
if (u==fa) continue;
dfs(u, v);
maxsub = max(maxsub, sz[u]);
sz[v] += sz[u];
}
maxsub = max(maxsub, _N-sz[v]);
if (maxsub<csize) {
center = v;
csize = maxsub;
}
}
_N-size[v]
v
給兩個樹,然後在這兩個樹中間加一條邊變成一棵樹 \(T\)
請找出怎麼加邊,會使樹 \(T\) 的點距離總和最小
定義樹 \(T\) 兩點 \(u,v\) 的距離為 \(d(u,v)\)
樹距離總和 \(S\) 為
$$S=\frac{1}{2}\sum\limits_{u,v\in T}d(u,v)$$
要怎麼計算這東西 ?
Hint : 計算每一條邊會被經過幾次
對於每一條邊 = 左邊有多少點 x 右邊有多少點
定義 \(s_r(v)\) 為以 \(r\) 為根時, \(v\) 子樹的大小
樹距離總和 \(S\) 有
$$S=\frac{1}{2}\sum\limits_{u,v\in T}d(u,v)=\sum\limits_{v \in T} s_r(v) \times (n-s_r(v))$$
long long ans = 0;
long long sol(int v, int fa) {
long long sum = 1;
for (int u:E[v])
if (u!=fa)
sum += sol(u, v);
ans += sum * (N-sum);
return sum;
}
總和 = 原來樹裡面的兩兩總和 + 跨過連接線的兩兩總和
\(S\)
\(T\)
(左邊所有點-S) - 連接線 - (T-右邊所有點)
每條新的配對新增
起點到 S 的距離 + \(1\) + 終點到 T 的距離
\(S\)
\(T\)
\(S\)
\(T\)
總距離 = 左樹到S的總距離 x 右樹到T的總距離 + 左樹大小 x 右樹大小
重心到所有點距離總和最短 !
\(S,T\) 是(在原來樹上的)重心時,答案最小 !
#include <bits/stdc++.h>
using namespace std;
int N;
vector<int> E[100001];
int count(int v, int fa) {
int sum = 1;
for(int u:E[v])
if (u!=fa)
sum += count(u, v);
return sum;
}
int sz[100001];
int center, csize; // csize = INT_MAX; for init
// ensure _N is how many node of the tree
void dfs(int v, int fa, int _N) {
int maxsub = 0;
sz[v] = 1;
for (int u:E[v]) {
if (u==fa) continue;
dfs(u, v, _N);
maxsub = max(maxsub, sz[u]);
sz[v] += sz[u];
}
maxsub = max(maxsub, _N-sz[v]);
if (maxsub<csize) {
center = v;
csize = maxsub;
}
}
long long ans = 0;
long long sol(int v, int fa) {
long long sum = 1;
for (int u:E[v])
if (u!=fa)
sum += sol(u, v);
ans += sum * (N-sum);
return sum;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> N;
for(int i=0;i<N-2;++i) {
int x, y;
cin >> x >> y;
E[x].emplace_back(y);
E[y].emplace_back(x);
}
vector<int> centers;
memset(sz, -1, sizeof(sz));
for(int i=1;i<=N;++i) {
if (sz[i] == -1) {
csize = INT_MAX;
dfs(i,i, count(i,i));
centers.emplace_back(center);
}
}
assert(centers.size() == 2);
E[centers[0]].emplace_back(centers[1]);
E[centers[1]].emplace_back(centers[0]);
ans = 0;
sol(1,1);
cout << ans << '\n';
return 0;
}
(樹)重心分解
重心分解樹 \(T_{CD}\) 定義如下
重心分解樹 \(T_{CD}\) 樹高為 \(O(\log n)\)
重心分解樹 \(T_{CD}\) 如果移除樹根
子樹的組成成員與原樹一樣
如果 \(c\) 是 \(a,b\) 在 \(T_{CD}\) 的最近共同祖先 (LCA) ,則
在原樹上 \(a\) 到 \(b\) 必通過 \(c\)
給一個藍色的樹,有兩個操作
update(a) 把 a 塗成紅色
query(a) 問離 a 最近的紅色點距離是多少
如果 \(c\) 是 \(a,b\) 在 \(T_{CD}\) 的最近共同祖先 (LCA) ,則
在原樹上 \(a\) 到 \(b\) 必通過 \(c\)
我們可以把問題變成
\(a\) 到 \(c\) 再到 \(c\) 子樹其他點
把 \(a\) 到所有點變成
\(a\) 到 \(c\) 再到 \(c\) 子樹其他點
對於 \(T_{CD}\) 上的每個點,紀錄他到到他子樹的答案是多少
對於每一個查詢枚舉中繼點 \(c\) 組合答案
因為 \(T_{CD}\) 的高度只有 \(O(\log n)\)
因此只需要檢查\(O(\log n)\) 個中繼點 !
樹直徑
圖的直徑,為圖上最長的最短距離
因為樹的最短距離唯一
因此樹的直徑就是最遠兩個點的距離
\(\operatorname{diam}(G)=\max\limits_{u, v \in G} \operatorname{dist}(u, v)\)
定理
若樹邊權皆非負整數,
從任意點出發,所能到達的最遠點為直徑的某一端點。
x
r
d
d'
很明顯的, \(r\) 比 \(d'\) 更適合當直徑端點
\(\operatorname{dist}(d,d') < \operatorname{dist}(r,d') \)
或
\(\operatorname{dist}(d,d') < \operatorname{dist}(r,d) \)
x
r
d
d'
x
r
d
d'
\(r\) 比 \(d'\) 更適合當直徑端點
\(\operatorname{dist}(d,d') < \operatorname{dist}(r,d') \)
\(\operatorname{dist}(x,d) < \operatorname{dist}(x,r) \)
\(\because\) 綠 > 紅 + 黃
\(\therefore\) 綠 + 紅 > 黃
x
r
d
d'
\(r\) 比 \(d'\) 更適合當直徑端點
\(\operatorname{dist}(d,d') < \operatorname{dist}(r,d') \)
\(\operatorname{dist}(x,d) < \operatorname{dist}(x,r) \)
\(\because\) 綠 > 紅 + 黃
\(\therefore\) 綠 + 紅 > 紅 + 黃
int fv, fd;
void dfs(int v, int pa, int d) {
if (d > fd) {
fd = d;
fv = v;
}
for (int u:V[v])
if (u!=pa)
dfs(u, v, d+1);
}
int diam() {
fd = -1;
dfs(0, 0, 0);
fd = -1;
dfs(fv, fv, 0);
return fd;
}
設 \(d[x]\) 表示以 \(x\) 為根子樹的最深深度
\(d[x] = \max\limits_{u \in \text{child of } x} 1 + d[u]\)
設 \(d[x]\) 表示以 \(x\) 為根子樹的最深深度
\(d[x] = \max\limits_{u \in \text{child of } x} 1 + d[u]\)
一個樹的直徑長就是枚舉 \(x\) ,取最長深度 + 次長深度 (若存在) 的最大值
x
d
d'
int dp[100000][2]; // dp 維護前兩大的深度
void push(int *a, int v) { // 單純的插入排序
if (v > a[1]) a[1] = v;
if (a[1] > a[0]) swap(a[1], a[0]);
}
int ans;
void dfs(int v, int pa) {
dp[v][0] = dp[v][1] = 0;
for (int u:V[v]) {
if (u == pa) continue;
dfs(u, v);
push(dp[v], 1 + dp[u][0]);
}
ans = max(ans, dp[v][0] + dp[v][1]);
}
int diam() {
ans = 0;
dfs(0, 0);
return ans;
}
一般而言,直徑常用於在樹上構造路徑
URAL 1752 Tree 2
給一個大小為 \(n\) 的樹 ,有 \(Q\) 次詢問
每次詢問點 \(v\) ,輸出任意一個與 \(v\) 距離為 \(d\) 的點。
Hint : \(v\) 能到的最遠點為樹直徑的端點
URAL 1752 Tree 2
給一個大小為 \(n\) 的樹 ,有 \(Q\) 次詢問
每次詢問點 \(v\) ,輸出任意一個與 \(v\) 距離為 \(d\) 的點。
Hint : \(v\) 能到的最遠點為樹直徑的端點
答案比然出現在 \(v\) 往樹直徑端點的路上,
因此求往樹直徑端點恰 \(d\) 步的點是什麼就好
紅字作法於 LCA 篇章提到
Problem : 101 - 捷運路線
x
d
d'
y
d
d'
一般的樹鏈剖分是按子樹大小剖分
最近共同祖先
給一個隨便的無根樹很麻煩,所以我們隨便挑一個點當作根
定義:在有根樹上,點 \(x,y\) 往 root 的最短路徑上,第一個相遇的點
\(\operatorname{lca}(x,y)\)
\(x\)
\(y\)
如果詢問的答案可以透過扣除來調整,可以考慮用 LCA
ex 兩點間最短路徑長
$$\operatorname{dist}(x,y) = \operatorname{dist}(r,x)+\operatorname{dist}(r,y)-2\times \operatorname{dist}(r,\operatorname{lca}(x,y))$$
求一個點的\(2^k\)祖先是哪一點,使用DP來完成
定義dp[i][j]表示點i的第\(2^j\)個祖先節點是誰
$$dp[i][j]=\begin{cases}-1 & \text{not exist} \\ fa(i)&j=0\\dp[dp[i][j-1]][j-1]&dp[i][j-1]\neq -1\end{cases}$$
for (int i=0; i<lgN; ++i)
for (int x=0; x<n; ++x)
{
if (P[x][i]==-1) P[x][i+1] = -1;
else P[x][i+1] = P[P[x][i]][i];
}
要先用 dfs 求 dp[i][0] 的答案
使用動態規劃,可以在 \(O(\log |V|)\) 的時間求出 \(v\) 的任意遠祖先
把移動距離二進位分解
再移動即可
int jump(int x,int d)
{
for(int i=0;i<lgN;++i)
if( (d>>i)&1 )
x=dp[x][i];
return x;
}
使用 DP 表格 + Jump 求 LCA
int find_lca(int a,int b){
if(dep[a]>dep[b])
swap(a,b);
b=jump(b,dep[b]-dep[a]);
if(a==b)
return a;
}
Step 1. 調整深度
把a,b移動到相同深度的位置去
如果移動完a=b就完成
使用 DP 表格 + Jump 求 LCA
int find_lca(int a,int b){
if(dep[a]>dep[b])
swap(a,b);
b=jump(b,dep[b]-dep[a]);
if(a==b)
return a;
for(int i=MAX_LOG;i>=0;--i)
{
if(dp[a][i]!=dp[b][i])
{
a=dp[a][i];
b=dp[b][i];
}
}
}
Step 2. 二分搜
k 由大到小跳 \(2^k\) 步
如果是同樣的點,就不動,不然就跳上去
使用 DP 表格 + Jump 求 LCA
int find_lca(int a,int b){
if(dep[a]>dep[b])
swap(a,b);
b=jump(b,dep[b]-dep[a]);
if(a==b)
return a;
for(int i=MAX_LOG;i>=0;--i)
{
if(dp[a][i]!=dp[b][i])
{
a=dp[a][i];
b=dp[b][i];
}
}
return dp[a][0];
}
Step 3.完成
最後會停再 LCA 下方
丟回 dp 的答案即可
一些定理 @
你得到了一棵 N 個節點的樹!! (頂點編號 1~N)
你選定了一個起點 S 跟一個終點 T,
現在你從 S 走到 T,請求出走第 K 步時走到的點。
( K=0 表示還待在 S )。
TIOJ 1687
\(O(n+Q)\) 一次算完
預備工具:路徑壓縮的 disjoint set
int x, y, N, Q;
vector<tuple<int,int>> query[100]; // (另一點, 答案放置位置)
vector<int> V[100];
int ans[100];
cin >> N >> Q;
for (int i = 0; i < N; ++i) {
cin >> x >> y;
V[x].emplace_back(y);
V[y].emplace_back(x);
}
for (int i = 0; i < Q; ++i) {
cin >> x >> y;
query[x].emplace_back(y, i);
query[y].emplace_back(x, i);
}
r
a
b
r
a
b
採用前序走訪 (進入就先標記該點)
r
a
b
採用前序走訪 (進入就先標記該點)
dfs 某一子樹 \(a\),把該子樹合併到 \(r\)
r
a
b
採用前序走訪 (進入就先標記該點)
dfs 某一子樹 \(a\),把該子樹合併到 \(r\)
如果要查詢的另一點沒被看過,就不做事
r
a
b
採用前序走訪 (進入就先標記該點)
dfs 某一子樹 \(a\),把該子樹合併到 \(r\)
如果要查詢的另一點沒被看過,就不做事
dfs 某一子樹 \(b\),把該子樹合併到 \(r\)
r
a
b
採用前序走訪 (進入就先標記該點)
dfs 某一子樹 \(a\),把該子樹合併到 \(r\)
如果要查詢的另一點沒被看過,就不做事
dfs 某一子樹 \(b\),把該子樹合併到 \(r\)
如果要查詢的另一點看過了,LCA 就是他的 disjoint set 的頭
void dfs(int v, int pa) {
used[v] = true;
for (int u : V[v]) {
if (u == pa)
continue;
dfs(u, v);
// 這裡的合併要保證 head 就是 lca
head[find(u)] = v;
}
for (auto [u, i] : query[v])
if (used[u])
ans[i] = find(u);
}
r
a
b
void dfs(int v, int pa) {
used[v] = true;
for (int u : V[v]) {
if (u == pa)
continue;
dfs(u, v);
// 這裡的合併要保證 head 就是 lca
head[find(u)] = v;
}
for (auto [u, i] : query[v])
if (used[u])
ans[i] = find(u);
}
int head[100]; // disjoint set
int find(int x) {
if (head[x] == x)
return x;
return head[x] = find(head[x]);
}
這裡的 find 因合併較特殊
分析後是平攤 \(O(1)\)
整體複雜度 \(O(n+Q)\)
\(\{1,2,5,2,6,2,7,2,1,3,1,4,1\}\)
一個點進去與出來構成的區間,包含子樹的所有點
能把問題轉換成 RMQ 問題!
最近共同祖先
更神奇的作法
Method of Four Russians
4個俄羅斯人法(!?),把\(O(\log n)\)壓到\(O(1)\)的技巧
點 | 1 | 2 | 5 | 2 | 6 | 2 | 7 | 2 | 1 |
---|---|---|---|---|---|---|---|---|---|
深度 | 0 | 1 | 2 | 1 | 2 | 1 | 2 | 1 | 0 |
點 | 1 | 2 | 5 | 2 | 6 | 2 | 7 | 2 | 1 |
---|---|---|---|---|---|---|---|---|---|
深度 | 0 | 1 | 2 | 1 | 2 | 1 | 2 | 1 | 0 |
A與B的LCA是:
第一個A與最後一個B構成的區間中,深度最小的點
點 | 1 | 2 | 5 | 2 | 6 | 2 | 7 | 2 | 1 |
---|---|---|---|---|---|---|---|---|---|
深度 | 0 | 1 | 2 | 1 | 2 | 1 | 2 | 1 | 0 |
觀察到剛剛的特性後,就能把LCA問題變成RMQ問題
不過有學過\(O(n)\)預處裡\(O(1)\)查詢的資料結構嗎?
st[x][y] = \(a_x\)到\(a_{x+2^y-1}\)的答案(共\(2^y\)個元素)
查詢任意範圍a~b,只要查詢
$$\min\begin{cases} st[a][c] \\ st[b-2^c][c]\end{cases} ,c=\lfloor\log_2(b-a+1)\rfloor$$
c=std::__lg(b-a+1);
點 | 1 | 2 | 5 | 2 | 6 | 2 | 7 | 2 | 1 |
---|---|---|---|---|---|---|---|---|---|
深度 | 0 | 1 | 2 | 1 | 2 | 1 | 2 | 1 | 0 |
+1 | +1 | -1 | +1 | -1 | +1 | -1 | -1 |
若\(a,b,c,d,e\)的差分序列是
\(<1,-1,-1,-1,>\)
最小值一定是\(e\)
\(O(n2^n)預處裡\)
\(O(1)查詢\)
看起來超級爛?
\(O(\frac{n}{K}+K)\)找出最小值
\(O(n)\)預處裡
點 | 1 | 2 | 5 | 2 | 6 | 2 | 7 | 2 | 1 |
---|---|---|---|---|---|---|---|---|---|
深度 | 0 | 1 | 2 | 1 | 2 | 1 | 2 | 1 | 0 |
0 | 1 | 1 | 1 | 0 |
---|
\(O(1+K)\)找出最小值
\(O(\frac{K}{n}\log \frac{K}{n} +n)\)預處裡
0 | 1 | 1 | 1 | 0 |
---|
\(O(1+1)\)找出最小值
\(O(\frac{K}{n}\log \frac{K}{n} + K2^K+ n)\)預處裡
點 | 1 | 2 | 5 | 2 | 6 | 2 | 7 | 2 | 1 |
---|---|---|---|---|---|---|---|---|---|
深度 | 0 | 1 | 2 | 1 | 2 | 1 | 2 | 1 | 0 |
\(O(\frac{K}{n}\log \frac{K}{n} + K2^K+ n)\)
K要怎麼設才能讓複雜度變好?
\(O(\frac{K}{n}\log \frac{K}{n} + K2^K+ n)\)
\(K=c\log n\)
\(O(\frac{K}{n}\log \frac{K}{n} + K2^K+ n)\)
\(K=c\log n\)
\(\frac{K}{n}\log \frac{K}{n}\\=\frac{c\log n}{n}\log \frac{c\log n}{n}\\=\frac{c\log n}{n}(\log{c\log n}-\log{n})=o(n)\)
\(O\)表示小於等於的話,\(o\)就表示小於
\(O(\frac{K}{n}\log \frac{K}{n} + K2^K+ n)\)
\(K=c\log n\)
\(K2^K\\=c\log{n}2^{c\log{n}}=c\log{n}2^{\log{n^c}}\\=c\log{n}\times n^c\)
\(O(\frac{K}{n}\log \frac{K}{n} + K2^K+ n)\)
\(K=c\log n\)
\(c\log{n}\times n^c\)
設\(c=0.5\)
\(0.5\log{n}\sqrt{n}\leq 0.5\sqrt{n}\sqrt{n}\in O(n)\)
\(O(1)\)找出最小值
\(O(n)\)預處裡
把樹分解成許多線段,查詢時把幾個線段上的答案合併
要怎麼切線段?
準備你會的資料結構
BIT / 線段樹 / Treep
不會的話先回去學資料結構...
利用 dfs 計算出必要資訊
後面步驟忘了什麼東西幾乎都加在這一步
struct info{
int w;
int dep;
int pa;
int size;
int next;
int id;
int root;
} inf[100001];
int prepare(int v, int pa, int d=0)
{
int maxsub = 0;
inf[v].pa = pa;
inf[v].dep = d;
inf[v].size = 1;
inf[v].next = -1;
for(int u:E[v])
{
if (u==pa) continue;
int sub = prepare(u, v, d+1);
inf[v].size += sub;
if ( sub > maxsub ) {
maxsub = sub;
inf[v].next = u;
}
}
return inf[v].size;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int mid = 1;
void mapTo(int v, int pa, int root)
{
inf[v].id = mid++;
inf[v].root = root;
seg.update(inf[v].id, inf[v].w);
if (inf[v].next!=-1)
mapTo(inf[v].next, v, root);
for(int u:E[v])
{
if (u==pa) continue;
if (u==inf[v].next) continue;
mapTo(u, v, u);
}
}
對於每一個點
紀錄他所在的重鏈頭是誰
要記得要改在 新邊號 的位置上!
void update(int s, int w)
{
inf[s].w = w;
seg.update(inf[s].id, inf[s].w);
}
有兩種 case
頭尾在不同重鏈上
S
T
頭尾在不同重鏈上
S
T
如果不同鏈,先算 root 最深的鏈
頭尾在不同重鏈上
S
T
如果不同鏈,先算 root 最深的鏈
頭尾在不同重鏈上
S
T
算完,把點移動到鏈 root 的 parent
頭尾在不同重鏈上
S
T
如果不同鏈,先算 root 最深的鏈
頭尾在不同重鏈上
S
T
如果不同鏈,先算 root 最深的鏈
頭尾在不同重鏈上
S
T
算完,把點移動到鏈 root 的 parent
頭尾在不同重鍊上
S
T
在同一條鏈,直接算!
重點
u
e
v : 紀錄 e 的資料
int query(int s, int t)
{
int ans = 0;
while( inf[s].root != inf[t].root )
{
if (inf[inf[s].root].dep < inf[inf[t].root].dep)
swap(s, t);
ans = margeans(ans, seg.query(inf[s].id, inf[inf[s].root].id));
s = inf[s].root;
s = inf[s].pa;
}
ans = margeans(ans, seg.query(inf[s].id, inf[t].id) );
return ans;
}
子樹的編號範圍有連續性 ! 因此可以直接區間修改
有向無環圖
DAG 有向無環圖,它的性質與他的名子一樣
討論圖論最重要的兩個特例
Tree
DAG
DAG 也有與 Tree 深度類似的特性
因為沒有環,從一個點出發後必定不會回到自己
如果一個問題計算的步驟先後順序恰好是 DAG,就容易規劃計算順序
如果對於每一個事件 x 要進行前,要先做 a,b,c...
是否能規畫一個合理的執行順序 ?
b 之前要先做 a
c 之前要先做 a
d 之前要先做 a b c
e 之前要先做 a c d
e
完成拓譜排序後
所有邊都只會由前面的點指向後面的點
DAG 的拓譜排序相當的簡單
e
使用一個 Queue 來記錄那些點的條件已經滿足了
為了方便,用一個陣列 indeg
紀錄一個點還有多少條件未達成
e
步驟1. 計算所有點的 indeg
a | b | c | d | e |
---|---|---|---|---|
0 | 1 | 1 | 3 | 3 |
e
步驟1. 計算所有點的 indeg
步驟2. 把 indeg 為 0 的點放入 Queue
a | b | c | d | e |
---|---|---|---|---|
0 | 1 | 1 | 3 | 3 |
a |
---|
e
步驟1. 計算所有點的 indeg
步驟2. 把 indeg 為 0 的點放入 Queue
while Queue 非空
p = Q.front(); 輸出 p
a | b | c | d | e |
---|---|---|---|---|
0 | 1 | 1 | 3 | 3 |
a |
---|
e
步驟1. 計算所有點的 indeg
步驟2. 把 indeg 為 0 的點放入 Queue
while Queue 非空
p = Q.front(); 輸出 p
檢查 p 的所有鄰居 v
a | b | c | d | e |
---|---|---|---|---|
0 | 1 | 1 | 3 | 3 |
a |
---|
e
步驟1. 計算所有點的 indeg
步驟2. 把 indeg 為 0 的點放入 Queue
while Queue 非空
p = Q.front(); 輸出 p
檢查 p 的所有鄰居 v
將 v 的 indeg - 1
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 2 | 2 |
a |
---|
e
步驟1. 計算所有點的 indeg
步驟2. 把 indeg 為 0 的點放入 Queue
while Queue 非空
p = Q.front(); 輸出 p
檢查 p 的所有鄰居 v
將 v 的 indeg - 1
如果 v 的 indeg 變成 0
就把 v 放到Queue
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 2 | 2 |
b | c |
---|
a |
---|
e
步驟1. 計算所有點的 indeg
步驟2. 把 indeg 為 0 的點放入 Queue
while Queue 非空
p = Q.front(); 輸出 p
檢查 p 的所有鄰居 v
將 v 的 indeg - 1
如果 v 的 indeg 變成 0
就把 v 放到Queue
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 2 | 2 |
c |
---|
a | b |
---|
e
步驟1. 計算所有點的 indeg
步驟2. 把 indeg 為 0 的點放入 Queue
while Queue 非空
p = Q.front(); 輸出 p
檢查 p 的所有鄰居 v
將 v 的 indeg - 1
如果 v 的 indeg 變成 0
就把 v 放到Queue
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 1 | 2 |
c |
---|
a | b |
---|
e
步驟1. 計算所有點的 indeg
步驟2. 把 indeg 為 0 的點放入 Queue
while Queue 非空
p = Q.front(); 輸出 p
檢查 p 的所有鄰居 v
將 v 的 indeg - 1
如果 v 的 indeg 變成 0
就把 v 放到Queue
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 1 | 2 |
c |
---|
a | b |
---|
e
步驟1. 計算所有點的 indeg
步驟2. 把 indeg 為 0 的點放入 Queue
while Queue 非空
p = Q.front(); 輸出 p
檢查 p 的所有鄰居 v
將 v 的 indeg - 1
如果 v 的 indeg 變成 0
就把 v 放到Queue
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 1 | 2 |
a | b | c |
---|
e
步驟1. 計算所有點的 indeg
步驟2. 把 indeg 為 0 的點放入 Queue
while Queue 非空
p = Q.front(); 輸出 p
檢查 p 的所有鄰居 v
將 v 的 indeg - 1
如果 v 的 indeg 變成 0
就把 v 放到Queue
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 0 | 1 |
a | b | c |
---|
e
步驟1. 計算所有點的 indeg
步驟2. 把 indeg 為 0 的點放入 Queue
while Queue 非空
p = Q.front(); 輸出 p
檢查 p 的所有鄰居 v
將 v 的 indeg - 1
如果 v 的 indeg 變成 0
就把 v 放到Queue
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 0 | 1 |
d |
---|
a | b | c |
---|
e
步驟1. 計算所有點的 indeg
步驟2. 把 indeg 為 0 的點放入 Queue
while Queue 非空
p = Q.front(); 輸出 p
檢查 p 的所有鄰居 v
將 v 的 indeg - 1
如果 v 的 indeg 變成 0
就把 v 放到Queue
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 0 | 1 |
a | b | c | d |
---|
e
步驟1. 計算所有點的 indeg
步驟2. 把 indeg 為 0 的點放入 Queue
while Queue 非空
p = Q.front(); 輸出 p
檢查 p 的所有鄰居 v
將 v 的 indeg - 1
如果 v 的 indeg 變成 0
就把 v 放到Queue
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
a | b | c | d |
---|
e
步驟1. 計算所有點的 indeg
步驟2. 把 indeg 為 0 的點放入 Queue
while Queue 非空
p = Q.front(); 輸出 p
檢查 p 的所有鄰居 v
將 v 的 indeg - 1
如果 v 的 indeg 變成 0
就把 v 放到Queue
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
e |
---|
a | b | c | d |
---|
e
步驟1. 計算所有點的 indeg
步驟2. 把 indeg 為 0 的點放入 Queue
while Queue 非空
p = Q.front(); 輸出 p
檢查 p 的所有鄰居 v
將 v 的 indeg - 1
如果 v 的 indeg 變成 0
就把 v 放到Queue
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
a | b | c | d | e |
---|
e
步驟1. 計算所有點的 indeg
步驟2. 把 indeg 為 0 的點放入 Queue
while Queue 非空
p = Q.front(); 輸出 p
檢查 p 的所有鄰居 v
將 v 的 indeg - 1
如果 v 的 indeg 變成 0
就把 v 放到Queue
a | b | c | d | e |
---|
e
如果演算法結束後,沒有輸出所有點的點
有可能代表 圖不是 DAG 或 圖不連通
e
除此方法之外
也能用一次 DFS 找出拓譜序 (的逆序)
方法也十分簡單
hint : 後序走訪
KEY : 如果目前這一步是 "先手必勝"
表示不論如何移動,都會使得 "先手必敗"
因為沒則選擇,只能選擇移動到 "先手必敗" 的狀況 (下一回合先後手交換GG)
Note : 這一個題目是 "後手做移動",如果是 "先手做移動" 結論會有點不一'樣
KEY : 如果目前這一步是 "先手必勝"
表示不論如何移動,都會使得 "先手必敗"
KEY : 如果目前這一步是 "先手必勝"
表示不論如何移動,都會使得 "先手必敗"
如果只有終點 : 先手必勝
KEY : 如果目前這一步是 "先手必勝"
表示不論如何移動,都會使得 "先手必敗"
只有通向必勝的路,因此必敗
KEY : 如果目前這一步是 "先手必勝"
表示不論如何移動,都會使得 "先手必敗"
終點前兩步 : 有路通向必敗,因此必勝
KEY : 如果目前這一步是 "先手必勝"
表示不論如何移動,都會使得 "先手必敗"
以此推出所有點是必勝還是必敗
按照拓譜排序的逆序進行
狀態A
狀態B
常見方法有
沒Bug的世界
已經發生Bug的世界
黑色邊
More practice : uva 816
網路流
今天老師請了一些同學幫忙從 302 搬運電腦硬碟到一間儲藏室存放,每位同學一次只能拿取一顆硬碟。
因為走廊通道較狹窄小,每條走廊都有通行人數的限制。
請問每單位時間最多可以運送幾顆硬碟到倉庫 ?
求取兩點間的最大運輸量
教室
倉庫
7
6
8
3
4
3
倉庫
7
3/6
8
3/3
4
3
答案 \(=3+\)
教室
倉庫
3/7
3/6
3/8
3/3
4
3/3
答案 \(=3+3\)
教室
倉庫
3/7
6/6
6/8
3/3
3/4
3/3
答案 \(=3+3+3\)
教室
3/8
在圖中,使用 \(x/y\)
代表這條邊的容量 Capacity 與流量 Flow
3/8
流量不能夠超過一條邊的容量
\(C(x,y) \geq F(x,y)\)
除了起點終點,進去的流量要等於出來的流量
\(\sum_{v\in x_{in}} F(v,x) = \sum_{w\in x_{out}} F(x,w)\)
所有到 \(x\) 的邊
所有離開 \(x\) 的邊
任兩點間流量
$$F(x,y)+F(y,x)=0$$
$$F(y,x)=-F(x,y)$$
反向流 視為 正向流取負數
5 / 6
不飽和邊,還有一單位的流量可以用
5 / 6
1 / 3
2 / 4
但這能構成最大流嗎?
因此,我們可以不斷的尋找不飽和邊的路徑,將其填充流量。
直到找不到不飽和邊路徑
3/7
6
8
3/3
3/3
3/3
隨意的流動可能會阻塞更多能流動的路徑
2/6
2
4
6/2
0+2=2
6-2=4
能夠把流錯的水,沿反方向推回去
1/1
0
1
能夠把流錯的水,沿反方向推回去
能夠把流錯的水,沿反方向推回去
0
1
能夠把流錯的水,沿反方向推回去
0
1
能夠把流錯的水,沿反方向推回去
1
0
對於下方的點,維持 進入 = 出來 的流量 !
能夠把流錯的水,沿反方向推回去
1
0
對於上方的點,維持 進入 = 出來 的流量 !
1
0
1
1
1
透過反向邊,維護最大能改變的量
在不破壞可行流的狀況下,修正答案
定義能使可行流擴張的路徑,是由下兩種邊構成的路
如果圖有可行流 \(F\) 之後,找不到由起點到匯點的增廣路徑
\(\iff\)
\(F\) 是最大流
假設圖是簡單的,且所有容量皆是非負整數
int flow = 0;
while( true )
{
if( 找到增廣路 )
{
把增廣路上的流量都+1
}
else
break;
}
利用 DFS 找增廣路
如果邊容量都是整數,每次答案至少會增加 \(1\)
如果邊長為小數,則無法保證答案必然可以收斂到最大流上
Wiki 的構造,FF 演算法會往 \(3+2r\) 收斂,而非最大流 \(2M+1\)
\(e_1=e_3=1\)
\(e_2=r\)
其中 \(r^2=1-r\)
其餘為 \(M\geq 2\)
UVA 820
求無向圖的最大流。
建立邊的時候,要把反向邊一起蓋出來
方法一、用 vector 全部存在一起
方法二、存邊陣列
vector<tuple<int,int,int>> V[101];
// x=>y 可以流 C
void add_edge(int x, int y,int c)
{
V[x].emplace_back(y, c, V[y].size());
V[y].emplace_back(x, 0, V[x].size()-1);
}
多用一個參數紀錄反向邊的位置
如果 \(x\to y\) 是單向邊,反向容量是 \(0\)
如果 \(x\to y\) 是雙向邊,反向容量是 \(c\)
雙向邊如果要蓋成4條邊其實也不是不行...
Why?
vector<int> V[101];
vector<tuple<int,int>> E;
// x=>y 可以流 C
void add_edge(int x, int y, int c)
{
V[x].emplace_back( E.size() );
E.emplace_back(y,c);
V[y].emplace_back( E.size() );
E.emplace_back(x,0);
}
反向邊的編號只要把自己的編號 xor 1 就能取得
int ffa(int s,int e)
{
int ans = 0, f;
End = e;
while(true)
{
memset(used, 0, sizeof(used));
f = dfs(s, INT_MAX); // 不同演算法找法不一樣
if( f<=0 ) break;
ans += f;
}
return ans;
}
int dfs(int v, int f)
{
if( v==End ) return f;
used[v] = true;
int e, w, rev;
for( int eid : V[v] )
{
tie(e,w) = E[eid];
if( used[e] || w==0 ) continue;
w = dfs(e, min(w,f));
if( w>0 )
{
更新流量
return w;
}
}
return 0;// Fail!
}
if( w>0 )
{
get<1>(E[eid ]) -= w;
get<1>(E[eid^1]) += w;
return w;
}
if( w>0 )
{
get<1>(t) -= w;
get<1>(V[e][rev]) += w;
return w;
}
在這一個步驟要對反向邊修改
Bipartite Matching
假設一個男生只能配一個女生,且假設一個女生只能配一個男生
紅色的邊集合是一個匹配
藍色的邊則不是
完美匹配不一定存在
OXOXO
XOXOX
反過來說呢?
UVA 193
判斷一張圖是不是二分圖
不斷找出增廣路徑,改成交錯路徑,直到找不到為止
int konig()
{
memset(match,-1,sizeof(match));
int ans=0;
for(int i=1;i<=n;++i)
{
memset(used, 0, sizeof(used));
if( dfs(i) )
ans++;
}
return ans;
}
Koning 透過逐一地把點加入集合中,檢查加入的新點是否構成增廣路徑
V[i] 記錄左半邊可以選擇右邊的哪些點
match[i] 記錄右半邊以配對到左半邊的哪個點
vector<int> V[205];
int match[205]; // A<=B
bool dfs(int v)
{
for(int e:V[v])
{
if( used[e] ) continue;
used[e] = true;
if( match[e]==-1 || dfs( match[e] ) )
{
match[e] = v;
return true;
}
}
return false;
}
總共要做 \(V\) 次DFS
總複雜度為 \(O(V(E+V))\)
UVA 10080
逃竄的地鼠問題
網路流可以很輕易的把二分圖問題轉換為一般的網路流問題來完成。
Key : 利用邊容量來限制點的度數
如果一個點要被匹配的話,只能選擇使用一條邊
Key : 利用邊容量來限制點的度數
超級起點
超級終點
Key : 利用邊容量來限制點的度數
超級起點
超級終點
1
1
1
控制左半邊deg = 1
Key : 利用邊容量來限制點的度數
超級起點
超級終點
1
1
1
控制左半邊deg = 1
1
1
1
控制右半邊deg = 1
從超級起點流到超級終點,形成的最大流就是最大匹配
超級起點
超級終點
1
1
1
1
1
1
網路流的二分途匹配,可以透過容量,自由地控制點能配對到的點數,甚至可以有三四層以上的關係,較前方法更活用
超級起點
超級終點
1
1
1
1
1
1
複雜度為 \(O((E+V)F)\),通常 \(F=O(V)\)
使用更好的網路流演算法 (Dinic) 可以到達 \(O(E\sqrt V)\)
有一個 \(N\times N\) 的西洋棋盤,想在上面放一些城堡
任兩個城堡不能在同一個直排或橫排上,而且有一些格子不能放城堡,問這一個西洋棋盤最多能放幾個城堡
簡化 hdu 1045
有一個 \(N\times N\) 的西洋棋盤,想在上面放一些城堡
任兩個城堡不能在同一個直排或橫排上,而且有一些格子不能放城堡,問這一個西洋棋盤最多能放幾個城堡
簡化 hdu 1045
如果一格城堡放在 \((a,b)\) 上,其他棋子的 \(x\) 座標就不能是 \(a\),\(y\) 座標座標就不能是 \(b\)
把座標數字當一個點
把合法座標當作邊
選擇最多邊不共用頂點
二分圖
最大匹配!
有一個 \(N\times M\) 的棋盤,上面放了 \(P\) 個棋子,每一次可以選擇一個直排或是一個橫排,把選擇到的那整排上面的棋子都拿走,問最少要幾次才能拿完所有棋子
把座標數字當一個點
把合法座標當作邊
選擇最少點
使得所有邊都至少有一個端點被選中
最小點覆蓋
最小點覆蓋數 = 2 (綠色點)
最小點覆蓋數 = 2 (綠色點)
定理 1. Kőnig's theorem 二分圖上
最小點覆蓋數 \(c\) = 最大匹配數 \(M\)
Prove
顯然的 \(c\geq M\)
因為匹配的每一個邊都不共用頂點
所以至少要 \(M\) 個點
如果能找到一個構造方法
恰好用 \(M\) 個點覆蓋全部邊
就證明完畢了
Hint: 利用匈牙利演算法
定理 1. Kőnig's theorem 二分圖上
最小點覆蓋數 \(c\) = 最大匹配數 \(M\)
給一個 \(n\times m\) 的格子花園,一些格子太溼不能種仙人掌,其他格子可以種仙人掌,但任兩個仙人掌不能相鄰。
求最多可以種幾棵仙人掌,並且把種植的方法輸出。
Hint : 相鄰的格子黑白交錯塗色
=> 二分圖 !
給一個 \(n\times m\) 的格子花園,一些格子太溼不能種仙人掌,其他格子可以種仙人掌,但任兩個仙人掌不能相鄰。
求最多可以種幾棵仙人掌,並且把種植的方法輸出。
Hint : 相鄰的格子黑白交錯塗色
=> 二分圖 !
選最多的點
使得點都不相臨
(一個邊只有一端點被選到)
最大點獨立集
最大點獨立集數 = 4 (橘色點)
定理 2. 二分圖上
最大點獨立集 \(P\) = 總點數 \(n-\) 最大匹配數 \(M\)
定理 3. (補充) 二分圖上
最大團 \(C\) = 補圖最大獨立集 \(M\)
最大獨立集 : 任兩點兩兩不相臨
最大團 : 任兩點兩兩相臨
補圖 :
有邊變成沒邊
沒邊變成有邊
今天有一個有向無環圖 (DAG)
有一些人要在 DAG 上跑步
任兩人跑步路徑不能相交
如果要讓 DAG 所有的頂點都被經過一遍,最少要幾人
hint 對每一個點 \(v\) 建立 \(v_{in}, v_{out}\)
=> 二分圖
HW
FF的優化
1000000000
1000000000
1000000000
1000000000
1
1000000000
1000000000
1000000000
1000000000
1
\(F=1\)
1000000000
1000000000
1000000000
1000000000
1
\(F=2\)
1000000000
1000000000
1000000000
1000000000
1
\(F=3\)
經過很久以後
1000000000
1000000000
1000000000
1000000000
1
\(F=2000000000\)
FFA隨意地亂找道路,不一定每次都挑到適合的道路
要如何改進?
用BFS找增廣路徑
int flow = 0;
while( true )
{
if( BFS 找到增廣路 )
{
擴充增廣路上的流量
}
else
break;
}
用 BFS 找增廣路徑,每次的增廣路路徑長都是最短的
如果找到長度是 P 的路,那就不存在比 P 更短的增廣路
每一次更新,至少讓一個點 BFS 距離變遠
\(O((E+V)EV)=O(E^2V)\)
int flow = 0;
while( 做出 BFS 層次圖 )
{
while( 在 BFS 層次圖 找到增廣路 )
{
擴充增廣路上的流量
}
}
7
6
8
3
4
3
7
6
8
3
4
3
深度0
深度1
深度2
6
8
3
3
深度0
深度1
深度2
7
3/6
8
3/3
4
3
7
3/6
8
3/3
4
3
深度0
深度1
深度2
深度3
3/7
3/6
3/8
3/3
4
3/3
深度0
深度1
深度2
深度3
3/7
6/6
6/8
3/3
3/4
3/3
深度0
深度1
深度2
深度3
3/7
6/6
6/8
3/3
3/4
3/3
$$O(\operatorname{min}(V^{2/3},E^{1/2})\times E)$$
故使用Dinic解二分圖最大匹配複雜度是\(O(E\sqrt{V})\)
比 Kőnig 或一般 Flow \(O(EV)\)快
特例
int dinic(int s,int e)
{
int ans = 0, f;
End = e;
while(bfs(s))
{
while( f = dfs(s, INT_MAX) )
ans += f;
}
return ans;
}
int End, dist[101];
bool bfs(int s) {
memset(dist, -1, sizeof(dist));
queue<int> qu;
qu.emplace(s);
dist[s]=0;
while( !qu.empty() ) {
int S = qu.front(); qu.pop();
for(auto &p : V[S]) {
int E, C;
tie(E, C, ignore) = p;
if( dist[E]==-1 && C!=0 ) {
dist[E]=dist[S]+1;
qu.emplace(E);
}
}
}
return dist[End] != -1;
}
int dfs(int v, int f) {
int e,w,rev;
if( v==End || f==0 ) return f;
for( auto &t : V[v] )
{
tie(e,w,rev) = t;
if( dist[e]!=dist[v]+1 || w==0 )
continue;
w = dfs(e, min(w,f));
if( w>0 )
{
get<1>(t) -= w;
get<1>(V[e][rev]) += w;
return w;
}
}
dist[v] = -1; //優化,這個點沒用了
return 0;// Fail!
}
網路流原來是單純用 dfs / bfs 找通路
改成用 最短路徑 找通路就是最小費用流!
邊的長度 = 費用
因為有負邊問題,只能使用 SPFA 等來找最短路
Match for general graph
一般圖存在奇環,二分圖全為偶環
在二分圖中,如果遇到可以匹配的新點,就會把路徑反轉
在一般圖中,配對兩點不保證可以二分,會產生矛盾
GG
定義Root是偶點
定義Root是偶點
花被演算法
因為花被的英文 Perianth 生澀
所以 Edmonds 使用 Blossom 來借代
花被算法提供了一個解決奇環的方法
假定所有的Match都是由偶點出發的
大陸奇怪翻譯:帶花樹算法!?
Root
Root
Root
找完奇環的Match後,收縮奇環到Root成為新圖
Root