圖論(二)

建國中學 陳仲肯

「meow」by蔡俊則

目錄

  • 最小生成樹
  • DFS TREEEEEEE
  • 關節點
  • SCC
  • Tarjan
  • Kosaraju
  • BCC(點)
  • BCC(邊)
  • 2-SAT
  • 噁心的題目們

最小生成樹

MST Minimum Spanning Tree

定義

對於所有連通圖,點數與原圖相同、邊數最少的連通子圖

一定是一棵樹,稱為生成樹

邊權總和最小的生成樹即稱為最小生成樹

利用性質

一個連通圖內兩個未完成的最小生成樹

用最短的邊連接一定最好

Kruskal演算法

每次找當前權重最小的邊,

看端點是否在兩個不同的最小生成樹中,

是的話就把兩棵樹合併起來。

如何合併?

還記得並查集嗎?

複雜度

把所有邊排序,所有邊看過一次並查集

\(O(E log E)\)

struct E{int u,v,w;};
bool cmp(E a,E b){return a.w<b.w;}
E edge[N];
int Kruskal(){
    sort(edge, edge+m, cmp);
    int ans=0;
    for(int i=0;i<m;i++){
        if(fa(edge[i].u)!=fa(edge[i].v)){
            ans+=edge[i].w;
            join(edge[i].u, edge[i].v);
        }
    }
    return ans;
}

Prim's Algorithm

跟Dijkstra類似

把「與原點距離」改成「與樹距離」

複雜度

pq的話跟dijkstra滿像的,每條邊push一次,對每個點pop一次

  • priority queue:\(O((E+V)log V)\)
  • 用矩陣存:\(O(V^2)\)
struct E{int v,w;};
struct cmp{bool operator()(E a,E b){return a.w>b.w;}};
priority_queue<E,vector<E>,cmp> pq;
pq.push({1,0});
int ans=0;
dis[1]=0;//到樹距離
while(!pq.empty()){
	E cur=pq.top();pq.pop();
	if(vis[cur.v])continue;
	vis[cur.v]=1;
	ans+=cur.w;
	for(auto [v,w]:G[cur.v]){
		if(!vis[v]&&dis[v]>w){
			dis[v]=w;
			pq.push({v,w});
		}
	}
}

耍笨

我在寫Dijkstra時被Prim混淆,以為是存到樹距離

題目:TIOJ 1211

裸題

最小比率生成樹

每條邊有a,b兩種權重,求生成樹中\(\frac{\sum{a}}{\sum{b}}\)最小的

好像不能直接做?

解法

二分搜可以湊出來的最小比率!

如果我希望有一棵生成樹\(\frac{\sum{a}}{\sum{b}}<r\)

則\(\sum{a}-r\sum{b}<0\)

把每個邊的邊權設為\(a-rb\)

找最小生成樹,如果總和<0就可以

複雜度

做\(O(logC)\)次MST

C是\(值域\div精度\)

\(O((E+V)logElogC)\)

或\(O(ElogElogC)\)

稍微有點難的題

與最小比率無關www

OJDL 7135

from YTP

DFS Tree

  • Tree edge: 走到兒子的邊
  • Back edge: 走到祖先的邊
  • Forward edge: 走到非兒子的子孫的邊
  • Cross edge: 走到非直系血親的邊

邊的種類

whut it look like

關節點

for無向圖

我是沉重肯醫師, 我信賴普拿疼

圖的膝蓋,拔掉會使圖不連通的

定義

如何找關節點

Tarjan

讀作:塔樣

DFS TREEEEEEE

無向圖的DFS TREEEEEEE

Tarjan的想法

對於一個不是根點的u,有任何一個小孩子樹裡所有back edge的終點都在u的子樹裡,則u是關節點

 

對於根點,如果dfs tree 上小孩個數>=2

就是關節點
e.g. 賴賴跟Wiwi有兩個小孩(小賴跟小Wi)

u

u的祖先

找關節點?

\(low_i:=i\)的子樹中的back edge的\(dfs\)序最小值

int cnt=0;
int low[N],in[N];
vector<int> G[N];
void dfstreeeeee(int u,int fa=-1){
    low[u]=in[u]=++cnt;
    int child=0;
    for(int v:G[u]){
        if(v==fa)continue;
        if(in[v])low[u]=min(low[u],in[v]);//u到v是back edge
        else{
            dfstreeeeee(v,u);
            if(fa!=-1&&low[v]>=in[u])IS_CUTPOINT(u);//u把v和fa分開
            ++child;low[u]=min(low[u],low[v]);
        }
    }
    if(fa==-1&&child>1)IS_CUTPOINT(u);
}

