# 基礎圖論
---
## 圖?
圖論(Graph Theory)是探討關於圖(Graph)這個數學模型的理論。透過研究如何解決 graph 上的各種問題,只要能將題目對應至 graph 這個模型上,就能用 graph 的做法去解這道題目。
有點像尋找萬靈藥的感覺,只要將各種題目想辦法轉到泛用的 graph 模型上,就能通通當作 graph 問題來解。
by [sa](https://hackmd.io/@sa072686/cp/%2F%40sa072686%2FSJoWKFtcm)
----
分享一個畫圖神器
[嘻嘻](https://csacademy.com/app/graph_editor/)
---
## 圖的基本
----
### 點&邊
圖由$點(node)$以及$邊(edge)$構成
```graphviz
graph exp
{
1 -- 1 -- 4 -- 5 -- 1 -- 4;
}
```
```graphviz=
graph exp
{
a -- b -- c -- d -- b;
}
```
```graphviz=
graph exp
{
k -- c -- c -- is -- handsome;
}
```
```graphviz=
graph exp
{
dad -- son;
mom -- son;
}
```
----
### 環
起點與終點相同的一條路徑,且路徑上經過的點不重複
```graphviz
graph
{
rankdir = LR
1 -- 2;
2 -- 3;
1 -- 3;
}
```
這裡有一個環為$1-2-3-1$
----
```graphviz
graph
{
rankdir = LR
1 -- 2;
2 -- 3;
3 -- 4;
2 -- 4;
1 -- 4;
}
```
$1-2-3-4-1$是一個環
$2-3-4-2$也是一個環
$1-2-3-4-2-1$不是一個環
----
### 連通塊
一個圖中的一個子圖,滿足任兩點之間互相連通
```graphviz
graph 連通塊{
subgraph cluster{
1, 3, 4, 6
}
subgraph cluster_a{
2, 5, 7, 8, 9
}
subgraph cluster_c{
a, b, c, d
}
subgraph cluster_t{
10
}
1 -- 3
1 -- 4
3 -- 6[constraint = none]
6 -- 4
2 -- 5 -- 7
2 -- 8
8 --9
a -- b
c -- d
a -- d
10
}
```
這張圖有4個連通塊
圖by tw87
---
## 圖的分類
----
### 有向圖vs.無向圖
```graphviz
digraph exp
{
1 -> 1 -> 4 -> 5;
}
```
```graphviz
graph exp
{
1 -- 1 -- 4 -- 5;
}
```
有向圖為單向道
無向圖為雙向道
----
### 帶權圖 vs. 無權圖
```graphviz=
graph{
9 -- 5 [label = 10];
5 -- 2 [label = 12];
2 -- 7 [label = 77];
}
```
```graphviz=
graph {
9 -- 5 ;
5 -- 2 ;
2 -- 7 ;
}
```
可以把邊權當成經過時需要支付的過路費
也可以把無權圖當做邊權都相同的帶權圖
---
## 存圖
* 鄰接矩陣
* 鄰接串列
----
### 鄰接矩陣
使用$n^{2}$大小的二維陣列儲存一張圖
其中$n$為點的數量
$ary[a][b] = 1$表示可從a走到b
----
```graphviz
graph exp
{
rankdir = LR
1 -- 2 -- 4 -- 5 -- 1 -- 3;
}
```
| \ | 1 | 2 | 3 | 4 | 5 |
| --- | --- | --- | --- | --- | --- |
| 1 | 0 | 1 | 1 | 0 | 1 |
| 2 | 1 | 0 | 0 | 1 | 0 |
| 3 | 1 | 0 | 0 | 0 | 0 |
| 4 | 0 | 1 | 0 | 0 | 1 |
| 5 | 1 | 0 | 0 | 1 | 0 |
----
### 鄰接串列
用$n$個一維陣列儲存每個點與誰相連
通常都是用這個方法存圖
----
```graphviz
graph exp
{
rankdir = LR
1 -- 2 -- 4 -- 5 -- 1 -- 3;
}
```
1 : 2、3、5
2 : 1、4
3 : 1
4 : 2、5
5 : 1、4
----
```cpp
for(int i = 0; i < m; i++){
int a, b;
cin >> a >> b;
v[a].pb(b);
v[b].pb(a);
}
```
----
[TOJ765](https://toj.tfcis.org/oj/pro/765/)
估一下複雜度
---
## 圖的走訪
dfs
bfs
----
### DFS(深度優先搜尋)
----
```graphviz
graph exp
{
1 -- 2 -- 3 -- 4 -- 5;
1 -- 6 -- 7;
6 -- 8
1 -- 10
7 -- 9
}
```
拜訪順序是$1-2-3-4-5-6-7-9-8-10$
----
```cpp
vector<int> edge[maxn];
bool vis[maxn];
void dfs(int id){
vis[id] = true;
for(auto i : edge[id]){
if(!vis[i]) dfs(i);
}
return;
}
```
----
### BFS(廣度優先搜尋)
minecraft水流
跟格子點的BFS很像
----
```graphviz
graph
{
1 -- 2 -- 3 -- 4 -- 5;
1 -- 6 -- 7;
6 -- 8
1 -- 10
7 -- 9
}
```
拜訪順序是$1-2-6-10-3-7-8-4-9-5$
也就是先依序拜訪起點、從起點走一步可以到的點、從起點走兩步可以到的點、從起點走三步可以到的點...
----
```cpp
while(q.size()){
int a = q.front();
q.pop();
if(vis[a]) continue;
vis[a] = true;
for(auto w : v[a]){
if(!vis[w]) q.push(w);
}
}
```
----
格子點BFS
[cses1193](https://cses.fi/problemset/task/1193)
[toj432](https://toj.tfcis.org/oj/pro/432/)
----
[cses1192](https://cses.fi/problemset/task/1192)
[cses1666](https://cses.fi/problemset/task/1666)
[cses1667](https://cses.fi/problemset/task/1667)
[cses1668](https://cses.fi/problemset/task/1668)
[cses1193](https://cses.fi/problemset/task/1193)
[cses1669](https://cses.fi/problemset/task/1669)
[cses1678](https://cses.fi/problemset/task/1678)
---
## 最短路徑問題
* Dijkstra
* Bellman-Ford
* Floyd–Warshall
----
### Dijkstra
換了名字的BFS
----
Dijk經常使用於單源最短路徑問題,但它有使用條件
**邊權必須大於0**
[cses1671](https://cses.fi/problemset/task/1671/)
----
主要概念
將所有還沒算出最短距離的點中,找出距離起點最近的點A,並將其設為最短距離
再更新A能夠經過一條邊抵達的所有點,判斷是否為更短的距離
~~好抽象~~
----
```graphviz
graph
{
rankdir = LR
a -- b [label = 2]
a -- c [label = 8]
a -- d [label = 8]
b -- e [label = 5]
c -- e [label = 6]
e -- f [label = 6]
d -- f [label = 5]
a[color = green]
}
```
將起點的距離設為0,其他點設為$\infty$
| \ | a | b | c | d | e | f |
| -------- | -------- | --- | --- | --- | --- | -------- |
| 距離 | 0 |$\infty$ |$\infty$ | $\infty$ | $\infty$ | $\infty$|
----
```graphviz
graph
{
rankdir = LR
a -- b [label = 2]
a -- c [label = 8]
a -- d [label = 8]
b -- e [label = 5]
c -- e [label = 6]
e -- f [label = 6]
d -- f [label = 5]
a[color = green]
b[color = red]
}
```
b為尚未算出最短距離中離起點最近的點
所以更新b的最短距離
| \ | a | b | c | d | e | f |
| -------- | -------- | --- | --- | --- | --- | -------- |
| 距離 | 0 |2 |$\infty$ | $\infty$ | $\infty$ | $\infty$|
----
```graphviz
graph
{
rankdir = LR
a -- b [label = 2]
a -- c [label = 8]
a -- d [label = 8]
b -- e [label = 5]
c -- e [label = 6]
e -- f [label = 6]
d -- f [label = 5]
a[color = green]
b[color = green]
e[color = red]
}
```
e為尚未算出最短距離中離起點最近的點
所以更新e的最短距離
| \ | a | b | c | d | e | f |
| -------- | -------- | --- | --- | --- | --- | -------- |
| 距離 | 0 |2 |$\infty$ | $\infty$ | 7 | $\infty$|
----
```graphviz
graph
{
rankdir = LR
a -- b [label = 2]
a -- c [label = 8]
a -- d [label = 8]
b -- e [label = 5]
c -- e [label = 6]
e -- f [label = 6]
d -- f [label = 5]
a[color = green]
b[color = green]
e[color = green]
c[color = red]
d[color = red]
}
```
c為尚未算出最短距離中離起點最近的點
所以更新c的最短距離
d也是,先更新哪個都可以
| \ | a | b | c | d | e | f |
| -------- | -------- | --- | --- | --- | --- | -------- |
| 距離 | 0 |2 |8 | 8 | 7 | $\infty$|
----
```graphviz
graph
{
rankdir = LR
a -- b [label = 2]
a -- c [label = 8]
a -- d [label = 8]
b -- e [label = 5]
c -- e [label = 6]
e -- f [label = 6]
d -- f [label = 5]
a[color = green]
b[color = green]
e[color = green]
c[color = green]
d[color = green]
f[color = red]
}
```
f為尚未算出最短距離中離起點最近的點
所以更新f的最短距離
| \ | a | b | c | d | e | f |
| -------- | -------- | --- | --- | --- | --- | -------- |
| 距離 | 0 |2 |8 | 8 | 7 | 13|
----
```graphviz
graph
{
rankdir = LR
a -- b [label = 2]
a -- c [label = 8]
a -- d [label = 8]
b -- e [label = 5]
c -- e [label = 6]
e -- f [label = 6]
d -- f [label = 5]
a[color = green]
b[color = green]
e[color = green]
c[color = green]
d[color = green]
f[color = green]
}
```
結束
| \ | a | b | c | d | e | f |
| -------- | -------- | --- | --- | --- | --- | -------- |
| 距離 | 0 |2 |8 | 8 | 7 | 13|
----
怎麼維護還沒算出最短距離的所有點中,離起點最近的點
----
通常用priority_queue來維護距離起點最近的點
要用set也是可以
```cpp
#include<bits/stdc++.h>
using namespace std;
#define KCC ios::sync_with_stdio(0);cin.tie(0);
#define pb push_back
#define F first
#define S second
#define pii pair<long long, int>
vector<pair<int, int>> v[(int)2e5 + 5];
long long dis[(int) 2e5 + 5];
int main()
{
int n, m;
cin >> n >> m;
for(int i = 0; i < m; i++){
long long x, y, w;
cin >> x >> y >> w;
v[x].pb({y, w});
}
for(int i = 2; i <= n; i++) dis[i] = 1e15;
priority_queue<pii, vector<pii>, greater<pii>> q;
q.push(make_pair(0, 1));
while(q.size()){
auto a = q.top();
q.pop();
if(dis[a.S] < a.F) continue;
for(auto w : v[a.S]){
if(dis[a.S] + w.S < dis[w.F]){
dis[w.F] = dis[a.S] + w.S;
q.push(make_pair(dis[w.F], w.F));
}
}
}
for(int i = 1; i <= n; i++) cout << dis[i] << ' ';
return 0;
}
```
複雜度?
----
$O(mlogm)$
AC!!!
----
### Bellman-Ford
可以處理邊權為負的情況
~~暴力~~
----
反覆檢查每一條邊,看是否能夠更新到起點的距離
如果將對所有邊檢查一次訂為一個回合,那麼經過$N-1$個回合後,就能算出所有點到起點的最短路徑,否則無解
每個回合檢查$E$條邊,最糟情況$N-1$回合
複雜度O(?)
----
如果第$N$回合還能夠更新距離,那麼存在負環
----
```cpp
void bellman_ford(){
for(int i = 1; i <= n; i++){
bool update = false;
for(int j = 1; j <= n; j++)
for(auto k : g[j])
if(dis[k.F] > dis[j] + k.S)
update = true, dis[k.F] = dis[j] + k.S;
if(!update)break;
if(i == n)cout << "neg_cycle" << "\n";
}
}
```
$O(NE)$
----
### Floyd–Warshall
多對多最短路徑
[cses1672](https://cses.fi/problemset/task/1672/)
----
令$dis[k][i][j]$為$i$走到$j$的最短距離,且經過的所有點中,編號均不超過k
```cpp
dis[k][i][j]
=min(dis[k - 1][i][j], dis[k - 1][i][k] + dis[k - 1][k][j])
```
----
令$dis[k][i][j]$為$i$走到$j$的最短距離,且經過的所有點中,編號均不超過k
```cpp
dis[k][i][j]
=min(dis[k - 1][i][j], dis[k - 1][i][k] + dis[k - 1][k][j])
```
其中
$dis[k - 1][i][k] = dis[k][i][k]$
$dis[k - 1][k][j] = dis[k][k][j]$
----
可以把k那維壓掉
```cpp
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j])
```
```cpp
for(int k = 1; k <= n; k++){
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}
}
}
```
$O(N^3)$
---
## DAG
有向無環圖
----
### 入度&出度
點被指向的箭頭數量就是入度
從點指出的箭頭數量就是出度
```graphviz
digraph topological{
{rank = same a b c d e}
b -> e
c -> e
a -> b
a -> c
a -> d
}
```
| | a | b | c | d | e |
| -------- | -------- | --- | --- | --- | -------- |
| 入度 | 0 | 1 | 1|1 | 2|
| 出度 | 3 | 1 | 1| 0 | 0 |
----
### 拓樸排序(Topological sorting)
訂出一個點的序列${v_1, v_2, ... , v_n}$使得$\forall i \le j$
皆不存在$v_j$到$v_i$的路徑
簡單來說就是把點排成一列然後邊不能往前指
```graphviz
digraph topological{
{rank = same 1, 2 3 4 5}
2 -> 5
3 -> 5
1 -> 2
1 -> 3
1 -> 4
}
```
DAG$\iff$有拓樸排序
但拓樸排序不一定只有一種,
上圖2,3,4怎麼交換都是拓樸排序
by tw87
----
如果有一個點,應該擺在它前面的點都已經放進序列中,那麼這個點便可以放進序列形成拓樸排序
----
[cses1679](https://cses.fi/problemset/task/1679/)
```cpp
while(q.size()){
int x = q.front();q.pop();
ans.pb(x);
for(int i : g[x]){
in[i]--;
if(in[i] == 0) q.push(i);
}
}
```
$O(?)$
----
[cses1679](https://cses.fi/problemset/task/1679/)
```cpp
while(q.size()){
int x = q.front();q.pop();
ans.pb(x);
for(int i : g[x]){
in[i]--;
if(in[i] == 0) q.push(i);
}
}
```
$O(N + E)$
----
[cses1681](https://cses.fi/problemset/task/1681/)
[atcoder_dp_g](https://atcoder.jp/contests/dp/tasks/dp_g)
---
## 圖中的特例 ---- 樹
----
一棵 tree 是一個無向的連通圖,具備以下性質:
* 有剛好$N-1$條邊
* 不存在環
* 任兩點間只存在$1$條簡單路徑
可由其一推得其他性質
----
### 簡單路徑
一條路徑中除了起點終點外,所有的點兩兩相異,稱為簡單路徑。
----
### 根(root)
一棵樹不一定要有根,如果題目沒給通常會隨便找一個點當根(通常是0或1)
```graphviz
graph {
1 [color = red]
1 -- 2
1 -- 3
1 -- 4
2 -- 5
2 -- 6
}
```
----
### 葉(leaf)
在樹上度數只有一的節點稱為葉節點
```graphviz
graph {
3, 4 ,5 ,6 [color = red]
1 -- 2
1 -- 3
1 -- 4
2 -- 5
2 -- 6
}
```
----
### 父節點(parent)&子節點 (child)
有根樹中,兩個相鄰的節點,離根較近的節點是另一個節點的父節點,反之為其子節點 (child)
```graphviz
graph {
1 -- 2
1 -- 3
1 -- 4
2 -- 5
2 -- 6
}
```
---
## 樹直徑(Tree Diameter)
----
樹上的任何最長長度的簡單路徑都是此樹的直徑。
----
### 求法
做兩次$DFS$
1. $從任意點開始做 DFS 並找到最遠的那個點x$
1. $接著把 x 當起點再做一次 DFS 找到距離點 x 最遠的點 y$
$如此一來 x與 y 的距離就是樹直徑$
----
[TIOJ1213](https://tioj.ck.tp.edu.tw/problems/1213)
[cses1131](https://cses.fi/problemset/task/1131)
---
## 最小生成樹
(Minimum Spanning Tree)
----
[cses1675](https://cses.fi/problemset/task/1675)
一張圖中,選一些邊把這張圖連通成樹
問最小邊權和
----
### Kruskal
----
對所有邊依權重排序,由小到大只要邊上兩點屬於不同連通塊,就加進去
----
```graphviz
graph {
rankdir = LR
1 -- 2 [label = 3]
2 -- 3 [label = 5]
2 -- 4 [label = 2]
3 -- 4 [label = 8]
5 -- 1 [label = 7]
5 -- 4 [label = 4]
}
```
----
```graphviz
graph {
rankdir = LR
1 -- 2 [label = 3]
2 -- 3 [label = 5]
2 -- 4 [label = 2, color = red]
3 -- 4 [label = 8]
5 -- 1 [label = 7]
5 -- 4 [label = 4]
2[color = red]
4[color = red]
}
```
----
```graphviz
graph {
rankdir = LR
1 -- 2 [label = 3, color = red]
2 -- 3 [label = 5]
2 -- 4 [label = 2, color = green]
3 -- 4 [label = 8]
5 -- 1 [label = 7]
5 -- 4 [label = 4]
2[color = green]
4[color = green]
1[color = red]
}
```
----
```graphviz
graph {
rankdir = LR
1 -- 2 [label = 3, color = green]
2 -- 3 [label = 5]
2 -- 4 [label = 2, color = green]
3 -- 4 [label = 8]
5 -- 1 [label = 7]
5 -- 4 [label = 4, color = red]
2[color = green]
4[color = green]
1[color = green]
5[color = red]
}
```
----
```graphviz
graph {
rankdir = LR
1 -- 2 [label = 3, color = green]
2 -- 3 [label = 5, color = red]
2 -- 4 [label = 2, color = green]
3 -- 4 [label = 8]
5 -- 1 [label = 7]
5 -- 4 [label = 4, color = green]
2[color = green]
4[color = green]
1[color = green]
5[color = green]
3[color = red]
}
```
----
```graphviz
graph {
rankdir = LR
1 -- 2 [label = 3, color = green]
2 -- 3 [label = 5, color = green]
2 -- 4 [label = 2, color = green]
3 -- 4 [label = 8]
5 -- 1 [label = 7, color = red]
5 -- 4 [label = 4, color = green]
2[color = green]
4[color = green]
1[color = green]
5[color = green]
3[color = green]
}
```
編號1跟編號5已經在同一個連通塊
所以這條邊不選
----
```graphviz
graph {
rankdir = LR
1 -- 2 [label = 3, color = green]
2 -- 3 [label = 5, color = green]
2 -- 4 [label = 2, color = green]
3 -- 4 [label = 8, color = red]
5 -- 1 [label = 7]
5 -- 4 [label = 4, color = green]
2[color = green]
4[color = green]
1[color = green]
5[color = green]
3[color = green]
}
```
編號3跟編號4已經在同一個連通塊
所以這條邊不選
----
```graphviz
graph {
rankdir = LR
1 -- 2 [label = 3, color = green]
2 -- 3 [label = 5, color = green]
2 -- 4 [label = 2, color = green]
3 -- 4 [label = 8]
5 -- 1 [label = 7]
5 -- 4 [label = 4, color = green]
2[color = green]
4[color = green]
1[color = green]
5[color = green]
3[color = green]
}
```
綠色部分便是最小生成樹
----
怎麼判斷兩點是否屬於同一連通塊?
----
DSU
複雜度?
----
DSU
$O(mlogm)$
----
```cpp
sort(ary, ary + m, cmp);
for(int i = 0; i < m; i++){
int p = ary[i].a;
int q = ary[i].b;
int x = findroot(p);
int y = findroot(q);
if(x != y){
ans += ary[i].c;
root[x] = y;
}
}
```
點p跟點q之間有一條邊權ary[i].c的邊
----
[kattis-islandhopping](https://open.kattis.com/problems/islandhopping)
---
新年快樂!!!!
{"title":"基礎圖論","description":"圖論(Graph Theory)是探討關於圖(Graph)這個數學模型的理論。透過研究如何解決 graph 上的各種問題,只要能將題目對應至 graph 這個模型上,就能用 graph 的做法去解這道題目。","contributors":"[{\"id\":\"a86f0f85-1d73-4927-861c-8c385cba4545\",\"add\":20871,\"del\":6192}]"}