圖論

fatman

目錄

  • 圖的定義及名詞

  • 遍歷,拓樸排序

  • 歐拉路徑,迴路

  • 最短路

  • 併查集

  • 最小生成樹

  • 連通性

  • 連通分量

  • 差分約束

  • 2-SAT

參考連結

講師

fatman

資安大施

密碼:剩你不知道

cses圖論題解

cses Advanced Technique題解

圖的定義

點和邊構成的集合,用G(V,E)表示\\ V代表點\\ E代表邊

邊(\(E\))

表示:(u,v)代表一條u和v之間的邊\\ 方向:分為有向及無向,有向會用箭頭表示\\ 邊權:邊上的權重\\ 重邊:對同一個(u,v)重複建邊\\ 自環:一條邊(u,v)中的u和v相同\\

點(\(V\))

度數:點上接的邊數\\ (有向圖會分為入度及出度)\\ 點權:點上的權重\\

路徑

  • 路徑:點與點間用邊連接的路

  • 簡單路徑:一條不經過重複點的路徑

  • 迴路:起點終點一樣的路徑

  • 環:起終點相同的簡單路徑

圖的分類

  • 簡單圖:沒有重邊自環的圖

  • DAG:有向無環圖

  • 連通圖:全部點對間都存在至少一條路徑(通常用於無向圖)

  • 完全圖:兩兩點對間接連邊

  • 二分圖:可以將點分為兩部分,單一部分內互相沒有邊

存圖

constexpr int maxN=...;

{//鄰接矩陣

//存圖
int G[maxN][maxN];

//加邊
G[u][v]=1;

//使用
for(int i = 0;i<maxN;i++){
	int to=g[from][i];
    dfs(to);
}

}

{//鄰接串列

//存圖
vector<int> adj[maxN];

//加邊
adj[u].emplace_back(v);

//使用
for(int to:adj[from])dfs(to);

}

例題

遍歷

DFS(深度優先搜尋)

constexpr int maxN=...;

vector<int> adj[maxN];

bitset<maxN> vis;

void dfs(int now){
	vis[now]=1;
    for(int i:adj[now])if(!vis[i])dfs(i);
}

遍歷

BFS(廣度優先搜尋)

constexpr int maxN=...;

vector<int> adj[maxN];

bitset<maxN> vis;

queue<int> q;

inline void bfs(int s){
	vis[q.emplace(s)]=1;
    for(int tmp;!q.empty();){
    	tmp=q.front(),q.pop();
        for(int i:adj[tmp])if(!vis[i])
        	vis[q.emplace(s)]=1;
    }
}

歐拉路徑

問題

給你一張圖,要你構出一組路徑從1走到n並經過全部邊(點可重複)

\(N \le 10^5, M \le 2 \times 10^5\)

存在條件?

 

除了起終點外,全部點的度數是偶數

起終點則要奇偶相同

歐拉路徑

正名

talaw guagua

 

例題

例題

拓樸排序

例題

大意: 給你\(n\)個工作,並給你\(m\)個工作間順序的限制,問你能不能做完全部工作並輸出任一個順序

 

可以把事件想成點,限制想成一條邊,每次去做已經沒有限制的事件,並把他所限制出去的邊拔掉

拓樸排序

看圖

拓樸排序

看圖

拓樸排序

看圖

拓樸排序

看圖

拓樸排序

看圖

拓樸排序

看圖

拓樸排序

看圖

拓樸排序

看圖

拓樸排序

看圖

拓樸排序

看圖

整個流程就是這樣

1.找到沒有限制的點

2.拔他的出邊

3.去看他拔掉的邊對到的點還有沒有限制

拓樸排序

看圖

整個流程就是這樣

1.找到沒有限制的點

2.拔他的出邊

3.去看他拔掉的邊對到的點還有沒有限制

一定可以做完全部事件?

拓樸排序

拓樸排序

拓樸排序

拓樸排序

動不了了

拓樸排序

動不了了

出現環了

拓樸排序

動不了了

出現環了

拓樸排序

動不了了

出現環了

無解

拓樸排序

可以發現拓樸排序只能在沒有環的情況下走到所有點,剩下的點不是在環上就是在從環連出去的鍊上。

 

 

AC扣

Functional Graph

定義:對於一張有像圖,每個點的出度皆為1

 

可以發現對於每個大小為\(n\)的連通塊,他都有\(n\)條邊,也就代表最多只有一個環。

這樣可以幹嘛?

 

對它拓樸排序,沒走到的點就在那個環上