for無向圖

橋?

橋牌?

定義

拔掉會使圖不連通的

如何找橋

Tarjan

讀作:塔樣

DFS TREEEEEEE

小性質

Back edge不會是橋

看Tree edge就好

Tarjan的想法

對於一個點u如果他兒子v的子樹裡所有 back edge的終點都在子樹裡

則u--v是bridge

u

u的兒子

找橋?

\(low_i:=i\)的子樹中的back edge的\(dfs\)序最小值

int cnt=0;
int low[N],in[N];
vector<int> G[N];
void dfstreeeeee(int u,int fa=-1){
    low[u]=in[u]=++cnt;
    int child=0;
    for(int v:G[u]){
        if(v==fa)continue;
        if(in[v])low[u]=min(low[u],in[v]);//u到v是back edge
        else{
            dfstreeeeee(v,u);
            low[u]=min(low[u],low[v]);
            if(low[v]==in[v])IS_BRIDGE(u,v);
        }
    }
}

如何描述有向圖的連通狀況?

弱連通、強連通

這兩個名詞都只適用於有向圖

  • 弱連通:
    • 把邊都改成無向邊後連通
    • 對於每個點對\(u,v\)存在\(u\to v\)或\(v\to u\)
  • 強連通:對於每個點對\(u,v\)存在\(u\to v\)和\(v\to u\)

this is SCC

this is not SCC

Tarjan

讀作:塔樣

DFS TREEEEEEE

作法

如果一個點的子樹經過back edge最高都只能到他自己則他是SCC的頭

詳細作法

開一個stack,存現在有可能在SCC的點

DFS時,看完相鄰的點,如果low[u]==in[u],則stack中直到自己的點都在同一個SCC,pop掉

 

每個點只會在一個SCC中,所以在最高點處理沒問題

遇到四種邊會怎樣?

  • Tree edge:       vis=0繼續DFS,用low更新low值
  • Back edge:      vis=1,inStack,用vis更新low值
  • Forward edge:vis=1,inStack or !inStack,反正看過了,沒差
  • Cross edge:     vis=1,!inStack的話不做事,inStack的話用vis更新low值
void dfs(int u){
    low[u]=in[u]=cnt++;
    stk.push(u);
    for(int v:G[u]){
        if(!vis[v]){
            dfs(v);
            low[u]=min(low[u],low[v]);
       	}else if(!scc[v]){
            low[u]=min(low[u],in[v]);
        }
    }
    if(low[u]==in[u]){
        int cur;
        do{
            cur=stk.top();stk.pop();
            scc[cur]=scctot;
        }while(cur!=u);
        ++scctot;
    }
}

縮點?

把每個SCC當作一個點,邊照連

縮點完會是DAG(有向無環圖)

PROOF:有環的話就應該要縮起來

力提

TIOJ 1981

SCC縮點,DAG DP好題

有點難

需要一點巧思

多設一個點0,代表任何點

有傳送站的可以到0

0可以到任何點

然後就跟上一題一樣了

Kosaraju

另一個找SCC的演算法

Kosaraju!

作法

做一次DFS,紀錄離開節點的順序

從後離開的開始對反向圖再DFS一次,能到的點就在同一個SCC

圖例

正確性

考慮u,v間的路徑

  • u--->v  且 v--->u 不管正的誰先,反的一定能走到
  • u-x->v 且 v-x->u顯然不會放在同SCC(反的走不到)
  • u--->v 且 v-x->u 反圖(u-x->v 且 v--->u)DFS一定要先看u,而正的不可能先離開u,因為一定會先離開v或本來就在v後面

建反向邊

DFS, 紀錄出來順序

void dfs(int u){
    if(vis[u])return;
    vis[u]=1;
    for(int v:G[u])dfs(u);
    out.push(u)
}

照出來順序對反向圖DFS, 能走到的都在同一個SCC

void bfs(int u){
    if(scc[u])return;
    scc[u]=cnt;
    for(int v:R[u])bfs(v);
}

然後就做完了耶

好像其實超好寫

但Tarjan常數比較小?

完整版的扣

