圖論2

by yungyao

前言

圖論

圖論是一個非常大的主題

一堆問題都能歸約成圖論問題

所以學好圖論絕對會是競賽程式裡很重要的一部分

常見的圖論問題

  • 相鄰陣列、矩陣
  • DFS/BFS
  • MST
  • 樹狀資結(線段樹/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的形式

例題

給一張圖,求至少需要選幾個點

才能從這些點出發走過所有點

2010 TOI-pre pB

TIOJ1717/zj a454

給定一張帶點權的DAG

求最大路徑

2020全國賽pB

zj f628/TIOJ 2224

給定一張帶有邊權的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)\)

  1. 先將兩個點的深度拉為相同(可以透過記錄每個點的深度為 \(d_u,d_v\),再將比較深的那個點往上拉 \(|d_u-d_v|\)格
  2. 接下來令 \(k\) 由最大的可能值 (\(\log n\)) 開始遞減,如果兩點的上\(2^k\)輩祖先是相異的點代表祖先還不一樣,那就讓兩點都向上爬\(2^k\)格,否則不做任何處理。
  3. 最後必定會走到兩個點的\(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