Winter Camp

- Graph

Table

GRAPH

intro

representations

basic traversal

sssp, apsp

binary lifting/functional graph

mst

scc/k-sat

euler tour

hamiltonian path

flow

Basics

1

4

3

6

5

2

1

4

6

5

3

2

Isomorphism

1

4

3

6

5

2

同構性

這兩張圖是一樣的

1

4

6

5

3

2

Connection

1

4

3

6

5

2

Connected

Disconnected

1

4

6

5

3

2

Vertex

1

4

6

5

3

2

Edge

1

4

6

5

3

2

Path

路徑

有起點跟終點的一條路

1

4

6

5

3

2

Edge Weight

3

6

7

2

1

邊權

1

4

6

5

3

2

Vertex Weight

3

6

7

2

1

7

點權

1

4

6

5

3

2

Component

連通塊

Component 1

Component 2

1

4

6

5

3

2

Cycle

1

4

6

5

3

2

Directed Graph

有向圖

1

4

6

5

3

2

Directed Acyclic Graph

有向無環圖 簡稱 DAG

1

4

6

5

3

2

Degree

Indeg: 1

Outdeg: 1

Indeg: 0

Outdeg: 1

Indeg: 0

Outdeg: 1

Indeg: 0

Outdeg: 0

Indeg: 0

Outdeg: 2

Indeg: 4

Outdeg: 0

度數

Indeg - 入度

Outdeg - 出度

1

4

6

5

3

2

Simplicity

簡單性質

通常一個邊會以(u, v)表示\\ 意思是由u往v走\\ 另一種方式可能是(a, b) \\ w 通常代表邊權(weight)\\ n 通常代表點數 \\ m 代表邊數 \\ MAXN 就是點的上限
u
v
a
b
n = 4, m = 2
沒有MAXM?

練習題:

Representation

Graph - 圖, \: 數學上會寫成 \:G = (V, E) \\[2ex] Sparse \: Graph - 稀疏圖 (邊不多的圖) \\ Dense \: Graph - 密集圖 (邊趨近於V^2的圖)
就是開一個陣列 \\ 存 u, v點, 還有它們之間的資訊

Edge List

Edge List

1

4

6

5

3

2

3

6

7

2

1

{1, 2, 6} {1, 4, 3} {1, 5, 7}  {2, 3, 1} {3, 1, 2}

Edge List - el:

#include <vector>

template<typename T>
struct edge
{
    int u, v;
    T data;
};

std::vector<edge<int>> edge_list;
# PRESENTING CODE
\mathcal{O}(E)
對於每一個點 \\ 存取可以到達終點和路線的資訊\\

Adjacency List

Adjacency List

1

4

6

5

3

2

3

6

7

2

1

2, 6 4, 3 5, 7
3, 1
1, 2
1
2
3
4
5
6

Code:

#include <vector>

template<typename T>
struct edp
{
    int ed;
    T data;
};
std::vector<edp<int>> al[MAXN];
# PRESENTING CODE
\mathcal{O}(V + E)
開二維陣列存edge \: info

Adjacency Matrix

Adjacency Matrix

1

4

6

5

3

2

3

6

7

2

1

1 2 3 4 5 6
1 0 6 3 7
2 0 1
3 2 0
4 0
5 0
6 0

Code:

#include <vector>

std::vector<vector<int>> am;
# PRESENTING CODE
\mathcal{O}(V^2)

Traversal

DFS

  • 中文翻譯是深度優先搜尋(depths first search)

  • 簡單來說就是一直走直到碰壁倒退或找到終點

  • 因為code很短所以常常用

DFS

1

4

3

6

5

2

DFS

1

4

3

6

5

2

DFS

1

4

3

6

5

2

DFS

1

4

3

6

5

2

DFS

1

4

3

6

5

2

DFS

1

4

3

6

5

2

DFS

1

4

3

6

5

2

DFS

1

4

3

6

5

2

DFS

1

4

3

6

5

2

DFS

1

4

3

6

5

2

DFS

1

4

3

6

5

2

DFS

1

4

3

6

5