例題

給你一張functional graph和\(q\)筆詢問,每次問你從\(x\)走\(k\)步之後會到哪個點。

\(k \le 10^{9}\)

Functional Graph

例題

給你一張functional graph和\(q\)筆詢問,每次問你從\(x\)走\(k\)步之後會到哪個點。

\(k \le 10^{9}\)

為甚麼這麼大?可以發現不管一開始在哪,走到最後一定會在環上。

Functional Graph

例題

給你一張functional graph和\(q\)筆詢問,每次問你從\(x\)走\(k\)步之後會到哪個點。

\(k \le 10^{9}\)

為甚麼這麼大?可以發現不管一開始在哪,走到最後一定會在環上。

這樣可以得出一個暴力\(O(\sum{k})\)的作法,但顯然不會過。

Functional Graph

例題

給你一張functional graph和\(q\)筆詢問,每次問你從\(x\)走\(k\)步之後會到哪個點。

\(k \le 10^{9}\)

因為他是functional graph,所以它的路徑一定,可以預處理算出來。

空間:\(O(n\max(k))\) MLE

Functional Graph

例題

給你一張functional graph和\(q\)筆詢問,每次問你從\(x\)走\(k\)步之後會到哪個點。

\(k \le 10^{9}\)

因為他是functional graph,所以它的路徑一定,可以預處理算出來。

空間:\(O(n\max(k))\) MLE

倍增!

空間:\(O(n\log{k})\) 時間:\(O(q\log{k})\) AC

Functional Graph

例題

最短路

主要分成幾種:

  • 單點源/全點對
  • 帶權/不帶權
  • 有負邊/無負邊
  • 有負環/無負環

鬆弛

你的皮炎


單點源最短路

看這張圖,找到從0出發,到每個點的最短距離

int dis[5]={0,1,1,2,1};

0

1

4

3

2

怎麼做出這個距離表格?

BFS 複雜度\(O(V+E)\)

最短路

看這張圖,找到從0出發,到每個點的最短距離

int dis[5]={0,1,1,2,1};

0

1

4

3

2

怎麼做出這個距離表格?

BFS 複雜度\(O(V+E)\)

每一張圖都可以這樣做嗎

最短路

看這張圖,找到從0出發,到每個點的最短距離

0

1

4

3

2

3

10

4

2

6

int dis[5]={0,3,6,5,7};

單純BFS行不通了

Bellman Ford

流程

開一個while(true),對於每條邊不斷更新他與0的最短距離

TLE?
發現在沒有負環的圖上,路徑上最多經過\(V\)個人,所以更新\(V\)次後一定可以找到最短路
負環則是可以無限更新,發現跑\(V\)次之後還能鬆弛就代表這張圖有負環


時間複雜度:\(O(VE)\)
空間複雜度:\(O(V+E)\)

Bellman Ford

vector<int> Bellman_Ford(int n,int s,vector<tuple<int,int,int>> edge){
	// n代表這張無向圖有n個點(0~n-1)
    // s代表起點
    // auto [u,v,c] = edge[i] 代表u和v間有一條邊權為c的雙向邊
	vector<int> dis(n,inf);// 初始成無限遠
    dis[s]=0;
    bool flag = 0;
    for(int i = 0;i<=n;i++){
    	for(auto [u,v,c]:edge){
        	if(dis[u]+c<dis[v])dis[v]=dis[u]+c,flag=1;
            if(dis[v]+c<dis[u])dis[u]=dis[v]+c,flag=1;
		}
        if(i==n)break;
        flag=0;
    }
    assert(!flag); // 如果有負環這裡會炸
    return dis;
}

\(V \le 10^3, E \le V \times (V-1)/2\) ?

好慢喔
SPFA(範例扣有錯)

Dijkstra

流程

1.找到離原點最接近的點

2.用他鬆弛他的全部鄰居

 

複雜度?
時間 : \(O(V^2+E)\)
空間 : \(O(V+E)\)

一種可以處理無負邊的圖的最短路算法

Dijkstra

vector<int> Dijkstra(int n,int s,vector<vector<pair<int,int>>> adj){
	// n代表這張無向圖有n個點(0~n-1)
    // s代表起點
    // auto [v,c] = adj[u] 代表u和v間有一條邊權為c的雙向邊
	vector<int> dis(n,inf);// 初始成無限遠
    vector<int> vis(n,0);
    dis[s]=0;
    for(int i = 0;i<n;i++){
    	int id = -1;
        for(int i = 0;i<n;i++)if(!vis[i]&&(id==-1||dis[id]>dis[i]))id=i;
        vis[id]=1;
        for(auto [v,c]:adj[id])dis[v]=min(dis[v],dis[u]+c);
    }
    return dis;
}