#include <bits/stdc++.h>
#define pb push_back
using namespace std;
int cnt=0;
const int N=1e5+10;
bool vis[N];
int scc[N];
vector<int> G[N],R[N];
stack<int> out;
void bfs(int u){
    if(vis[u])return;
    vis[u]=1;
    for(int v:R[u])bfs(v);
    out.push(u);
}
void dfs(int u){
    if(scc[u])return;
    scc[u]=cnt;
    for(int v:G[u])dfs(v);
}
int main(){
    int n,m,a,b,u;
    cin>>n>>m;
    while(m--){
        cin>>a>>b;
        G[a].pb(b);
        R[b].pb(a);
    }
    for(int i=1;i<=n;i++)bfs(i);
    while(!out.empty()){
        u=out.top();
        out.pop();
        if(scc[u])continue;
        cnt++;
        dfs(u);
    }
    cout<<cnt<<"\n";
    for(int i=1;i<=n;i++){
        cout<<scc[i]<<" ";
    }
    return 0;
}

力提

就上面那個啊

by謝一

TIOJ 1451

卦傳播系統

(謝一:「我以前的扣居然長這樣?!」)

#include <bits/stdc++.h>
using namespace std;
int cnt, sccn, scc[100010];
bool vis[100010], in[100010];
pair<int, int> past[100010];
vector<int> road[100010];
vector<int> backway[100010];
void dfsb(int now){
    past[now].second = now;
    vis[now] = true;
    for(int i = 0; i < backway[now].size(); i++){
        if(!vis[backway[now][i]]) dfsb(backway[now][i]);
    }
    past[now].first = cnt;
    cnt++;
}
void dfs(int now){
    vis[now] = true;
    scc[now] = sccn;
    for(int i = 0; i < road[now].size(); i++){
        if(!vis[road[now][i]]){
            dfs(road[now][i]);
        }
    }
}
int main(){
    int n, m, a, b, ans;
    cin >> n >> m;
    past[0] = {0, 0};
    ans = 0;
    for(int i = 1; i <= n; i++){
        road[i].clear();
        backway[i].clear();
        vis[i] = false;
    }
    for(int i = 0; i < m; i++){
        cin >> a >> b;
        road[a].push_back(b);
        backway[b].push_back(a);
    }
    cnt = 1;
    for(int i = 1; i <= n; i++){
        if(!vis[i]) dfsb(i);
    }
    sort(past + 1, past + n + 1);
    memset(vis, false, n + 1);
    sccn = 0;
    for(int i = n; i > 0; i--){
        if(!vis[past[i].second]){
            dfs(past[i].second);
            sccn++;
        }
    }
    memset(in, true, sccn);
    for(int i = 1; i <= n; i++){
        for(int j = 0; j < road[i].size(); j++){
            if(scc[road[i][j]] != scc[i]) in[scc[road[i][j]]] = false;
        }
    }
    for(int i = 0; i < sccn; i++){
        if(in[i]) ans++;
    }
    cout << ans << "\n";
    return 0;
}

點雙連通分量(BCC)

不存在關節點的連通分量

Tarjan

讀作:塔樣

DFS TREEEEEEE

一個點可能在多個BCC中

但一條邊一定會且只會在一個BCC中

點雙連通分量可以把邊分群!

怎麼找?

用找關節點的方法+紀錄SCC的方法

找到時把stack裡的pop掉

記得考慮關節點

實作

int low[N],tin[N],id[N],t,cnt;
vector<int> bcc[N],G[N];
stack<int> s;
void dfs(int u,int p=0){
    low[u]=tin[u]=++t;
    s.push(u);
    for(int v:G[u])if(v!=p){
        if(tin[v])low[u]=min(low[u],tin[v]);
        else{
            dfs(v,u);
            if(low[v]>=tin[u]){
                cnt++;
                int k;
                do{
                    k=s.top();s.pop();
                    bcc[k].pb(cnt);
                }while(k!=v);
                bcc[u].pb(cnt);
            }
            low[u]=min(low[u],low[v]);
        }
    }
}
bool iscut[N];
vector<int> T[N];
void build(){
    dfs(1);
    for(int i=1;i<=n;i++){
        if(bcc[i].size()>1){
            id[i]=++cnt;
            iscut[id[i]]=true;
            for(int j:bcc[i]){
                T[id[i]].PB(j);
                T[j].PB(id[i]);
            }
        }else{
            id[i]=bcc[i][0];
        }
    }
}

如果一個點被多個BCC覆蓋,那他是關節點

Block-cut tree

把每個BCC縮點,中間用關節點連起來

對於有些問題,於把圖的問題變成樹的問題會大大簡化問題