2

DFS

1

4

3

6

5

2

Code:

void dfs(int u)
{
	visited[u] = 1
    for (int v : al[u])
    	if (!visited[v])
        	dfs(v);
}
# PRESENTING CODE
\mathcal{O}(V + E)

BFS

  • 中文翻譯是廣度優先搜尋breadth first search

  • 從起點開始逐漸往外層層擴散

  • 因為要用queue寫好麻煩,所以單純遍歷用dfs就好

BFS

1

4

3

6

5

2

BFS

1

4

3

6

5

2

BFS

1

4

3

6

5

2

BFS

1

4

3

6

5

2

BFS

1

4

3

6

5

2

BFS

1

4

3

6

5

2

BFS

1

4

3

6

5

2

BFS

1

4

3

6

5

2

BFS

1

4

3

6

5

2

BFS

1

4

3

6

5

2

BFS

1

4

3

6

5

2

BFS

1

4

3

6

5

2

BFS

1

4

3

6

5

2

Code:

queue<int> bfs;
bfs.push(start);

while (!bfs.empty())
{
	int u = bfs.front();
    bfs.pop();
    
    visited[u] = 1;
    for (int v : al[u])
    	if (!visited[v])
        	bfs.push(v);
}	
# PRESENTING CODE
\mathcal{O}(V + E)

例題:

給你一個\:7\times 7的矩陣和一個字串 S\newline 字串由U,D,L,R,?\:組成,?代表當下如何移動未知\newline 問有幾個走法能從(0,0)走到(0,6) 且一格只能走一次
S = 48
把每一格想成是一個點 \\ 答案就是從起點dfs可以到達終點的所有方法\\ 在讀取到問號時,上下左右各走一遍 \\

Code:

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
 
string s;
bool visited[7][7];
int ans = 0;
 
void dfs(int i, int j, int steps = 0) 
{
 
    if (i == 6 && j == 0) 
    {
        if (steps != 48)
            return;
        ++ans;
    }
 
    if ((i == 0 || visited[i - 1][j]) && (i == 6 || visited[i + 1][j]) && j > 0 && j < 6 && !visited[i][j - 1] && !visited[i][j + 1]) 
    {
        visited[i][j] = false;
        return;
    }
 
    if ((j == 0 || visited[i][j - 1]) && (j == 6 || visited[i][j + 1]) && i > 0 && i < 6 && !visited[i - 1][j] && !visited[i + 1][j]) 
    {
 
        visited[i][j] = false;
        return;
    }
 
    visited[i][j] = true;
    if (s[steps] == '?' || s[steps]  == 'U') 
        if ( i && !visited[i-1][j]) 
            dfs(i-1, j, steps + 1);

    if (s[steps] == '?' || s[steps]  == 'D') 
        if ( i < 6 && !visited[i+1][j]) 
            dfs(i+1, j, steps + 1);
        
    
    if (s[steps] == '?' || s[steps]  == 'L') 
        if ( j && !visited[i][j-1]) 
            dfs(i, j-1, steps + 1);
    
    if (s[steps] == '?' || s[steps]  == 'R') 
        if ( j < 6 && !visited[i][j+1]) 
            dfs(i, j+1, steps + 1);
        
    visited[i][j] = false;
}
 
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
 
    cin >> s;
    dfs(0, 0);
 
    cout << ans;
}
# PRESENTING CODE

練習題:

Shortest Path

如果已經知道有一個最短路徑 \\ 已知這個路徑長度為k \\ 則 \: k < n \\ 因此只要做 n-1次 "鬆弛" \\ 即可找到最短路徑
鬆弛為何意味
首先要知道 \\[2ex] 假設要求起點到某一點的位置 \\ 則若起點為u, 終點為v\\ \\而有一個中繼點 k\\ 則u點到v點的最短路徑有可能會是\\ u \rightsquigarrow k \rightsquigarrow v
u點到k點的最短路徑應也有中繼點 \\ 一直找中繼點到最後就是直接連接u點的點 \\ 因此不斷從起點往外推點到鄰居點的距離 \\ 就會有最短路徑