\(V \le 10^5,E \le 10^6\)
還是太慢ㄝ

Dijkstra

優化第一個步驟

用一個min heap維護每個人到原點的距離

時間複雜度\(O((V+E) \log(V+E))\)空間複雜度\(O(V+E)\)

vector<int> Dijkstra(int n,int s,vector<vector<pair<int,int>>> adj){
	// n代表這張無向圖有n個點(0~n-1)
    // s代表起點
    // auto [v,c] = adj[u] 代表u和v間有一條邊權為c的雙向邊
	vector<int> dis(n,inf);// 初始成無限遠
    dis[s]=0;
    priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>> pq;// min heap
    // auto [d,v] = pq.top() 代表v是當前距離原點最近的點且距離原點'可能'是d
    // 按照距離原點的距離排序 所以應該是距離放pair前,點的index放後
    for(;!pq.empty();){
    	auto [d,u] = pq.top();pq.pop();
        if(dis[u] < d)continue;// 代表從原點到v有一條比d更短的路
        for(auto [v,c]:adj[u])if(dis[v]>d+c)pq.emplace(dis[v]=d+c,v);
    }
    return dis;
}

Johnson

有負邊的時候可做

 

全點源最短路

\(V \le 500, E \le V \times (V-1) / 2\)

dijkstra?


\(O(VE \log V)\)

 

倒了

不要用pq優化?

\(O(V^3+VE)\)

可是我不屌這個

我要講新東西

Floyd-Warshall

一點dp的想法:


\(dp[i][j] = 從i走到j的最短路徑\)

轉移:\(dp[i][j] = \min_{1 \le k \le n}(dp[i][k]+dp[k][j])\)

 

複雜度:\(O(V^3)\)

 

其實就是矩陣乘法的複雜度

Floyd-Warshall

注意迴圈裡\(i, j, k\)的順序

原本的dp狀態:
\(dp[i][j][k] = i到j之間的最短路上i和j以外index最大是k\)

轉移:

\(chmin(dp[i][j][k],dp[i][k][any]+dp[k][j][any]) ,any \lt k\)

發現最後一維可以直接丟掉

vector<vector<int>> Floyd_Warshall(int n,vector<vector<int>> adj){
	vector dis(n,vector(n,inf));
    for(int i = 0;i<n;i++)dis[i][i]=0;
    for(int k = 0;k<n;k++)
    	for(int i = 0;i<n;i++)
        	for(int j = 0;j<n;j++)
    			dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
    return dis;
}

差分約束

題目

 

\(給你V個點V \le 5000\)

和M個兩個節點間的數量關係\(E \le 5000\)

問你是否存在符合全部關係的情況

關係:

1.a最多比b多c

2.a最少比b多c

3.a和b要同

差分約束

幫每個點設一個函數\(d(v)\)代表他的數量(現在還不知道是多少)

可以發現三個關係都可以轉成一種限制兩點d(x)差的邊:

1.\(d(a)-d(b) \le c\)

\(\to d(a) \le d(b) + c\)

\(\to b到a建一條長度為 \ c \ 的邊\)

2.\(d(a)-d(b) \ge c\)

\(\to d(a) \le d(b) - c\)

\(\to b到a建一條長度為- c \ 的邊\)

3.

\(d(a)-d(b) = 0\)

\(\to b到a,a到b各建一條長度為0的邊\)

差分約束

怎麼樣會不可行?

\(\to\)有兩點間距離沒辦法決定的時候

\(\to\)最短路會出事

\(\to\)有負環

 

\(\to\)Bellman-Ford判負環

複雜度:\(O(VE)\)

例題

並查集

\(現在有N個人,M個操作,操作有兩種 :\)

\(1.給你一個關係a,b代表b是a的pa\)

\(2.問你現在為止a最上面的pa是誰\)

\((定義每個人一開始的pa都是自己且保證不會有矛盾的操作)\)

 

暴力?

\(修改O(1) ,查詢O(V)\)

並查集?

\(修改O(\log V) ,查詢O(\log V)\)(均攤)

並查集

想法:

原本暴力的瓶頸是在於你每次都要看過全部的\(pa\)才能找到答案,但其實中間的\(pa\)根本不重要,所以可以做路徑壓縮

並查集

想法:

原本暴力的瓶頸是在於你每次都要看過全部的\(pa\)才能找到答案,但其實中間的\(pa\)根本不重要,所以可以做路徑壓縮

並查集

想法:

原本暴力的瓶頸是在於你每次都要看過全部的\(pa\)才能找到答案,但其實中間的\(pa\)根本不重要,所以可以做路徑壓縮

 

struct DSU{

	int n;
    
    vector<int> dsu;
    
    DSU(int _):n(_),dsu(_){
    	iota(dsu.begin(),dsu.end(),0);
    }
    
   	inline int findpa(int pos){
    	return dsu[pos]==pos?pos:(dsu[pos] = find(dsu[pos]));
    }
    
    inline void modify(int a,int b){
		dsu[a] = findpa(b);
	}
    
    inline int query(int a){
    	return findpa(a);
    }
    
};

並查集

如果modify沒有規定祖先關係

還可以用啟發式合併優化複雜度到

修改、詢問\(O(V \alpha (V))\)(均攤)

struct DSU{

	int n;
    
    vector<int> dsu;
    
    DSU(int _):n(_),dsu(_){
    	fill(dsu,dsu+n,-1);
    }
    
   	inline int findpa(int pos){
   		return dsu[pos]>=0?dsu[pos]=findpa(dsu[pos]):pos;
    }
    
    inline void modify(int a,int b){
    	a=find(a),b=find(b);
        if(a==b)return;
        if(dsu[a]<dsu[b])swap(a,b);
        dsu[b]+=dsu[a],dsu[a]=b;
	}
    
    inline int query(int a){
    	return findpa(a);
    }
    
};

並查集

題目

給你一張圖,問你連通塊數量和最大連通塊大小

並查集

題目

給你一張圖,問你連通塊數量和最大連通塊大小

 

對於一塊連通塊,你可以決定一個代表這個連通塊的點,而剩下的點都連到他

\(\to\)和並查集的想法類似

 

擴展域並查集

題目

給你一些關係,要你判斷每個關係的真假

擴展域並查集

題目

給你一些關係,要你判斷每個關係的真假

 

先來設狀態

\(val[i][0] := i被誰吃\)

\(val[i][1] := i是誰\)

\(val[i][2] := i吃誰\)

擴展域並查集

想想看每組關係代表甚麼

\(1.x和y同類:\)

\(i \in \{0,1,2\}, val[x][i] = val[y][i]\)

\(2.x吃y\)

\(i \in \{0,1,2\}, val[x][(i+1) \mod 3] = val[y][i]\)

可以用\(dsu\)做耶

#include<bits/stdc++.h>
using namespace std;
#define IOS cin.tie(nullptr)->sync_with_stdio(0),cin.exceptions(cin.failbit);
#define lb(x) (x)&-(x)
#define all(x) (x).begin(),(x).end()
#define ll long long

constexpr int maxN=5e5+5;

int n,q,cnt,dsu[maxN<<2];

inline int find(int pos){
    return dsu[pos]<0?pos:(dsu[pos]=find(dsu[pos]));
}

inline void merge(int a,int b){
    a=find(a),b=find(b);
    if(a==b)return;
    if(dsu[a]<dsu[b])swap(a,b);
    dsu[b]+=dsu[a],dsu[a]=b;
}

int main(){
    IOS
    cin>>n>>q;
    fill(dsu,dsu+3*n+1,-1);
    for(int a,b,c;q--;){
        cin>>c>>a>>b;
        if((c==2&&b==a)||b>n||a>n)cnt++;
        else if(c==1){
            if(find(a)==find(b+n)||find(a+n)==find(b))cnt++;
            else merge(a,b),merge(a+n,b+n),merge(a+2*n,b+2*n);
        }
        else {
            if(find(a+n)==find(b+n)||find(a+2*n)==find(b+n))cnt++;
            else merge(a,b+n),merge(a+n,b+2*n),merge(a+2*n,b);
        }
    }
    cout<<cnt<<'\n';
}

例題

最小生成樹

給你一張圖耀你找到

(邊權中位數最小/邊權平均最小)

的子圖讓整張圖連通

kruskal

流程:

\(1.幫所有邊按照邊權排序\)

\(2.按照順序 如果這個邊的兩端不連通 mst就要加上這條邊\)

反之不用

\(3.答案就會是那幾條邊\)

 

複雜度:\(O(E \log E + E \alpha(E))\)

prim

流程:

\(1.隨便選一個人當起點\)

\(2.把這個人連到還沒走過的點的邊加進pq並按邊權排序\)

