圖論
Overview
拓樸排序
換根 DP
DFS tree
強連通分量 (SCC)
雙連通分量 (BCC)
2-sat
圖論雜題
我最近真的沒啥刷題
所以今年多丟的題目主要會是 OI 題
想要 vir 的自行迴避
警告
拓樸排序
有人說是拓撲
但這種 ikea 式的問題我們就念 topo 就沒問題了
P.S. 原本要在複賽前講的,但我忘了
還記得什麼是 DAG 嗎
有向無環圖
拓樸排序就是在 DAG 上找到一個頂點的排列順序讓以下條件成立:
- 對於所有邊 u -> v,u 在 v 之前
為什麼你找的到 => 數歸
還記得什麼是 DP 嗎
當前的問題要被分解成更小的問題求解
更小的問題要先被解好 => 沒有環
假設你把 DP 的所有狀態都當成圖上的一個點,每個轉移都是一條有向邊,那理論上他不會有環
反過來想,你可以在 DAG 上 DP
要怎麼做?假設你有拓樸排序
並且按照拓樸排序的順序 DP
那就可以保證在考慮到一個點的時候所有需要先被考慮的點都被考慮過了
實作
開一個 queue 維護目前入度為 0 的點的集合,每次把一個點 pop 出來並檢查哪些新的點的入度會變 0
延伸
要給每個點 label,對於 u -> v,u 的 label 要比 v 的 label 小
求 label 陣列的最小字典序
要讓字典序最小的目標
變成讓 1 盡早出現,接下來讓 2 盡早出現...
看起來不太好正著做(?
反著做呢,考慮誰要當 label N
label N 的一定是出度為 0 的 id 最大的點
因為要是這個點的 label = X,把 X, X+1, ..., N cyclic shift 一下會得到更好的字典序,至於正確性自己想一下
是說這東西怎麼可能是經典題
樹上拓樸排序
對於每條被刪的邊,兩端點在被刪掉時的奇偶性會相同,兩端點被刪掉時都是奇數的叫做奇邊,偶數叫做偶邊
假設一個點的 deg 是 d
不管 d 是奇數或是偶數,刪除序列會是
... -> 偶邊 -> 奇邊 -> 偶邊 -> 奇邊
=> 我們可以看出來點往父親的邊是奇邊還是偶邊 (或無解)
假設我們知道每條邊是奇邊還是偶邊了,那麼對於一個點,我們只要確保所有經過這個點的邊的順序是對的就好了
把奇邊跟偶邊分別隨便排,然後建邊 -> 邊的圖
a -> b 代表 a 必須排在 b 的前面,可以發現他是 acyclic 的
最後跑一遍拓樸排序就好了
習題
說句笑話,打比賽記得按 rated
SCC
一個有向圖是 SCC iff 任意兩點皆能互相到達
Tarjan!
你們以後會一直看到他
Tarjan
考慮 dfs tree
對於每個點維護 dfn 表示 dfs 序
low 表示從當前的點的子樹可以走到的最小 dfs 序的點
dfn == low <=> 是 SCC 中第一個遍歷到的點
證明
dfn != low 代表從他開始可以走到已經走過的點 -> 可以往上走 -> SCC 裡面有人比他更早遍歷過
實作
用 stack 維護當前的 dfs 序跟哪些點還沒有走過
在判 low 的時候要注意要記得判是不是已知這個點的 SCC,如果是的話就不要更新 low
Code
vector<vector<int> > e;
vector<int> dfn,low,tt;
stack<int> s;
int cnt=1,cc=0;
void dfs(int u){
dfn[u]=low[u]=cnt;
cnt++;
s.push(u);
for(auto v:e[u]){
if(!tt[v]){
if(dfn[v]){
low[u]=min(low[u],dfn[v]);
}
else{
dfs(v);
low[u]=min(low[u],low[v]);
}
}
}
if(dfn[u]==low[u]){
cc++;
while(u!=s.top()){
tt[s.top()]=cc;
s.pop();
}
tt[u]=cc;
s.pop();
}
}
//tt[] = SCC
題目
我不會,自己看
BCC (Edge)
一條邊是 bridge 代表把他拔掉之後的連通塊數量會增加
一張圖是 BCC 代表他連通並且沒有橋
在此討論無向圖
同樣考慮 dfs tree
可以發現 back edge 不會是 bridge
=> 只要考慮 tree edge
可以發現一條 tree edge 是 bridge 的充要條件是 low[下面那個點] = dfn[下面那個點]
維護 dfn 跟 low 陣列
Extra: 找到每個點分別在哪個 BCC 裡面
用 stack!
vector<vector<int> > e;
vector<int> dfn,low;
vector<pair<int,int> > bridge;
stack<int> s;
int cnt=1,cc=0;
void dfs(int u,int f){
dfn[u]=low[u]=cnt;
cnt++;
s.push(u);
for(auto v:e[u]){
if(v==f){
continue;
}
if(dfn[v]){
low[u]=min(low[u],dfn[v]);
}
else{
dfs(v,u);
low[u]=min(low[u],low[v]);
if(low[v]==dfn[v]){
bridge.push_back({u,v});
}
}
}
}
Code
BCC (Vertex)
一個點是割點代表拔掉他之後的連通塊數量會增加
一張圖是 BCC (Vertex) 代表連通且沒有割點
一樣只考慮無向圖
一樣是 dfs tree 上面維護 dfn 跟 low
對於一個點 v 是割點
- 如果 v 不是根,代表說他有一個小孩 u 使得 low[u] >= dfn[v]
- 如果 v 是根,代表小孩個數 >= 2
vector<vector<int> > e;
vector<int> dfn,low;
vector<pair<int,int> > bridge;
stack<int> s;
int cnt=1,cc=0;
void dfs(int u,int f=-1){
dfn[u]=low[u]=cnt;
cnt++;
s.push(u);
int ch=0;
for(auto v:e[u]){
if(v==f){
continue;
}
if(dfn[v]){
low[u]=min(low[u],dfn[v]);
}
else{
dfs(v,u);
low[u]=min(low[u],low[v]);
if(f!=-1 && low[v]>=dfn[u]){
IS_CUTPOINT(v);
}
ch++;
}
}
if(f==-1 && ch>=2){
IS_CUTPOINT(v);
}
}
Code
他為什麼不講圓方樹阿
圓方樹
假設把 BCC 的割點叫做方點,BCC 本身叫做圓點,可以發現會形成一棵樹,其中相鄰的點的形狀不相同
然後你就做樹上會做的事情就好了
實作細節
挺重要的小知識
- 假設你做完 tarjan,之後要蓋一張新的圖,可以開一個 vis 陣列去看當前的 ?CC 的邊然後 rollback,如果不這樣做的話可能會多一個 log
- 圓方樹不好寫,我喜歡直接多建一棵樹出來
題目
2-SAT
你有 n 個條件,每個條件是形如 \( ( A \lor B )\) 的形式,問你有沒有辦法透過決定變數的值使每個條件皆成立
對於每個變數建 \(A\) 與 \(\neg A\) 兩個點
對於每個條件 \( A \lor B \) 建 \( \neg A \rightarrow B \) 與 \( \neg B \rightarrow A \) 兩條邊,代表 A 不成立的話 B 就必須成立,另一邊也是這樣
來建圖吧
可以發現從一個 A 開始,能走到的點都會必須成立
代表說只要 A 能走到反 A 且 反 A 能走到 A
整個邏輯式就無解
判兩個點是不是互相到達 -> SCC!
跑一次 tarjan,求出是不是有 A 跟反 A 在同一個 SCC 裡面,有的話就輸出無解
否則剩下的圖一定是 DAG -> 拓樸排序
假設 A 的拓樸排序在 反 A 前面 -> 反 A 一定不能走到 A
所以要輸出每個變數的值的話就是看 A 跟反 A 哪個比較後面就好
Code 跟模板題
#include <bits/stdc++.h>
#pragma GCC optimize("Ofast")
#define AquA cin.tie(0);ios_base::sync_with_stdio(0);
#define fs first
#define sc second
#define p_q priority_queue
using namespace std;
// using ~ as neg
// 2x -> true, 2x+1 -> false
struct TS{
vector<vector<int> > v;
vector<int> low,dfn,tt,s;
int n;
int cnt=0,cc=0;
inline void init(int x){
n=2*x;
v.resize(n);
low.resize(n);
dfn.resize(n);
tt.resize(n);
}
int addvar(){
v.push_back(vector<int>());
low.push_back(0);
dfn.push_back(0);
tt.push_back(0);
n++;
return v.size()-1;
}
inline void add(int a,int b){
a=max(2*a,-1-2*a);
b=max(2*b,-1-2*b);
v[a].push_back(b);
}
inline void addor(int a,int b){
a=max(2*a,-1-2*a);
b=max(2*b,-1-2*b);
v[a^1].push_back(b);
v[b^1].push_back(a);
}
inline void set(int a){
addor(a,a);
}
inline void addand(int a,int b){
set(a);
set(b);
}
inline void imply(int a,int b){
addor(~a,b);
}
inline void same(int a,int b){
imply(a,b);
imply(~a,~b);
}
inline void addxor(int a,int b){
imply(~a,b);
imply(a,~b);
}
void atMostOne(const vector<int>& li){
if(li.size()<=1){
return;
}
int cur=~li[0];
for(int i=2;i<li.size();i++){
int next=addvar();
addor(cur,~li[i]);
addor(cur,next);
addor(~li[i],next);
cur=~next;
}
addor(cur,~li[1]);
}
void dfs(int r){
cnt++;
dfn[r]=low[r]=cnt;
s.push_back(r);
for(auto h:v[r]){
if(!tt[h]){
if(dfn[h]){
low[r]=min(low[r],dfn[h]);
}
else{
dfs(h);
low[r]=min(low[r],low[h]);
}
}
}
if(dfn[r]==low[r]){
cc++;
while(s.back()!=r){
tt[s.back()]=cc;
s.pop_back();
}
tt[r]=cc;
s.pop_back();
}
}
int solve(){
for(int i=0;i<n;i++){
if(!tt[i]){
dfs(i);
}
}
int flag=1;
for(int i=0;i<n/2;i++){
if(tt[2*i]==tt[2*i+1]){
flag=0;
}
}
return flag;
}
//return 0-1 array, 1 -> true, 0 -> false
vector<int> print(){
vector<int> re(n/2);
for(int i=0;i<n/2;i++){
if(tt[2*i]<tt[2*i+1]){
re[i]=1;
}
}
return re;
}
};
int main(){
AquA;
return 0;
}
題目
所以來做題吧
DSU
也許多一個資料結構(?
再多一點點東西(?
Cycle Basis
要怎麼表示一個圖的所有環
其實他沒有這麼毒瘤,真正的 cycle basis 似乎挺複雜的
但是高中大概記得貝祖跟一些線性獨立就夠用了
習題
以下含有 OI
估構題
沒,就我花了一個晚上跟某金牌吵這個複雜度是好的
P.S. 有很漂亮的兩個 log 的作法,但是我賽中用了 virtual tree 弄成一個 log 然後寫爛所以還是兩個 log
難得的 APIO 好題
其實這題不難而且應該不能 vir
如果你沒去過 IOIC 那你該去了
如果今年沒有講 patterns 再說
以下是想推的題
如果你沒有辦法看到記得加 group
有機會是整份簡報最難的一題,雖然這是我賽中唯一會的題目
進階圖論-1
By alvingogo
進階圖論-1
- 485