s

t

s

t

s

t

s

t

s

t

Bellman Ford:

while (--n)
{
	for (auto [a, b, len] : el)
    	if (dist[a] + len < dist[b])
        	dist[b] = dist[a] + len
        if (dist[b] + len < dist[a])
        	dist[a] = dist[b] + len

}
# PRESENTING CODE
\mathcal{O}(V^2)
若今天邊權不會 < \: 0 \\ 則對於現在可以到達的全部點 \\ 每次都選擇邊權最小的點去 \\ 當第一次到達某一點時 \\ 即道該點的最短路徑

Dijkstra:

priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq
vector<int> dist(n, 0x7fffffff)

pq.push(0, start)

while (!pq.empty)
	w = pq.top().first, v = pq.top().second
    pq.pop()
    
    if (dist[v] != w)
    	continue
    for (a, b : al[v])
    	if (dist[v] + a < dist[b])
        	dist[b] = dist[v] + a
            pq.push({dist[b], b});
    
    
# PRESENTING CODE
\mathcal{O}(E\log{V})
假設要從\: i \:去\: j\:點 \\ 假設\: i, j 之間有個中間點\: k\\ 那如果知道\: i\: to\: k, k\: to\: j \:的距離\\ 就可以知道\: i\: to\: j \:的距離 \\

Floyd Warshall:

vector<vector<int>> dp(MAXN, vector<int>(MAXN, 0x7fffffff))
for (int i=1; i <= n; i++)
	dp[i][i] = 0
    
while (m--)
	u, v, w
    cin >> u >> v >> w
    dp[u][v] = dp[v][u] = w

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])
        	
# PRESENTING CODE
\mathcal{O}(V^3)

例題:

給你n個點m個邊 (a, b, c)\\ 如果你有一個可以把一個邊權減半的東西\\ 問1 \rightarrow n 的最短路徑
2 \leq n \leq 10^5 \\ 1 \leq m \leq 2 \times 10^5 \\ 1 \leq a, b \leq n \\ 1 \leq c \leq 10^9 \\
直接用dijkstra\\ 但每個點有已經用折價券和沒有用兩種值\\

Code:

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INF = 1e18 + 114514;
 
int main()
{
    int n, m; cin >> n >> m;
    vector<vector<pair<int, int>>> al(n + 1);
    while (m--)
    {
        int a, b, c; cin >> a >> b >> c;
        al[a].push_back({b, c});
    }
    vector<array<ll, 2>> dist(n+1, array<ll, 2>({INF, INF}));
    dist[1][0] = 0; dist[1][1] = 0;
 
    priority_queue<tuple<ll, int, bool>, vector<tuple<ll, int, bool>>, greater<>> pq;
    pq.push({0, 1, 0}); pq.push({0, 1, 1});
    while (!pq.empty())
    {
        auto [cost, cur_node, used] = pq.top(); pq.pop();
        if (dist[cur_node][used] != cost) continue;
 
        for (auto &[v, w] : al[cur_node])
        {
            if (dist[v][0] > dist[cur_node][0] + w)
            {
                dist[v][0] = dist[cur_node][0] + w;
                pq.push({dist[v][0], v, 0});
            }
            if (dist[v][1] > dist[cur_node][0] + w/2)
            {
                dist[v][1] = dist[cur_node][0] + w/2;
                pq.push({dist[v][1], v, 1});
            }
            if (dist[v][1] > dist[cur_node][1] + w)
            {
                dist[v][1] = dist[cur_node][1] + w;
                pq.push({dist[v][1], v, 1});
            }
        }
    }
    cout << min(dist[n][0], dist[n][1]);
}
# PRESENTING CODE

練習題:

Topological Sort

做事情有順序 \\ 像是要先吃早餐才能吃午餐的 \\ 圖也可以表示順序\\ 在一個有向無環圖裡\\ 若有 (u, v) 則代表 u 必須比 v 還早完成
  • 中文翻譯是拓樸排序

  • 簡單來說就是找順序

  • 只能用在DAG