\(3.對於pq.top()的人 如果要更新的點還沒經過就把這條邊加入mst\)

\(4.跑第2點直到全部點被走過\)

 

複雜度:\(O((E+V) \log(E+V))\)

boruvka

\(1.對於每個連通塊找到連出去最賺的邊 連起來\)

\(2.重複 \log V次\)

 

複雜度:\(O((V+E) \log V)\)

例題

連通分量

通常叫CC(connecting component)

 

一張圖連在一起的就叫連通分量

\(\to\)同一塊裡面可以分成很多塊

通常討論最外面最大那塊

單連通分量

就是連通塊,用\(dsu\)維護就好

 

一些後面會用到的名詞:

 

割點(cut):

拔掉這個點和所有和他相連的邊後這個連通塊會變兩塊

 

橋(bridge):

拔掉這條邊後這個連通塊會變兩塊

dfs tree

邊雙連通分量

BCC-edge

 

\(對於每對在同一個BCC裡的(u,v)\)

,可以找到兩條邊互斥的路徑

 

偷動畫

邊雙連通分量

實作想法:

看剛剛的動畫可以發現對於bridge兩端的連通塊裡的各一點\(u,v\)之間必定只能有一條路徑

\(\to\)找完橋後直接把整個連通塊放在同一個BCC裡

 

口糊:

假設同一個BCC裡有點對\((u,v)\)無法滿足條件就代表他們之間只能有一條路徑

\(\to\)連通塊裡有bridge

\(\to\)矛盾

邊雙連通分量

實作:

(1)找橋(例題)

在dfs tree上記錄每個點的dfn和low分別代表他的dfs序和往下走能走到的最小dfs序

對於一條邊是不是橋需要在上面的端點判,判斷下面的端點是不是能走到上面即可

(2)縮BCC

用stack在dfs的時候同時縮或是找完橋後再幫每塊跑dfs縮

 

複雜度:\(O(V+E)\)(一次dfs)

邊雙連通分量

vector<int> bcc_edge_find(int n,vector<vector<int>>& adj){

	int cnt = 0,cc_cnt = 0;
	vector<int> dfn(n),low(n,inf),stk,bcc_id(n);
    
    auto dfs = [&](int now,int last) -> void {
    	dfn[now] = low[now] = ++cnt;
        stk.emplace_back(now);
        for(int i:adj[now])if(i!=last){
			if(!dfn[i]){
            	dfs(i,now);
                low[now] = min(low[now],low[i]);
              	if(low[i]>dfn[now]){
                    for(++cc_cnt;stk.back()!=i;stk.pop_back())
                    	bcc_id[stk.back()] = cc_cnt;
                   	bcc_id[stk.back()] = cc_cnt;
                    stk.pop_back();
                }
            }
            else low[now] = min(low[now],dfn[i]);
        }
    };
    for(int i = 0;i<n;i++)if(!dfn[i])dfs(i,i);
    return bcc_id;
}

點雙連通分量

BCC-vertex

 

對於一個BCC裡面可以找到兩條點互斥的路徑

 

動畫

點雙連通分量

實作想法:

發現對於一個割點的兩端必定不會在同一個BCC

\(\to\)當你找到割點就把他連出去的連通塊縮起來

 

懶得口糊

點雙連通分量

實作想法:

(1)找割點(例題)

與找橋大致相同,但你當成根的點在dfs的點需要特判

(判斷是不是有一個以上的獨立子樹)

(2)縮BCC
同樣是用stack在dfs過程中縮

 

複雜度:\(O(V+E)\)

點雙連通分量

vector<vector<int>> bcc_vertex_find(int n,vector<vector<int>>& adj){

	int cnt = 0,cc_cnt = 0;
	vector<int> dfn(n),low(n,inf),stk;
    
    vector<vector<int>> bcc_id(n);
    
    auto dfs = [&](int now,int last) -> void {
    	dfn[now] = low[now] = ++cnt;
        stk.emplace_back(now);
        for(int i:adj[now])if(i!=last){
			if(!dfn[i]){
            	dfs(i,now);
                low[now] = min(low[now],low[i]);
              	if(low[i]>=dfn[now]){
                	++cc_cnt;
                    for(int p = -1;p!=i;stk.pop_back())
                    	p=stk.back(),bcc_id[p].emplace_back(cc_cnt);
                    bcc_id[now].emplace_back(cc_cnt);
                }
            }
            else low[now] = min(low[now],dfn[i]);
        }
    };
    for(int i = 0;i<n;i++)if(!dfn[i])dfs(i,i);
    return bcc_id;
}