以上我一題都沒寫過

我弱,對不起

橋雙連通分量(也是BCC)

不存在橋的連通分量

Tarjan

讀作:塔樣

DFS TREEEEEEE

跟點的大致雷同

邊不一定都在BCC中

但一條點一定會且只會在一個BCC中

點BCC是把邊分群

邊BCC是把點分群

void dfs(int u,int be=-1){//be: edge from last vertex
    ++cnt;
    vis[u]=low[u]=cnt;
    stk.push(u);
    for(int e:G[u]){
        if(e==be)continue;
        int v=E[e];
        if(!vis[v]){
            dfs(v,e^1);
            low[u]=min(low[u],low[v]);
            if(low[v]>vis[u]){//u-v is bridge
                ++bcctot;
                int cur;
                do{
                    cur=stk.top();stk.pop();
                    bcc[bcctot].pb(cur);
                }while(cur!=v);
            }
        }
        else if(vis[v]<vis[u])low[u]=min(low[u],low[v]);
    }
}

要仔細判斷重邊

縮點

一定會是樹(沒有環)

對於有些問題,於把圖的問題變成樹的問題會大大簡化問題

以上我一樣一題都沒寫過

我弱,對不起

2-SAT

不知道講不講得到這裡

(a \lor b) \land (-a \lor c) \land (c \lor -b)

你想找到有沒有解

(a \lor b) \land (-a \lor c) \land (c \lor -b)

建圖

-a \to b \\ -b \to a \\
a \to c \\ -c \to -a \\
-c \to -b \\ b \to c \\

如何判斷

看 \(x\) 跟 \(-x\) 是不是在同一個 \(SCC\) 裡面就好了

構解

拓樸排序 從後面開始選

#include <bits/stdc++.h>
#define pb push_back
using namespace std;
const int N=1e5+10;
int cnt=0;
bool ans[N],vis[2*N];
int scc[2*N],in[2*N];
vector<int> G[2*N],R[2*N],S[2*N],C[2*N];
stack<int> out,ord;
void bfs(int u){
    if(vis[u])return;
    vis[u]=1;
    for(int v:R[u])bfs(v);
    out.push(u);
}
void dfs(int u){
    if(scc[u])return;
    scc[u]=cnt;
    for(int v:G[u])dfs(v);
}
void topological_sort(int n){
    int u;
    bool ok;
    queue<int> Q;
    for(int i=1;i<=cnt;i++){
        if(!in[i])Q.push(i);
    }
    while(!Q.empty()){
        u=Q.front();
        Q.pop();
        ord.push(u);
        for(int v:S[u]){
            in[v]--;
            if(!in[v])Q.push(v);
        }
    }
    while(!ord.empty()){
        u=ord.top();
        ord.pop();
        ok=1;
        for(int v:C[u]){
            if(vis[v/2])ok=0;
        }
        if(!ok)continue;
        for(int v:C[u]){
            ans[v/2]=v&1;
            vis[v/2]=1;
        }
    }
}
signed main(){
    int n,m,a,b,u;
    char wa,wb;
    cin>>m>>n;
    while(m--){
        cin>>wa>>a>>wb>>b;
        a<<=1,b<<=1;
        if(wa=='+')a++;
        if(wb=='+')b++;
        G[a^1].pb(b);
        G[b^1].pb(a);
        R[b].pb(a^1);
        R[a].pb(b^1);
    }
    for(int i=2;i<=2*n+1;i++){
        bfs(i);
    }
    while(!out.empty()){
        u=out.top();
        out.pop();
        if(!scc[u])cnt++;
        dfs(u);
    }
    for(int i=1;i<=n;i++){
        if(scc[2*i]==scc[2*i+1]){
            cout<<"IMPOSSIBLE";
            return 0;
        }
    }
    for(int i=2;i<=2*n+1;i++){
        C[scc[i]].pb(i);
        for(int v:G[i]){
            if(scc[v]==scc[i])continue;
            S[scc[i]].pb(scc[v]);
            in[scc[v]]++;
        }
        vis[i/2]=0;
    }
    topological_sort(n);
    for(int i=1;i<=n;i++){
        cout<<(ans[i]?"+":"-");
    }
    return 0;
}

力提

就上面那個啊

課後問題: 想一下3-SAT怎麼做

抄來的一大堆題

劉澈給的題目都不是圖論的...

From 蕭梓宏

TIOJ 1484

CF 487 E

Made with Slides.com