有一種想法是 \\[2ex] 因為沒有入度代表是最早的事情\\ 所以先把全部沒有入度的點處理完\\ 再把這些點可以到達的點的入度 - 1 \\ 持續直到全部做完就可以了

Kahn's algorithm:

#include <bits/stdc++.h>
using namespace std;
 
int main()
{
    int n, m; 
    cin >> n >> m;
    vector<int> indeg(n+1);
    vector<vector<int>> al(n+1);
    
    while (m--)
    {
        int a, b; cin >> a >> b;
        al[a].push_back(b);
        indeg[b]++;
    }
    queue<int> bfs;
    for (int i=1; i <= n; i++)
        if (!indeg[i]) bfs.push(i);
 
    vector<int> order;
    while (!bfs.empty())
    {
        int u = bfs.front(); 
        bfs.pop();
        order.push_back(u);
        for (int v : al[u])
        {
            --indeg[v];
            if (!indeg[v]) bfs.push(v);
 
        }
    }
}
# PRESENTING CODE
\mathcal{O}(V + E)
有點像bfs \\
另一種想法是dfs \\ 如果dfs直到沒有點可以去 \\ 那現在在的點就是最"晚"的點 \\

DFS:

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 2e5 + 215;
bool visited[MAXN];
vector<int> al[MAXN];
vector<int> order;

void dfs(int u)
{
	visited[u] = 1;
	for (int v : al[u])
    	if (!visited[v])
        	dfs(v);
            
    order.push_back(u);
}

int main()
{
	int n, m;
    cin >> n >> m;
    while (m--)
    {
    	int a, b;
        cin >> a >> b;
        al[a].push_back(b);
    }
    
    dfs(1);
    reverse(order.begin(), order.end());
    
}
# PRESENTING CODE
\mathcal{O}(V + E)

例題:

例題:

模板題

SCC

Strongly Connected Component

  • 中文翻譯是強連通分塊

  • 簡單來說就是在一個連通分塊裡面點與點之間可以互通

  • 一定是有向圖

如何在一個圖裡面找到SCC?
如何在一個圖裡面找到SCC?
kosaraju 說我們要先認識\\ "condensation \: graph"

1

3

6

6

5

2

4

4

4

7

0

8

9

1

3

6

6

5

2

4

4

4

7

0

8

9

可以注意到\\ 如果把每個SCC當成一個一個vertex \\ 它們會形成一個DAG
如果從隨機起點dfs \\ 每一個點完成搜尋時放進一個stack\\ 最後我們用這個stack的順序\\ 跑dfs在一個邊都是反轉的圖\\ 那由於依照順序dfs\\ 跑到一個已經visited的點時\\ 代表它屬於另一個SCC\\ 在condensation \: graph裡面\\ 可以注意到這個時間順序會讓scc卡在這趟dfs裡面

Kosaraju:

#include <bits/stdc++.h>
using namespace std;
 
const int MAXN = 2e5 + 10;
int n, m;
static bool visited[MAXN];
vector<vector<int>> al(MAXN);
vector<vector<int>> ral(MAXN);
stack<int> ft;
 
void dfs(int u)
{
    for (int v : al[u])
    {
        if (!visited[v])
        {
            visited[v] = 1;
            dfs(v);
        }
    }
    ft.push(u);
}
void rdfs(int u, int &id)
{
    for (int v : ral[u])
    {
        if (visited[v]) continue;
 
        visited[v] = 1;
        p[v] = id;
        rdfs(v, id);
    }
}
int main()
{
    cin >> n >> m;
    
    while (m--)
    {
        int a, b; cin >> a >> b;
        al[a].push_back(b);
        ral[b].push_back(a);
    }
    
    for (int i=1; i <= n; i++)
        if (!visited[i]) 
            {
                visited[i] = 1;
                dfs(i);
            }
 
    memset(visited, false, sizeof(visited));
    int cnt = 0;
    while (!ft.empty())
    {
        while (!ft.empty() && visited[ft.top()] ) ft.pop();
        if (ft.empty()) break;
        cnt++;
        
        int u = ft.top(); ft.pop();
        visited[u] = 1;
        p[u] = cnt;
        rdfs(u, cnt);
    }
}
# PRESENTING CODE
如果有人問為什麼沒有tarjan \\
im \: too \: bad \: at \: cp

