圖論2
by yungyao
前言
圖論
圖論是一個非常大的主題
一堆問題都能歸約成圖論問題
所以學好圖論絕對會是競賽程式裡很重要的一部分
常見的圖論問題
相鄰陣列、矩陣DFS/BFSMST樹狀資結(線段樹/BIT)- 最短路問題
- LCA
- DAG DP
- 樹DP
- 樹壓平
- 樹鍊剖分
- Self Balancing Tree
今天的內容
(X)
(X)
(X)
(X)
樹ㄉ部分
單點源最短路
小前提
在接下來的slides裡面
\(V\) 代表點數,\(E\) 代表邊數
\((u,v)\) 代表一條由 \(u\) 通往 \(v\) 的邊
定義問題
- 在一張有向/無向圖上
- 每條邊有一個權重 \(w_e\)
- 一條路徑上邊的權重和(\(\displaystyle\sum_{e\in path}w_e\))就是路徑的長度
- 問你從一個原點出發,到所有點的最短路徑
Dijkstra's Algorithm
在那之前
先來介紹 Bellman-Ford Algorithm
我們可以先很直觀的claim
走最短路徑時,最多只會經過 \(V-1\) 條邊
所以我們只要好好模擬這個過程就好了
But how?
我們先定除了起點外,到每個點的距離都是 \(\infty\)
而若邊 \((u,v)\) 的權重為 \(w\)
則我們可以更新 \(dis[v] = min(dis[v],dis[u] + w)\)
這個過程稱為 鬆弛\((u,v)\) (Relaxation)
也就是把結束在 \(u\) 的路徑多考慮 \((u, v)\) 來延長路徑
鬆弛
1
3
2
\(w = 3\)
\(w = 1\)
\(w = 1\)
\(d = 0\)
\(d = \infty\)
\(d = 3\)
\(d = 2\)
\(d = \infty\)
\(d = 1\)
回來
所以回到Bellman-Ford
我們每做一次鬆弛,實際上就是模擬走了一次邊\((u,v)\)
而既然我們已經有了結論最短路只會走\(V-1\)條邊
那我們只要重複 \(V-1\) 次鬆弛所有邊
就可以用 \(O(VE)\) 的時間得到最短路了
優化
我們可以發現,在Bellman-Ford中
我們做了很多無謂的鬆弛
但如果我們已經知道 \(dis[u]\) 不可能再被減少了
再去鬆弛所有從 \(u\) 出發的邊
就不會浪費了
Then?
那我們要如何知道 \(dis[u]\) 已經是最小呢?
如果權重都是正的,那所有大於 \(dis[u]\) 的點都不能拿來鬆弛 \(u\)
而現在每個點都只作為鬆弛起點一次(也就是已知距離最小了)
因此如果把點分為已知距離最小和還不確定距離能不能更小
還不確定的點裡面距離最小的點就確定是最小了
Dijkstra's Algorithm
用priority queue維護一些 \((dis_i, i)\) 的pair
每次鬆弛成功就把 pair 丟進去
再從priority queue裡面找出 \(dis_i\) 最小且 \(i\) 尚未作為鬆弛起點的點來鬆弛
複雜度:\(O(V+E\log E)\)
Code
vector <pair<int, int>> adj[maxn];
int dis[maxn];
bool vis[maxn];
int dijkstra(int st){
priority_queue <pair<int, int> ,vector<pair<int, int>>, greater<pair<int, int>>> pq;
fill(dis, dis+maxn, inf);
dis[st] = 0;
pq.push(make_pair(0, st));
while (!pq.empty()){
auto [d, at] = pq.top(); pq.pop();
if (vis[at]) continue;
vis[at] = true;
for (auto [nx, w]:adj[at]){
if (d + w < dis[nx]){
dis[nx] = d + w;
pq.push(make_pair(dis[nx], nx));
}
}
}
}
Bellman-Ford vs Dijkstra
乍看之下 Bellman-Ford 好像沒什麼用途
然而 Dijkstra 很重要的是無法處理任何有負邊的圖
另外 Bellman-Ford 如果在第 \(V\) 輪鬆弛依然成功
我們可以推論圖上有負環
為甚麼需要Bellman-Ford
次短路徑
給一張圖
對每個點 \(i\) 找 \(\min\{2d(i,j) + a_j\}\)
\(d(i,j)\) 為 \(i\) 到 \(j\) 的最短路
\(n \leq 2e5\)
Hint:
如果構造假的點?
如果邊權是 \(T\) 線性函數?
求 \(u\) 到 \(v\) 最短路
\(\sum_{i \in path} C_i + (T-1)P_i \\ = \sum_{i \in path} C_i + (T-1)\sum_{i\in path} P_i\)
Hint \(\downarrow\)
全點對最短路
Floyd-Warshall Algorithm
這大概是今天最簡單的一個章節
扣的只有4行
扣
int dis[maxn][maxn];
void floyd_warshall(){
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(V^3)\)
p.s. 要預處理 \(dis[u][v]\) 拉
Why
其實跟Bellman-Ford的證明有點相似
可以自己想想,只是從單點源變成多點源而已
DFS 用途
DAG與拓樸排序
什麼是拓樸排序?
對於一張有向圖 \(G\)
我們考慮一個所有點的排序方式 \(P\)
使對於圖上的所有邊 \((u,v)\)
\(u\) 在 \(P\) 當中的位置必定在 \(v\) 前面
例如
存在多組拓樸排序
etc. \((1,3,6,2,5,4,7), (1,2,3,4,6,5,7)\) and more
1
7
4
5
6
2
3
一些引理
- 對於一張有向圖 \(G\),\(G\) 是一張有向無環圖和 \(G\) 存在拓樸排序互為充要條件
- 拓樸排序不唯一
- 令 \(P\) 為圖 \(G\) 的一組拓樸排序,則將點 \(P_1\) 由 \(G\) 中移除,\(G\) 仍為有向無環圖 (可由第一條推導)
如何找拓樸排序
我們再觀察一下DAG,應該可以發現
\(P_1\) 必為入度為 0 的點
而再套回前面的定理,我們如果每次都把 \(P_1\) 拔掉
那一定還是會存在至少一個入度為 0 的點
也就是說
我們可以不斷拔掉入度為 0 的點,並加入拓樸排序
直到所有點都被拔掉aka加入拓樸排序為止
Try it
拓樸排序:
1
7
4
5
6
2
3
1
7
4
5
6
2
3
如何實作
我們只要維護每個點的入度
在拔掉點的時候,順便也去更新入度
不論用DFS或BFS都能實作
code
int inDeg[maxn];
vector <int> graph[maxn];
//dfs
//run dfs(x) on all x with in-degree of 0 without removing edges
void dfs (int x){
for (auto i:graph[x]){
--inDeg[i];
if (!inDeg[i])
dfs(i);
}
}
//bfs
void bfs(){
queue <int> bfs;
for (int i=1;i<=maxn;++i){
if (!inDeg[i])
bfs.push(i);
}
while (bfs.size()){
int x = bfs.front();
bfs.pop();
for (auto i:graph[x]){
--inDeg[i];
if (!inDeg[i])
bfs.push(i);
}
}
}
給定\(n\)個小寫英文字母組成的字串
請重新建立一組英文字母的排序方式
使那 \(n\) 個字串是按字母序排序好的,或輸出無解
所以DAG能幹麻
我們若把DAG上的每個點都視為DP狀態,邊視為轉移
則我們可以很輕易的證明
只要按照拓樸排序做轉移
則就是一個好的DP方式
btw其實所有DP問題都能被轉成DAG的形式
例題
給一張圖,求至少需要選幾個點
才能從這些點出發走過所有點
給定一張帶點權的DAG
求最大路徑
給定一張帶有邊權的DAG
求由原點通往所有點的最短路徑和
邊權可能為負
在一張DAG上求是否存在漢彌爾頓路徑
(Hamilton Path指一條經過所有點的簡單路徑)
有 \(n\) 台車在數線上,可能往正向或負向走
其中有 \(m\) 種關係
表示 \(a, b\) 兩台車不可能相撞或一定會相撞
請構造一組合法每台車的左右關係
或輸出無解
LCA
1
2
3
4
5
6
7
9
8
\(lca(4,7) = 2\)
\(lca(2,8) = 1\)
\(lca(8,9) = 6\)
\(lca(3,8) = 3\)
我假設你們都知道LCA是什麼了
那除了找到LCA本身之外
更重要的是可以找到路徑上的東西
因此LCA可以求的東西會非常多
倍增
先來看一個題目
現在有一個長條狀的棋盤
而第 \(i\) 格上會有一個指示 \(a_i\)
表示走到這格後下一步要走到第 \(a_i\) 格
那現在問你從第 \(c\) 格開始走 \(k\) 步
你會結束在哪裡
請用 \(O(\log n)\) 的複雜度回答這個問題(預處理\(O(n \log n)\))
Ans
對於每個點,我們都去存從該點再走 \(2^j(j \in N)\) 步的位置
也就是令 \(f[i][j]\) 為從點 \( i \) 走 \(2^j\) 步的位置
而每次查詢時,我們都把走 \(k\) 步拆解成 \(2\) 的冪次
就可以用 \(\log k\) 的時間達成回答
p.s. 這叫 binary lifting 或按位二分搜 by youou
好啦這也叫binary jump或倍增法,反正名字很多
回來LCA
由一樣的想法,但我們現在對於每一個點
都儲存其上 \(2^k(k \in N)\) 輩的祖先
接下來就可以把倍增的想法套上來ㄌ
Step by Step求\(lca(u,v)\)
- 先將兩個點的深度拉為相同(可以透過記錄每個點的深度為 \(d_u,d_v\),再將比較深的那個點往上拉 \(|d_u-d_v|\)格
- 接下來令 \(k\) 由最大的可能值 (\(\log n\)) 開始遞減,如果兩點的上\(2^k\)輩祖先是相異的點代表祖先還不一樣,那就讓兩點都向上爬\(2^k\)格,否則不做任何處理。
- 最後必定會走到兩個點的\(lca\)下方一層,再各往上爬一格就會抵達\(lca(u,v)\)了
1
2
3
4
5
6
7
9
8
求\(lca(4,8)\)
Complexity
- 空間複雜度\(O(VlogV)\)
- 預處理時間複雜度\(O(VlogV)\)
- 每次詢問複雜度\(O(logV)\)
然後勒
你會發現,每次往上爬的過程其實都是在走樹上的一段路徑
所以你也可以把這段路徑的某些資訊存起來
進而回答一些其他問題(例如樹上兩點的距離)
例題
求兩點間\(lca\)及距離
和 root 的距離關係?
Hint:
給定一棵樹,多筆詢問
每筆詢問問你由 \(S\) 往走最短路徑到 \(T\)
第 \(k\) 步會停在哪個點 或是超過 \(T\) ?
給定一張圖,多筆詢問
每筆詢問問你兩點路徑中,經過的最大邊權的最小值
(這題跟Floyd-Warshall的例題一樣,但\(V\)變大了)
給你一張圖
對每條邊,都求出包含該邊的最小生成樹
次小生成樹
圖論2
By yungyao
圖論2
- 412