# 圖論 --- ## 目錄 - Introduction(簡介) - Example(範例) - Basic Algorithms(基本演算法) - Example(範例) - Advanced Topic(稍微進階的內容) --- ## Introduction ---- 圖 $G$ 由點 $V$ 與邊 $E$ 構成,記做 $G=(V,E)$ 。 聽起來很難? ---- 其實就是像這樣的東西 ```graphviz digraph hierarchy { nodesep=1.0 // increases the separation between nodes node[color=Blue,fontname=Courier,shape=circle] //All nodes will this shape and colour edge [color=Black, style=dashed] //All the lines look like this "0"->{"2" "3"} "1"->{"2" "4","0"} "2"->"3" "4"->{"3","2"} {rank=same;"1" "2" "3"} // Put them on the same level } ``` --- ## 線上好用工具 https://csacademy.com/app/graph_editor/ ---- <img src="https://live.staticflickr.com/65535/51873707080_94ef96f520_o.png" width="681" height="681" alt="Binary Tree"> ---- <img src="https://live.staticflickr.com/65535/51873070581_70a37c7fd0_o.png" width="681" height="681" alt="CommonGraph"> --- 所以這就是圖,不過這樣的東西到底會問怎樣的問題呢? --- ## 例題一 ---- 輸入一個有向圖 G 與一個起點 s - 請計算由 s 出發可以到達的點數(不包含 s) - 並且計算這些可以到達的點與 s 的距離和 - 假設每個邊的長度均為 1。 - 兩點之間可能有多個邊 - 邊的起點與終點未必不同 ---- ### 例圖 ```graphviz digraph hierarchy { nodesep=1.0 // increases the separation between nodes node[color=Blue,fontname=Courier,shape=circle] //All nodes will this shape and colour edge [color=Black, style=dashed] //All the lines look like this "0"->{"2" "3"} "1"->{"2" "4","0"} "2"->"3" "4"->{"3","2"} {rank=same;"1" "2" "3"} // Put them on the same level } ``` ---- ### 那怎麼辦 --- ## 存圖 ---- ### 鄰接串列(Adjacency Link) ---- 因為比賽中多數都用不到鄰接矩陣(Adjacency Matrix),所以我先講鄰接串列 ---- 簡單說就是用 `vector` 陣列存 ```cpp= const int N=100010; vector<int> g[N]; ``` ---- `g[1]` 是一個 `vector` ,裡面存的是從 1 這個點出發可以到達的點 ---- 新增從 a 到 b 的邊 - 有向圖 `g[a].push_back(b);` - 無向圖還要加上 `g[b].push_back(a);` 因為無向圖代表的是 a 可以到 b , b 也可以到 a 。 --- ## BFS(廣度優先搜索) ---- 依照這個順序訪問點 ---- ```graphviz digraph hierarchy { nodesep=1.0 // increases the separation between nodes node[color=Blue,fontname=Courier,shape=circle] //All nodes will this shape and colour edge [color=Black, style=dashed] //All the lines look like this "0"->{"2" "3"} "1"[color=red] "1"->{"2" "4","0"} "2"->"3" "4"->{"3","2"} {rank=same;"1" "2" "3"} // Put them on the same level } ``` ---- ```graphviz digraph hierarchy { nodesep=1.0 // increases the separation between nodes node[color=Blue,fontname=Courier,shape=circle] //All nodes will this shape and colour edge [color=Black, style=dashed] //All the lines look like this "0"[color=red] "0"->{"2" "3"} "1"[color=black] "1"->{"2" "4","0"} "2"->"3" "4"->{"3","2"} {rank=same;"1" "2" "3"} // Put them on the same level } ``` ---- ```graphviz digraph hierarchy { nodesep=1.0 // increases the separation between nodes node[color=Blue,fontname=Courier,shape=circle] //All nodes will this shape and colour edge [color=Black, style=dashed] //All the lines look like this "0"[color=black] "0"->{"2" "3"} "1"[color=black] "1"->{"2" "4","0"} "4"[color=red] "2"->"3" "4"->{"3","2"} {rank=same;"1" "2" "3"} // Put them on the same level } ``` ---- ```graphviz digraph hierarchy { nodesep=1.0 // increases the separation between nodes node[color=Blue,fontname=Courier,shape=circle] //All nodes will this shape and colour edge [color=Black, style=dashed] //All the lines look like this "0"[color=black] "0"->{"2" "3"} "1"[color=black] "1"->{"2" "4","0"} "4"[color=black] "2"[color=red] "2"->"3" "4"->{"3","2"} {rank=same;"1" "2" "3"} // Put them on the same level } ``` ---- ```graphviz digraph hierarchy { nodesep=1.0 // increases the separation between nodes node[color=Blue,fontname=Courier,shape=circle] //All nodes will this shape and colour edge [color=Black, style=dashed] //All the lines look like this "0"[color=black] "0"->{"2" "3"} "1"[color=black] "1"->{"2" "4","0"} "4"[color=black] "2"[color=black] "2"->"3" "3"[color=red] "4"->{"3","2"} {rank=same;"1" "2" "3"} // Put them on the same level } ``` ---- ```graphviz digraph hierarchy { nodesep=1.0 // increases the separation between nodes node[color=Blue,fontname=Courier,shape=circle] //All nodes will this shape and colour edge [color=Black, style=dashed] //All the lines look like this "0"[color=black] "0"->{"2" "3"} "1"[color=black] "1"->{"2" "4","0"} "4"[color=black] "2"[color=black] "2"->"3" "3"[color=black] "4"->{"3","2"} {rank=same;"1" "2" "3"} // Put them on the same level } ``` ---- 以程式來實作的話需要一些簡單的資料結構(queue) ---- code ```cpp= vector<int> g[N]; void BFS(int s){ queue<int> q; q.push(s); while(!q.empty()){ int now=q.front(); q.pop(); for(auto e:g[now]){ q.push(e); } } } ``` ---- 不過光是這樣寫詪本沒用,我們需要在這樣的架構下添加一些東西 ---- ```cpp= struct info{ int to,dis; }; vector<int> g[N]; int bfs(int s){ queue<info> q; q.push({s,0}); int ret=0; while(!q.empty()){ info now=q.front(); q.pop(); ret+=now.dis; for(auto e:g[now.to]){ q.push({e,now.dis+1}); } } return ret; } ``` ---- ### 可是 ---- 如果是這樣的圖呢? ```graphviz digraph hierarchy { nodesep=1.0 // increases the separation between nodes node[color=Blue,fontname=Courier,shape=circle] //All nodes will this shape and colour edge [color=Black, style=dashed] //All the lines look like this "0"->{"2"} "1"->{"0"} "2"->"1" {rank=same;"1" "2"} // Put them on the same level } ``` ---- 從 1 開始 - 將 2 推入 queue - 進入節點 2 - 將 3 推入 queue 中 - 進入節點 3 - 將 1 推入 queue 中 - ... ---- 好像變成無窮迴圈了 ---- 加入狀態 isv - true 此節點已被丟進 queue 過 - false 於上面相反 ---- code ```cpp= struct info{ int to,dis; }; vector<int> g[N]; bool isv[N]; int bfs(int s){ queue<info> q; q.push({s,0}); int ret=0; while(!q.empty()){ info now=q.front(); q.pop(); ret+=now.dis; for(auto e:g[now.to]){ if(!isv[e]){ isv[e]=true; q.push({e,now.dis+1}); } } } return ret; } ``` --- ## DFS(深度優先搜索) ---- 依照以下順序訪問節點 ---- ```graphviz digraph hierarchy { nodesep=1.0 // increases the separation between nodes node[color=Blue,fontname=Courier,shape=circle] //All nodes will this shape and colour edge [color=Black, style=dashed] //All the lines look like this "0"->{"2" "3"} "1"[color=red] "1"->{"2" "4","0"} "2"->"3" "4"->{"3","2"} {rank=same;"1" "2" "3"} // Put them on the same level } ``` ---- ```graphviz digraph hierarchy { nodesep=1.0 // increases the separation between nodes node[color=Blue,fontname=Courier,shape=circle] //All nodes will this shape and colour edge [color=Black, style=dashed] //All the lines look like this "0"[color=red] "0"->{"2" "3"} "1"[color=black] "1"->{"2" "4","0"} "2"->"3" "4"->{"3","2"} {rank=same;"1" "2" "3"} // Put them on the same level } ``` ---- ```graphviz digraph hierarchy { nodesep=1.0 // increases the separation between nodes node[color=Blue,fontname=Courier,shape=circle] //All nodes will this shape and colour edge [color=Black, style=dashed] //All the lines look like this "0"[color=black] "0"->{"2" "3"} "1"[color=black] "1"->{"2" "4","0"} "3"[color=red] "2"->"3" "4"->{"3","2"} {rank=same;"1" "2" "3"} // Put them on the same level } ``` ---- ```graphviz digraph hierarchy { nodesep=1.0 // increases the separation between nodes node[color=Blue,fontname=Courier,shape=circle] //All nodes will this shape and colour edge [color=Black, style=dashed] //All the lines look like this "0"[color=black] "0"->{"2" "3"} "1"[color=black] "1"->{"2" "4","0"} "3"[color=black] "2"[color=red] "2"->"3" "4"->{"3","2"} {rank=same;"1" "2" "3"} // Put them on the same level } ``` ---- ```graphviz digraph hierarchy { nodesep=1.0 // increases the separation between nodes node[color=Blue,fontname=Courier,shape=circle] //All nodes will this shape and colour edge [color=Black, style=dashed] //All the lines look like this "0"[color=black] "0"->{"2" "3"} "1"[color=black] "1"->{"2" "4","0"} "3"[color=black] "2"[color=black] "2"->"3" "4"[color=red] "4"->{"3","2"} {rank=same;"1" "2" "3"} // Put them on the same level } ``` ---- ```graphviz digraph hierarchy { nodesep=1.0 // increases the separation between nodes node[color=Blue,fontname=Courier,shape=circle] //All nodes will this shape and colour edge [color=Black, style=dashed] //All the lines look like this "0"[color=black] "0"->{"2" "3"} "1"[color=black] "1"->{"2" "4","0"} "4"[color=black] "2"[color=black] "2"->"3" "3"[color=black] "4"->{"3","2"} {rank=same;"1" "2" "3"} // Put them on the same level } ``` ---- DFS 的好處是可以用遞迴實作,而不用使用資料結構(也可以用 stack) ---- ```cpp= vector<int> g[N]; bool isv[N]; void DFS(int n){ for(auto u:g[n]){ if(!isv[u]){ isv[u]=true; DFS(u); } } } ``` ---- 因為 DFS 明顯比較好寫,因此在可以用 DFS 的情況下幾乎會使用他。 --- ## 例題二(ap325 P-7-2) ---- 題目是無向圖 ---- 補充說明:通常題目給的圖不一定可以從起點到達所有的點,這樣我們會說這個圖是非連通的,反之從起點可以到任何地方的稱為連通圖 ---- code ```cpp= #include<bits/stdc++.h> using namespace std; const int N=100010; int val[N]; vector<int> g[N]; bool isv[N]; int dfs(int n){ int ret=val[n]; for(int next:g[n]){ if(!isv[next]){ isv[next]=true; ret+=dfs(next); } } return ret; } void solve(){ int n,m; cin>>n>>m; for(int i=0;i<n;++i){ cin>>val[i]; } for(int i=0;i<m;++i){ int a,b; cin>>a>>b; g[a].emplace_back(b); g[b].emplace_back(a); } int ans=0; for(int i=0;i<n;++i){ if(!isv[i]){ isv[i]=true; ans=max(ans,dfs(i)); } } cout<<ans<<"\n"; } int main(){ ios::sync_with_stdio(0);cin.tie(0); solve(); } ``` --- ## 常見題型 ---- ### 格子圖 - [CSES_graph_1](https://cses.fi/problemset/task/1192) - [CSES_graph_2](https://cses.fi/problemset/task/1193) ---- 先來看第一題 - 給你一張圖,請你算出房間數量。地圖有 $n \times m$ 個格子,每一個格子不是地板就是牆壁。你可以往上下左右的地板行走,所有依照這個方式走的到的位置都算同一個房間。 - 第一行輸入兩個數字 $n,m$ ,緊接著是 $n$ 行 $m$ 列的字元(字串),`'#'` 表示牆壁,`'.'` 表示地板。 ---- ---- 第二題 - 給你一張圖,請你從 A 走到 B。地圖有 $n \times m$ 個格子,每一個格子不是地板就是牆壁。你可以往上下左右的地板行走,所有依照這個方式走的到的位置都算同一個房間。 - 第一行輸入兩個數字 $n,m$ ,緊接著是 $n$ 行 $m$ 列的字元(字串),`'#'` 表示牆壁,`'.'` 表示地板, A 是起點, B 是終點。 ---- 第二題自行練習 參考程式碼 ```cpp= #include<bits/stdc++.h> #pragma GCC optimize ("O3,unroll-loops,fast-math") using namespace std; // 3 // 2 0 // 1 const int dx[4]{1,0,-1,0},dy[4]{0,-1,0,1}; map<int,char> toc{{0,'R'},{1,'U'},{2,'L'},{3,'D'}}; map<char,int> toi{{'R',0},{'U',1},{'L',2},{'D',3}}; struct info{ int r,c; info(int _r,int _c){ r=_r; c=_c; } }; int n,m; vector<string> mp,tp,pre; string ans; info bfs(int r,int c){ queue<info> q; q.emplace(r,c); while(!q.empty()){ info now=q.front(); q.pop(); if(tp[now.r][now.c]=='B'){ return info(now.r,now.c); } for(int i=0;i<4;i++){ int px=now.c+dx[i],py=now.r+dy[i]; if(px>=0 && py>=0 && px<m && py<n){ if(mp[py][px]!='#'){ mp[py][px]='#'; pre[py][px]=toc[i]; q.emplace(py,px); } } } } return info(-1,-1); } void solve(){ mp.clear(); cin>>n>>m; for(int i=0;i<n;i++){ string line; cin>>line; mp.emplace_back(line); } tp=pre=mp; info pos(-1,-1),st(0,0); for(int i=0;i<n;i++){ for(int j=0;j<m;j++){ if(mp[i][j]=='A'){ pos=bfs(i,j); st=info(i,j); break; } } } if(pos.r!=-1){ cout<<"YES"<<"\n"; while(pos.r!=st.r || pos.c!=st.c){ ans+=pre[pos.r][pos.c]; int p=toi[pre[pos.r][pos.c]]; pos.r-=dy[p]; pos.c-=dx[p]; } reverse(ans.begin(),ans.end()); cout<<ans.size()<<"\n"<<ans<<"\n"; }else{ cout<<"NO"<<"\n"; } } int main(){ ios::sync_with_stdio(0);cin.tie(0); // freopen("test_input.txt","r",stdin); // freopen("output.txt","w",stdout); int t=1; while(t--){ solve(); } } ``` ---- ### 一般圖 - [CSES_graph_3](https://cses.fi/problemset/task/1666/) - [CSES_graph_4](https://cses.fi/problemset/task/1667/) ---- - 給你 $n$ 個點 $m$ 條無向邊。請你找出要讓這張圖連通的最少所需新增邊數。還有給出具體方案。節點編號為 $1...n$ - 第一行輸入兩個數字 $n,m$ ,接下來有 $m$ 行,輸入邊a,b - 保證沒有自環或重邊 ---- ---- 第二題 - 給你 $n$ 台電腦 $m$ 組連接關係。請你找出從點 $1$ 能否到點 $n$ 。如果可以,一並輸出最短路徑。 - 第一行輸入兩個數字 $n,m$ ,接下來有 $m$ 行, - 保證沒有自環或重邊 ---- 第二題自行練習 參考程式碼 ```cpp= #include<bits/stdc++.h> using namespace std; const int N=100010; int n,m,a,b; int pre[N]; vector<int> g[N]; bool isv[N]; int bfs(int u,int v){ queue<int> q; q.emplace(u); while(!q.empty()){ int now=q.front(); q.pop(); if(now==v){ return v; } for(auto i:g[now]) if(!isv[i]){ isv[i]=true; pre[i]=now; q.emplace(i); } } return -1; } void solve(){ cin>>n>>m; for(int i=1;i<=m;++i){ int a,b; cin>>a>>b; g[a].emplace_back(b); g[b].emplace_back(a); } a=1,b=n; pre[a]=a; isv[a]=true; int p=bfs(a,b); if(p!=-1){ vector<int> ans; while(p!=a){ ans.emplace_back(p); p=pre[p]; } ans.emplace_back(a); cout<<ans.size()<<"\n"; reverse(ans.begin(),ans.end()); for(int i=0;i<ans.size();++i){ cout<<ans[i]<<" "; } }else{ cout<<"IMPOSSIBLE"<<"\n"; } } int main(){ ios::sync_with_stdio(0);cin.tie(0); int t=1; while(t--){ solve(); } } ``` --- ## 圖論演算法 ---- ### 目錄 - 二分圖(Bipartite graph) - 最短路(Shortest Path) - 全點對 - 單點源 - 最小生成樹(Minimum Spanning Tree) - DAG(Directed Acyclic Graph) - 拓鋪排序(Topological Sorting) --- ## 二分圖 ---- 有沒有一種方式可以將圖中的點分成兩組,使組內沒有成員之間的邊。 ---- 如下圖 <img src="https://live.staticflickr.com/65535/51943863462_0d65d866f1_n.jpg" width="299" height="299" alt="二分圖"> ---- ### 二分圖判定 ---- 想像將圖上色,用 12 表示 ---- code ```cpp= int color[N]; bool isBipartiteGraph(int n){ for(auto u:g[n]){ // 用 color==0 代替 bool isv[N]; if(color[u]==0){ color[u]=color[n]%2+1; if(!isBipartiteGraph(u)) return false; }else if(color[u]==color[n]){ return false; } } return true; } ``` ---- - [CSES_graph_5]https://cses.fi/problemset/task/1668 ---- - 有 n 個人, m 個朋友關係,請你將他們分兩組,使得各組內成員間皆沒有朋友關係 - 第一行輸入 n, m ,接下來有 m 行,每行有兩個數字 a, b ,表示 a 與 b 之間有朋友關係, n 個人編號 1~n - 保證沒有自己與自己是朋友的,且相同朋友關係不會建立兩次 - 輸出 n 個數字,每一個都是 1 或 2 ,第 k 個數字表示第 k 個人的組別,可以輸出任何一種合法的解 - 無解時輸出 "IMPOSSIBLE" (不含括號). --- ## 最短路徑 ---- 這邊我們要先進入另一個章節「動態規劃」 ---- ### 全點對 ### Floyd-Warshall ---- - `dp[i][j][k]` 表示經過前 k 個點"鬆弛"後從 i 到 j 的最短距離 - 經過一系列的通靈後,我們可以得知下式 - `dp[i][j][k] = min(dp[i][j][k-1],dp[i][k][k-1]+dp[k][j][k-1]);` - 由於 `dp` 過程中只會用到 `dp[i][j][k-1]` ,所以第三個可以省略 - 最後就像下式 - `dp[i][j] = `</br>`min(dp[i][j],dp[i][k]+dp[k][j])` ---- code ```cpp= for(int k=1;k<=n;++k){ for(int i=1;i<=n;++i){ for(int j=1;j<=n;++j){ dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]); } } } // 也可以透過巨集簡化 #define REP(i,s,n) for(int i=(s);i<=(n);++i) REP(k,1,n) REP(i,1,n) REP(j,1,n){ dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]); } ``` ---- ### 單點源 ---- ### 1.Bellman-Ford ---- 如果只要求一個起點,所有到起點 s 的最短距離 又不可以依據三角不等式決定距離時(BFS不適用) ---- 如下圖 <img src="https://live.staticflickr.com/65535/51972600276_091292c8a5_o.png" width="324" height="324" alt="不符和三角不等式的圖"> ---- 很明顯從 2 繞到 1 拜訪 4 比較快,更何況有邊權是負的情況 ---- - 所以,同樣經過精湛的通靈,我們發明一個新名詞"鬆弛" - 顧名思義,鬆弛就是在不滿足三角不等式的區域做的 - 具體請講師說明(阿就是我 :rolling_on_the_floor_laughing:) ---- ---- 在實作上,我們不再使用 `vector<int> g[N];` 來存圖,轉而使用 `struct` 所以順便複習一下 ```cpp= struct edge{ int from,to,dis; // 建構式 edge(int f=0,int t=0,int d=0){ from=f; to=t; dis=d; } }; vector<edge> es; int main(){ int n; cin>>n; for(int i=0;i<n;++i){ int f,t,d; cin>>f>>t>>d; // 剛剛的建構式單純是為了這樣方便 es.emplace_back(f,t,d); } } ``` ---- ```cpp= const int INF=0x3f3f3f3f; int dis[N]; void Bellman_Ford(int s){ fill(dis,dis+N,INF); dis[s]=0; while(true){ bool update=false; for(auto e:es){ if(dis[e.from]!=INF && dis[e.from]+e.dis<dis[e.to]){ update=true; dis[e.to]=dis[e.from]+e.dis; } } if(!update){ break; } } } ``` ---- 跑完之後 `dis` 陣列裡面存的就會是從起點 s 到 --- ## 進階內容 ---- ### 目錄 - DFS Tree - 網路流(Flows) - 雙連通分量 ---- 很多講師也不會,提供關鍵字讓大家搜尋
{"metaMigratedFrom":"YAML","metaMigratedAt":"2023-06-16T19:21:27.761Z","title":"圖論","breaks":true,"contributors":"[{\"id\":\"3978a08d-c47c-4560-b04d-dfbd8e71d0a3\",\"add\":35011,\"del\":17401},{\"id\":\"4f67c477-a6ac-4743-a794-5e693b4248d6\",\"add\":28,\"del\":1}]"}
    313 views
   owned this note