例題:

2 - sat 模板題
給你一堆像這樣的boolean \: conjunction \\ (!a \:\lor \:b) \land (!c \:\lor \:d) \land (!e \:\lor \:!k)...... \\ 問如何排列 a, b...為0或1才能讓式子成立
考慮式子什麼時候會矛盾 \\ \longrightarrow當有 !x 和 x 同時必須存在時矛盾\\ 假設有一個or式子長這樣 (x \lor !y) \\ x = 0隱含 y = 0, y = 1隱含 x = 1 \\ 拉到圖論的視角就是 !x指向!y,\: y 指向 x
矛盾的條件是x 最終指向 !x\\ 放到SCC的視角就是\\ x 跟 !x在同一個SCC裡面
你們都是電神 \\ 你們可以自己寫

練習題:

Tree

Definition

1

4

6

5

3

2

聽電神說樹都是往下長的 \\[3ex] N \: verticies, \: N-1 \: edges \\ 點與點之間只有一條路徑 \\[2ex] 圖如果全部的component都是tree\\ 它就是一個forest

Definition

1

4

3

6

5

2

根據 "isomorphism" \\ 可以把前一頁的圖這樣表示

Definition

1

4

3

6

5

2

我們把最上面的點(1) 叫做根節點\: root\\ 一個點的上面是他的父節點\: parent \: node\\ 一個點下方的點是他的子節點 \: child \: node\\ 沒有子節點的叫做葉節點leaf \: node
由於我樹學不好, \:所以今天先不講太多樹

spanning tree

所有的點都有連起來的tree圖就是\\ spanning \:tree

Minimum Spanning Tree

  • 中文翻譯是最小生成樹

  • 用最小花費本全部的點連起來

  • 然後輸入的圖要是connected的

1

4

6

5

3

2

1

6

7

5

4

9

8

10

2

3

1

4

3

6

5

2

1

5

4

2

3

MST

找到最小生成樹的大略步驟是
1.let\:set \:S = \emptyset \\ 2. while \: S \neq spanning \: tree\\ 3. find \: safe \: edge (u, v)\\ 4.return \:S

*每一步都會找到邊(u, v) 且它會符合mst的定義 則 (u, v) 是一個 safe edge

如何找到MST?
prim說如果我們現在在找一個\\ 有全部的點的樹\\ 那對於我們現在在建的這一棵樹\\ 應該找一個花費最少且不在集合裡面的點\\ \\[2ex] 然後其實有點像dijkstra

步驟:

步驟:

1. 隨便抓一個點

步驟:

1. 隨便抓一個點

2. 預設到起點花費 = 0

 

步驟:

1. 隨便抓一個點

2. 預設到起點花費 = 0

3. 把起點丟到一個 min heap

步驟:

1. 隨便抓一個點

2. 預設到起點花費 = 0

3. 把起點丟到一個 min heap

4. 把可以到的所有點和其距離加到heap裡面

步驟:

1. 隨便抓一個點

2. 預設到起點花費 = 0

3. 把起點丟到一個 min heap

4. 把可以到的所有點和其距離加到heap裡面

5. 選一個距離最小且沒有加進來的點

步驟:

1. 隨便抓一個點

2. 預設到起點花費 = 0

3. 把起點丟到一個 min heap

4. 把可以到的所有點和其距離加到heap裡面

5. 選一個距離最小且沒有加進來的點

6. 更新可以到達的點的距離

Prim:

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int n, m;
 
int main()
{
    cin >> n >> m;
    vector<vector<pair<int, int>>> al(n+1);
    while (m--)
    {
        int a, b, c; cin >> a >> b >> c;
        al[a].push_back({b, c});
        al[b].push_back({a, c});
    }

    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq;
    pq.push({0, 1});
    vector<bool> visited(n+1, false);
    ll total = 0;
    while (!pq.empty())
    {
        auto [w, u] = pq.top(); pq.pop();
        if (visited[u]) 
        	continue; 
            
        visited[u] = 1;
        total += w;
        for (auto &[v, d] : al[u]) 
            if (!visited[v]) 
            	pq.push({d, v});
    }
    cout << total;
}
# PRESENTING CODE