圓方樹

 

例題

強連通分量

強連通分量

tarjanSCC:

在dfs tree上記錄dfn和low,如果low=dfn的時候代表可以縮SCC,用stack實作

需要注意對於dfs tree上的交錯邊,需要判斷指到的節點是否已經被標記SCC

 

複雜度:\(O(V+E)\)

強連通分量

tarjanSCC:

vector<int> scc_tarjan(int n,vector<vector<int>>& adj){

	int cnt = 0,cc_cnt = 0;
	vector<int> dfn(n),low(n,inf),stk,scc_id(n);
    
    auto dfs = [&](int now) -> void {
    	dfn[now] = low[now] = ++cnt;
        stk.emplace_back(now);
        for(int i:adj[now])if(!scc_id[i]){
			if(!dfn[i])dfs(i,now),low[now] = min(low[now],low[i]);
            else low[now] = min(low[now],dfn[i]);
        }
        if(dfn[now]==low[now]){
        	++cc_cnt;
			for(int p = -1;p!=now;stk.pop_back())
				p=stk.back(),scc_id[p]=cc_cnt;
        }
    };
    for(int i = 0;i<n;i++)if(!dfn[i])dfs(i,i);
    return scc_id;
}

強連通分量

kosarajuSCC:

對於正圖先dfs一次透過後序加入stack找出反的拓排序,

再根據正的拓排序在反圖上dfs一次並對於走過的點都縮進同一個SCC

 

複雜度:\(O(V+E)\)

強連通分量

kosarajuSCC:

vector<int> scc_kosaraju(int n,vector<vector<int>>& adj){

	int cc_cnt=0;
	
    vector<vector<int>> jda(n);
    
    vector<int> scc_id(n),vis(n),topo_order;
    
    for(int i = 0;i<n;i++)for(int j:adj[i])jda[j].emplace_back(i);
    
    auto dfs1 = [&](int now) -> void {
    	for(int i:adj[now])if(!vis[i])dfs1(i);
        topo_order.emplace_back(now);
    };
    
    auto dfs2 = [&](int now) -> void {
    	scc_id[now] = cc_cnt;
    	for(int i:jda[now])if(!scc_id[i])dfs2(i);
    };
    
    for(int i = 0;i<n;i++)if(!vis[i])dfs(i);
    
    reverse(topo_order.begin(),topo_order.end());
    
    for(int i:topo_order)if(!scc_id[i])++cc_cnt,dfs2(i);
    
    return scc_id;
    
}

強連通分量

kosarajuSCC:

vector<int> scc_kosaraju(int n,vector<vector<int>>& adj){

	int cc_cnt=0;
	
    vector<vector<int>> jda(n);
    
    vector<int> scc_id(n),vis(n),topo_order;
    
    for(int i = 0;i<n;i++)for(int j:adj[i])jda[j].emplace_back(i);
    
    auto dfs1 = [&](int now) -> void {
    	for(int i:adj[now])if(!vis[i])dfs1(i);
        topo_order.emplace_back(now);
    };
    
    auto dfs2 = [&](int now) -> void {
    	scc_id[now] = cc_cnt;
    	for(int i:jda[now])if(!scc_id[i])dfs2(i);
    };
    
    for(int i = 0;i<n;i++)if(!vis[i])dfs(i);
    
    reverse(topo_order.begin(),topo_order.end());
    
    for(int i:topo_order)if(!scc_id[i])++cc_cnt,dfs2(i);
    
    return scc_id;
    
}

強連通分量

大意:

有n個pizza和m個人,每個人有兩個希不希望某個配料的願望,問你有沒有方法能讓每個人至少滿足一個願望

強連通分量

大意:

有n個pizza和m個人,每個人有兩個希不希望某個配料的願望,問你有沒有方法能讓每個人至少滿足一個願望

 

可以想成如果一個願望沒被滿足那另一個胃一定要被大滿足

強連通分量

開心(水族箱)建模

 

先幫每個配料開兩個點代表要不要他

對於每個人的要求就變成

\(1.不滿足a就要滿足b\)

\(\to反a指向b\)

\(2.不滿足b就要滿足a\)

\(\to反b指向a\)

 

施竣耀升級了!

強連通分量

判斷可不可行就是對於每個配料\(v\),

他的反\(v\)是不是跟他在同一個SCC

 

構解就選\(v和反v\)在鍊前端的(不連通的話就亂選就好)

 

例題

Made with Slides.com