還有另一種方法

kruskal 說先把全部的點想成是單獨的樹\\ 我們要做的就是把樹全部合併\\ 每一次找到兩個不在同一個樹的集合\\ 把它們用最小花費的邊連起來

Kruskal:

#include <bits/stdc++.h>
using namespace std;

int n, m;
const int MAXN = 2e5 +215;
int main()
{
    cin.tie(nullptr)->sync_with_stdio(false);

    cin >> n >> m;
    while (m--)
    {
        int a, b, c;
        cin >> a >> b >> c;
        saves.push_back({a, b, c});
    }

    sort(saves.begin(), saves.end(), [&](auto t1, auto t2)
         {
             auto [a, b, c] = t1;
             auto [d, e, f] = t2;
             return c < f;
         });

    long long ans = 0;
    int tn = 0;
    for (auto [u, v, c] : saves)
        if (fd(u) != fd(v))
            unions(u, v), ans += c, ++tn;
}
# PRESENTING CODE
kruskal裡面的union \_ set \: function \\ 是從一個很重要叫 DSU 的資料結構來的 \\

DSU?

  • disjoint set union, 併查集

  • 可以支援合併兩個集合, 和查詢所屬集合

  • N 個點組成的森林

Naive DSU:

int find_set(int v) 
{
    if (v == p[v])
        return v;
    return find_set(p[v]);
}

void union_sets(int a, int b) 
{
    a = find_set(a);
    b = find_set(b);
    if (a != b)
        p[b] = a;
}
# PRESENTING CODE
有一個啟發式的優化說的是\\ 我們應該union \: by \: size \\ 聽說這樣可以讓union變成 \mathcal{O}(\alpha (n)) 的時間 \\ 證明有點複雜所以先不講(因為我不會)

Size Optimization:

bool union_set(int a, int b)
{
    a = find_set(a);
    b = find_set(b);
    if (a == b) return 0;
 
    if (sz[a] < sz[b]) swap(a, b); 
    parent[b] = a;
    sz[a] += sz[b];
   
    return 1;
}
# PRESENTING CODE
另一個優化是path \: compression \\ 找根節點時把自己連接到根節點

Path Compression:

int find_set(int v) 
{
    if (v == p[v])
        return v;
    return p[v] = find_set(p[v]);
}
# PRESENTING CODE

flow

聽說MLG說不要教flow

Additional

Seven Bridges of Königsberg

  • 據說是圖論的起源

  • Euler thing

Seven Bridges of Königsberg

題目說要走過繞城市一圈\\ 但橋皆需要走過一次且只能一次 \\ 這就是Eulerian \: Circuit \: Problem

Eulerian Circuit Problem

把一個圖的邊都走過一次\\ 回到起點稱為Eulerian \: Circuit \\ 沒有回到起點是Eulerian \: Path
首先要知道每一個點的入度 = 出度 \\ 且路徑長度為 m + 1
已知可以形成Euler \: Circuit\\ 如何找到它?
正常變歷就好但是走過的邊要刪掉

例題:

例題:

相信你們可以自己寫

Hamiltonian Path

  • 哈密瓜路線

  • 把全部的點都走過一遍且只有一遍

哈密瓜路線

哈密瓜路線是NP-complete題 \\ 直接暴力枚舉就好

例題:

讓一個knight走完西洋棋盤 \\ 且每一個點都只走過一次
西洋棋盤有64格 \\ 所以如果暴力走完要 \sim \: 8 ^ {64} \: operations \\ 2 ^ {192}有點多, 所以需要一點點啟發式算法 \\
Warnsdorff \: Algorithm
你們都是電神所以可以自己寫

謝謝roychuang的教並回答 讓我不再納悶

Graph

By treeman667

Graph

  • 72