進階圖論

來上數學了 :P

這堂課預設你已學過以下知識:

 

基礎圖論 (大概到最短距離 MST那裡)

基礎dp (能知道dp有一些知識就好了)

BIT (用來解決樹壓平的區間問題)

線段樹 (只有樹頗才會用到 不用太擔心)

如果忘記基礎圖論的話:

講師介紹:

成電CKCSC 38th - 教學 + 網管

翁釩予

MLGnotCOOL

@mlgnotcool

 

自我介紹網站?

講師介紹:

興趣?(除了扣丁之外)

講師介紹:

學術力:

競程、演算法、網頁設計、python

APCS 高級 5+5、北市賽三等獎 第17名

Topological Sort

拓撲排序

我就用我之前做的簡報好了 :U

SCC

Kosaraju's Algorithm

SCC

什麼是 SCC 呢??? (可以吃嗎)

SCC (Strongly Connected Components) 簡單來說

就是一群點,並且這群點中,任何的點都可以互相走到

 

通常用於有向圖中 因為一個連通塊不一定是 SCC

Kosaraju's algorithm

我們找SCC通常會用到這個演算法

他運作的邏輯其實非常簡單:

1. 對有向圖的每個點做DFS,直到每個點都被走過為止。

同時,我們紀錄每個點的exit順序 (就是他完整DFS後的順序)

 

2. 我們將這個順序倒過來,並且把每個邊都翻轉

只要照這個順序看,這個點可以走到的點,都會在同一個SCC裡

Kosaraju's algorithm

那為什麼可以這樣做呢?

假設我們有兩點A B,A的exit順序在B之後

(這樣代表A可以走到B)

那我們將每個邊翻轉,並且從A看的時候 (exit順序比較晚的會先看)。

 

A可以走到B,那就代表在原圖中B可以走到A

就代表他們在同一個SCC內

 

反之則否。

Kosaraju's algorithm

那為什麼可以這樣做呢?

我們可以以此類推,假設有一群點他們exit順序都在點A前面,

我們就可以由上面的推論得出 這些點是否有和A在同一個SCC內了

Kosaraju's algorithm

扣:

void dfs(int x){
    vis[x] = 1;
    for (int i:e[x]){
        if (vis[i]) continue;

        dfs(i);
    }
    order.push_back(x);
}

void rdfs(int x){
    vis[x] = 1;
    for (int i:re[x]){
        if (vis[i]) continue;

        rdfs(i);
    }
}
for (int i=1; i<=m; ++i){
    int u, v; cin >> u >> v;
    e[u].push_back(v);
    re[v].push_back(u);
}

for (int i=1; i<=n; ++i){
    if (!vis[i]){
        dfs(i);
    }
}

reverse(order.begin(), order.end());
for (int i=1; i<=n; ++i) vis[i] = 0;

for (int i:order){
    if (!vis[i]){
        rdfs(i);
    }
}

Kosaraju's algorithm

模板題

應該可以看出來蠻 SCC 的

只要判斷整個圖是否為SCC就好了

Kosaraju's algorithm

扣:

#include <bits/stdc++.h>
#define endl '\n'
#define int long long
#define maxn 100005
using namespace std;
 
int n, m;
vector<int> e[maxn], re[maxn];
vector<int> order, ans;
bool vis[maxn];
 
void dfs(int x){
    vis[x] = 1;
    for (int i:e[x]){
        if (vis[i]) continue;
 
        dfs(i);
    }
    order.push_back(x);
}
 
void rdfs(int x){
    vis[x] = 1;
    for (int i:re[x]){
        if (vis[i]) continue;
 
        rdfs(i);
    }
}
 
signed main(){
    ios::sync_with_stdio(0); cin.tie(0);
 
    cin >> n >> m;
    for (int i=1; i<=m; ++i){
        int u, v; cin >> u >> v;
        e[u].push_back(v);
        re[v].push_back(u);
    }
 
    for (int i=1; i<=n; ++i){
        if (!vis[i]){
            dfs(i);
        }
    }
 
    reverse(order.begin(), order.end());
    for (int i=1; i<=n; ++i) vis[i] = 0;
 
    for (int i:order){
        if (!vis[i]){
            rdfs(i);
            ans.push_back(i);
        }
    }
 
    if (ans.size() == 1) cout << "YES" << endl;
    else{
        cout << "NO" << endl;
        cout << ans[1] << " " << ans[0] << endl; //in reverse, cus the graph is in reverse too
    }
}

Kosaraju's algorithm

先觀察到幾個性質:

1. 一個SCC中全部的金幣都可以被取到

2. 若將每個非在SCC內的邊留下
(就是當作把SCC連起來)

 

他一定會形成一個DAG (因為若有環,就又會形成SCC)

那我們就可以在這個新的圖上做dp找最大值就好了
(如果不會做可以考慮看看這題)

Kosaraju's algorithm

扣:

#include <bits/stdc++.h>
#define endl '\n'
#define int long long
#define maxn 100005
using namespace std;
 
int n, m, a[maxn];
vector<int> e[maxn], re[maxn], se[maxn];
vector<int> order;
bool vis[maxn];
int cur, s[maxn], b[maxn];
int dp[maxn], ans;
 
void dfs(int x){
    vis[x] = 1;
    for (int i:e[x]){
        if (vis[i]) continue;
 
        dfs(i);
    }
    order.push_back(x);
}
 
void rdfs(int x){
    vis[x] = 1;
    s[cur] += a[x];
    b[x] = cur;
    for (int i:re[x]){
        if (vis[i]) continue;
 
        rdfs(i);
    }
}
 
void fdfs(int x){
    vis[x] = 1;
    int cur = 0;
    for (int i:se[x]){
        if (!vis[i]) fdfs(i);
        
        cur = max(cur, dp[i]);
    }
 
    dp[x] = s[x] + cur;
    ans = max(ans, dp[x]);
}
 
signed main(){
    ios::sync_with_stdio(0); cin.tie(0);
 
    cin >> n >> m;
    for (int i=1; i<=n; ++i) cin >> a[i];
    for (int i=1; i<=m; ++i){
        int u, v; cin >> u >> v;
        e[u].push_back(v);
        re[v].push_back(u);
    }
 
    for (int i=1; i<=n; ++i){
        if (!vis[i]){
            dfs(i);
        }
    }
 
    reverse(order.begin(), order.end());
    for (int i=1; i<=n; ++i) vis[i] = 0;
 
    for (int i:order){
        if (!vis[i]){
            ++cur;
            rdfs(i);
        }
    }
    
    for (int u=1; u<=n; ++u){
        for (int v:e[u]){
            se[b[u]].push_back(b[v]);
        }
    }
 
    for (int i=1; i<=n; ++i) vis[i] = 0;
    for (int i=1; i<=cur; ++i){
        if (!vis[i]){
            fdfs(i);
        }
    }
 
    cout << ans << endl;
}

Kosaraju's algorithm

一些題目:(要答案來找我)

2-SAT

SAT

什麼是SAT?

SAT (Boolean satisfiability problem)

是一種數學問題,是將布林值附屬到變數中

並且找到一組可以讓函數成立的問題

(a \lor \lnot b \lor c) \land (\lnot a \lor b) \land (\lnot a \lor \lnot b)

舉例來說,底下為一個SAT問題:

2-SAT

什麼是2-SAT?

2-SAT 是 SAT 問題的一種限制

簡單來說,就是每個括號內都只會有兩個變數

 

(a \lor \lnot b) \land (\lnot a \lor b) \land (\lnot a \lor \lnot b) \land (a \lor \lnot c)

舉例來說,底下為一個2-SAT問題:

2-SAT

CNF (conjuncitve normal form)

SAT問題通常給的形式會是 CNF,

簡單來說就是會是一群括號and起來

並且括號內的變數都取or

 

(a \lor \lnot b) \land (\lnot a \lor b) \land (\lnot a \lor \lnot b) \land (a \lor \lnot c)

舉例來說,底下為一個以CNF表示的SAT問題:

2-SAT

SAT 問題是 NP-compelete

但是 2-SAT 有 O(n+m) 解法 (阿當然我們就是要認識他)

2-SAT

因為每個括號只有兩個變數,

我們先將題目假設為正確的,我們可以列出以下條件

(a \lor \lnot b) \land (\lnot a \lor b) \land (\lnot a \lor \lnot b)
\lnot a \Rightarrow \lnot b \land b \Rightarrow a
a \Rightarrow b \land \lnot b \Rightarrow \lnot a

因為 或 我們一定要使一邊正確

1. 先觀察到以下特質:

a \Rightarrow \lnot b \land b \Rightarrow \lnot a

2-SAT

我們可以將以上條件畫成一個有向圖,如下:

\lnot a \Rightarrow \lnot b \land b \Rightarrow a
a \Rightarrow b \land \lnot b \Rightarrow \lnot a
a \Rightarrow \lnot b \land b \Rightarrow \lnot a

2-SAT

2. 再觀察到這些特質:

假設我們有一個變數x,

1. 若 x 可以走到 not x,則x一定不成立 (互相矛盾)

2. 若 not x 可以走到 x,則 not x 一定不成立 (互相矛盾)

 

結論:

我們可以由此判斷是否有解,

因為若 x 和 not x 在同個 SCC 內,就不會有解

2-SAT

3. 再觀察到這些特質:

我們在Kosaraju's的時候,他有幫我們紀錄exit順序了,

因此我們可以用這個順序來判斷

 

1. x 與 not x 是否在同個 SCC 內

2. x 與 not x 哪個先哪個後 (依性質2推論出)

2-SAT

模板題 相信你們看得懂前面的東西 :)

2-SAT

扣:

#include <bits/stdc++.h>
#define endl '\n'
#define int long long
#define maxn 200005
using namespace std;

int n, m; vector<int> e[maxn], re[maxn];
vector<int> route;
bool vis[maxn], f=true;
int cur, s[maxn];
int ans[maxn];

void dfs(int x){
    vis[x] = 1;
    for (int i:e[x]){
        if (vis[i]) continue;

        dfs(i);
    }
    route.push_back(x);
}

void rdfs(int x){
    vis[x] = 1;
    s[x] = cur;
    for (int i:re[x]){
        if (vis[i]) continue;

        rdfs(i);
    }
}

signed main(){
    ios::sync_with_stdio(0); cin.tie(0);

    cin >> n >> m;
    for (int i=1; i<=n; ++i){
        char c, d; int u, v;
        cin >> c >> u >> d >> v;

        int a = 2*u, b = 2*v, nega = 2*u, negb = 2*v;
        //true -> 2x, false -> 2x-1
        if (c == '+') --nega;
        else --a;

        if (d == '+') --negb;
        else --b;

        e[a].push_back(negb);
        e[b].push_back(nega);
        re[negb].push_back(a);
        re[nega].push_back(b);
    }

    for (int i=1; i<=2*m; ++i){
        if (!vis[i]) dfs(i);
    }

    reverse(route.begin(), route.end());
    for (int i=1; i<=2*m; ++i) vis[i] = 0;
    for (int i:route){
        if (!vis[i]){
            ++cur;
            rdfs(i);
        }
    }

    for (int i=1; i<=2*m; i+=2){
        if (s[i] == s[i+1]) f = false;
        else if (s[i] < s[i+1]) ans[i/2+1] = 0;
        else ans[i/2+1] = 1; 
    }

    if (!f) cout << "IMPOSSIBLE" << endl;
    else{
        for (int i=1; i<=m; ++i){
            if (ans[i]) cout << '+';
            else cout << '-';

            if (i==m) cout << endl;
            else cout << " ";
        }
    }
}

2-SAT

一些題目:(要答案來找我)

中等:

CSES - Giant Pizza

 

沒題目了 :P

Euler tour/curcuit

Euler Tour/Curcuit

就是我們可以在圖上找到一個route
這個route會經過每個邊剛好一次

 

curcuit -> 出發和結束點相同
tour -> 出發和結束點不同

Euler Tour/Curcuit

我們每個點都要有進和出,

那在這個route中的點進出的次數一定會一樣,所以可以推論出:


- 如果圖是無向邊:

除了出發和結束點,每個點的 degree 一定為偶數


- 如果圖是有向邊:

除了出發和結束點,每個點的 in degree = out degree

1. 先觀察到以下特性:

Euler Tour/Curcuit

並且能進一步判斷

 

無向圖中:

如果deg奇數的 = 0 -> 那就會是euler curcuit
如果deg奇數的 = 2 -> 那就會是euler tour

(那兩點就會是出發和結束點)

 

在有向圖中:

如果每個點 in degree = out degree -> 那就會是euler curcuit
如果有一個點 in = out+1,有另一個點 out = in+1 -> 那就會是euler tour 

(那兩點就會是出發和結束點)

2. 再進一步觀察到以下特性:

Euler Tour/Curcuit

那我們只要dfs出去,把經過的邊刪掉就好了

(可以用set來實作)

Euler Tour/Curcuit

扣:

void dfs(int x){
    while (!e[x].empty()){
        int v = *e[x].begin();

        e[x].erase(v);
        e[v].erase(x);
        dfs(v);
    }
    route.push_back(x);
}

前面先判斷是否為euler tour/curcuit (相信你們會做的)

Euler Tour/Curcuit

模板題 相信你們看得懂前面的東西 :)

Euler Tour/Curcuit

扣:

#include <bits/stdc++.h>
#define endl '\n'
#define int long long
#define maxn 200005
using namespace std;
 
int n, m; set<int> e[maxn];
int deg[maxn], f;
vector<int> route;
 
void dfs(int x){
    while (!e[x].empty()){
        int v = *e[x].begin();
 
        e[x].erase(v);
        e[v].erase(x);
        dfs(v);
    }
    route.push_back(x);
}
 
signed main(){
    ios::sync_with_stdio(0); cin.tie(0);
 
    cin >> n >> m;
    for (int i=1; i<=m; ++i){
        int u, v; cin >> u >> v;
        e[u].insert(v);
        e[v].insert(u);
 
        ++deg[u];
        ++deg[v];
    }
 
    for (int i=1; i<=n; ++i){
        if (deg[i]%2 == 1) ++f;
    }
 
    //f == 0 -> euler curcuit
    //f == 2 -> euler tour (starts and end on both nodes)
    if (f != 0) cout << "IMPOSSIBLE" << endl;
    else{
        dfs(1);
 
        bool flag = false;
        for (int i=1; i<=n; ++i){
            if (!e[i].empty()){
                flag = true;
                break;
            }
        }
 
        if (flag) cout << "IMPOSSIBLE" << endl;
        else{
            for (int i=0; i<route.size(); ++i){
                if (i == route.size()-1) cout << route[i] << endl;
                else cout << route[i] << " ";
            }
        }
    }
}

Euler Tour/Curcuit

想想看 要怎們變成 Euler Tour 問題

Euler Tour/Curcuit

我們將每個可能的都標成一個點

我們點之間可以用一個有向邊表示他們相差多少

 

舉例來說:

01 -> 11 的邊會是 1,因為 01加上1 可以變出11

我們最後因為每個邊都要走過,

所以只要跑Euler Tour 就好了

Euler Tour/Curcuit

扣:

#include <bits/stdc++.h>
#define endl '\n'
#define int long long
#define maxn 16
using namespace std;
 
int n;
bool vis[1<<maxn];
set<int> e[1<<maxn];
vector<int> route;
 
void dfs(int x){  
    vis[x] = 1;
    while (!e[x].empty()){
        int i = *e[x].begin();
        e[x].erase(i);
 
        if (!vis[i]) dfs(i);
    }
 
    route.push_back(x&1);
}
 
signed main(){
    ios::sync_with_stdio(0); cin.tie(0);
 
    cin >> n;
    for (int i=0; i<=(1<<n)-1; ++i){
        int v1=0, v2=0;
        if (i & (1<<(n-1))){
            v1 = (i<<1) - (1<<n);
            v2 = ((i<<1) - (1<<n)) | 1;
        }else{
            v1 = (i<<1);
            v2 = (i<<1) | 1;
        }
 
        e[i].insert(v1);
        e[i].insert(v2); 
    }
 
    dfs(0);
    reverse(route.begin(), route.end());
    for (int i=1; i<n; ++i) cout << 0;
    for (int i:route) cout << i;
    cout << endl;
}

Euler Tour/Curcuit

一些題目:(要答案來找我)

樹dp

樹dp

主要都會是從他的subtree往上轉移,

或是從parent往下轉移

剩下就是你dp的能力了

(所以我沒有要講什麼 給你們寫寫看題目好了)

樹dp

我們只要數他subtree有幾個點就好了

 

我們可以從tree最底下的點往上轉移,

每次都加上他leaf node的點數

樹dp

扣:

#include <bits/stdc++.h>
#define endl '\n'
#define maxn 200005
using namespace std;
 
int n, ans[maxn]; vector<int> v[maxn];
 
void dfs(int u, int cnt){
    int curcnt = cnt;
    for (int i: v[u]){
        dfs(i, curcnt);
        cnt += ans[i];
    }
 
    ans[u] = cnt;
}
 
int main(){
    ios::sync_with_stdio(0); cin.tie(0);
 
    cin >> n;
    for (int i=2; i<=n; ++i){
        int a; cin >> a;
        v[a].push_back(i);
    }
 
    dfs(1, 1);
    for (int i=1; i<=n; ++i){
        if (i==n) cout << ans[n]-1 << endl;
        else cout << ans [i]-1 << " ";
    }
}

樹dp

題目有可能有點難懂,我解釋一下:

我們目標是選出一些邊,

使每個點最多只會出現在這些邊一次

 

找出最多會有幾個邊

樹dp

我們先發現一個事情:

我們對於每個點考慮他的subtree,有兩種情況:

 

1 - 取

我們取的話就會是找他leaf node中 不取最小的

取與他的這條邊,且剩下的都加上他們各自最大值

 

2 - 不取

加上 leaf node 他們各自最大值

 

base case - 最底下的當然是 0 條邊

輸出點1的值就好了

樹dp

扣:

#include <bits/stdc++.h>
#define endl '\n'
#define int long long
#define maxn 200005
#define inf 1e18
#define pii pair<int, int>
using namespace std;
 
int n; vector<int> e[maxn];
pii dp[maxn]; //yes no take
 
void dfs(int x, int p){
    if (e[x].size() == 1 && e[x][0] == p) return; //no more edges
 
    int curmax=-inf, curidx=0;
    for (int i:e[x]){
        if (i==p) continue;
 
        dfs(i, x);
        if (dp[i].second - dp[i].first > curmax){
            curmax = dp[i].second - dp[i].first;
            curidx = i;
        }
    }
 
    for (int i:e[x]){
        if (i==p) continue;
 
        dp[x].second += max(dp[i].first, dp[i].second);
 
        if (i!=curidx) dp[x].first += max(dp[i].first, dp[i].second);
        else dp[x].first += dp[i].second + 1;
    }
}
 
signed main(){
    ios::sync_with_stdio(0); cin.tie(0);
 
    cin >> n;
    for (int i=1; i<=n-1; ++i){
        int a, b; cin >> a >> b;
 
        e[a].push_back(b);
        e[b].push_back(a);
    }
 
    dfs(1, 0);
    cout << max(dp[1].first, dp[1].second) << endl;
}

樹dp

樹dp

首先先發現,樹root的答案非常好找 (就把每個點的depth加起來就好了),所以就把這個當base case

接下來,我們對一個點往leaf node轉移討論

假設目前點 u 要轉移到點 vsiz[x]代表 x 之 subtree的大小

dp[x]代表題目所求,可以整理出以下式子:

dp[v] = (dp[u] - siz[v]) + (n - siz[v]);

前段代表 v subtree被影響的,因為在 u 轉移到 v 的同時,v subtree點的距離都被 -1

後段代表 其他點,因為往下了一層,其他非 v subtree 的點距離都被 +1

樹dp

扣:

DFS跑兩次,第一次找base case和紀錄 siz

第二次作轉移

#include <bits/stdc++.h>
#define endl '\n'
#define int long long
#define maxn 200005
using namespace std;
 
int n, siz[maxn], dp[maxn]; vector<int> e[maxn];
 
void dfs1(int x, int p, int d){
    siz[x] = 1;
    dp[1] += d;
    for (int v:e[x]){
        if (v == p) continue;
 
        dfs1(v, x, d+1);
        siz[x] += siz[v];
    }
}
 
void dfs2(int x, int p){
    for (int v:e[x]){
        if (v == p) continue;
 
        dp[v] = (dp[x] - siz[v]) + (n - siz[v]);
        dfs2(v, x);
    }
}
 
signed main(){
    ios::sync_with_stdio(0); cin.tie(0);
 
    cin >> n;
    for (int i=1; i<=n-1; ++i){
        int a, b; cin >> a >> b;
        e[a].push_back(b);
        e[b].push_back(a);
    }   
 
    dfs1(1, 0, 0);
    dfs2(1, 0);
 
    for (int i=1; i<=n; ++i){
        if (i==n) cout << dp[i] << endl;
        else cout << dp[i] << " ";
    }
}

樹dp

一些題目:(要答案來找我)

樹直徑

樹直徑

樹直徑:

樹中任兩個節點之間的最長距離

舉例來說,以下樹的直徑為5

樹直徑

找的方法其實非常簡單:

我們先找一個點,DFS找他最遠的點 u

在從點 u DFS找他最遠的點 v

u 和 v 的距離就會是樹直徑

證明等一下會和另一題解釋,先有個感覺就好了

樹直徑

題目:

APCS 201603 P4

模板題 相信你們看得懂前面的東西 :)

樹直徑

扣:

#include <bits/stdc++.h>
#define endl '\n'
#define int long long
#define maxn 100005
using namespace std;

int n, maxd, snode;
vector<int> e[maxn];
void dfs(int x, int p, int d){
    if (d > maxd){
        maxd = d;
        snode = x;
    }

    for(int i:e[x]){
        if (i == p) continue;
        dfs(i, x, d+1);
    }

    return;
}

signed main(){
    ios::sync_with_stdio(0); cin.tie(0);

    cin >> n;
    for (int i=1; i<=n; ++i){
        int u, v; cin >> u >> v;
        e[u].push_back(v);
        e[v].push_back(u);
    }

    dfs(0, 0, 0);
    dfs(snode, snode, 0);
    cout << maxd << endl;
}

樹直徑

接下來要講一些證明 :)

樹直徑

Claim:任何一點他距離最遠的點 必為取樹直徑的那兩個點

Proof:

假設我們取樹直徑的兩點爲 (s, t)

隨機取一點 u,要找他距離最遠的點 v

假設 v 不為 s 或 t,那就一定會有比 d(s, t) 更長的路徑

(在找樹直徑第一次dfs時就不會找到 s 或 t 了)

 

與原假設矛盾,得證

樹直徑

簡單來說,我們可以推導出以下性質:

求距離點 u 最遠的點 v,

d(u, v) = max\{d(u, s), d(u, t)\}

我們只要從 s 和 t 往外 dfs ,

紀錄他們到每個點的距離,最後再去取 max 就好了。

樹直徑

扣:

#include <bits/stdc++.h>
#define endl '\n'
#define maxn 200005
using namespace std;
 
int n, s, t, maxcnt; vector<int> v[maxn];
int dist1[maxn], dist2[maxn];
 
void dfs(int x, int p, int d, int r){
    if (d > maxcnt){
        maxcnt = d;
        if (!r) s = x;
        else t = x;
    }
 
    for (int i:v[x]){
        if (i==p) continue;
        dfs(i, x, d+1, r);
    }
}
 
void find_dist(int x, int p, int d, int r){
    if (!r) dist1[x] = d;
    else dist2[x] = d;
 
    for (int i:v[x]){
        if (i==p) continue;
        find_dist(i, x, d+1, r);
    }
}
 
int main(){
    ios::sync_with_stdio(0); cin.tie(0);
 
    cin >> n;
    for (int i=1; i<=n-1; ++i){
        int a, b; cin >> a >> b;
        v[a].push_back(b);
        v[b].push_back(a);
    }
 
    dfs(1, 0, 0, 0);
    maxcnt = 0;
    dfs(s, 0, 0, 1);
 
    find_dist(s, 0, 0, 0);
    find_dist(t, 0, 0, 1);
 
    for (int i=1; i<=n; ++i){
        if (i==n) cout << max(dist1[i], dist2[i]) << endl;
        else cout << max(dist1[i], dist2[i]) << " ";
    }
}

樹直徑

一些題目:(要答案來找我)

提示一:唯一和CSES那題不同的就是 若距離相同要找數字最大的點,想想看要怎麼找到一個work around

 

提示二:怎麼使兩個距離相同的分辨出來哪個點較大

 

提示三:float

 

解答 :)

樹壓平

Euler Tour Technique

樹壓平

當我們在樹上DFS紀錄in和exit順序的時候,

我們會發現一個很神奇的性質:

這也叫做 時間戳記

樹壓平

我們會發現,在一個點in和exit的時間區間內,他底下的都會是其subtree的點

(邏輯應該蠻簡單的 自己想想看)

樹壓平

  • 𝑑[𝑖]: 發現節點 𝑖 的時間 (discovery time)
  • 𝑓[𝑖] : 節點 𝑖 完成所有鄰居拜訪的時間 (finishing time)
void dfs(int x, int prev){
    d[x] = ++timer;

    for (int i:e[x]){
        if (i==prev) continue;

        dfs(i, x);
    }

    f[x] = ++timer;
}

實作扣也非常的簡單

樹壓平

重要的部分:

 

因為我們將樹「壓成」一條樹線,我們可以做區間修改和查詢

 

最常使用的是BIT或線段樹來解決區間問題

樹壓平

蠻明顯可以樹壓平,然後帶BIT來解決問題

注意:我們BIT的大小要開 2*n ,因為in和exit順序各占一個點

樹壓平

扣:

#include <bits/stdc++.h>
#define endl '\n'
#define maxn 200005
#define int long long
using namespace std;

int n, q, a[maxn], bit[2*maxn];
int timer, d[maxn], f[maxn];
int i1, i2, i3;
vector<int> e[maxn];

inline int lb(int x){
    return x&(-x);
}

void modify(int x, int v){
    while (x <= 2*n){
        bit[x] += v;
        x += lb(x);
    }
}

int query(int x){
    int cnt = 0;
    while (x>0){
        cnt += bit[x];
        x -= lb(x);
    }
    return cnt;
}

void dfs(int x, int prev){
    d[x] = ++timer;

    for (int i:e[x]){
        if (i==prev) continue;

        dfs(i, x);
    }

    f[x] = ++timer;
}

signed main(){
    ios::sync_with_stdio(0); cin.tie(0);

    cin >> n >> q;
    for (int i=1; i<=n; ++i) cin >> a[i];
    for (int i=1; i<=n-1; ++i){
        int u, v; cin >> u >> v;

        e[u].push_back(v);
        e[v].push_back(u);
    }

    dfs(1, 1);

    for (int i=1; i<=n; ++i){
        modify(d[i], a[i]);
    }

    for (int i=1; i<=q; ++i){
        cin >> i1;
        if (i1==1){
            cin >> i2 >> i3;

            modify(d[i2], i3-query(d[i2])+query(d[i2]-1));
        }else{
            cin >> i2;

            cout << query(f[i2]) - query(d[i2]-1) << endl;
        }
    }
}

樹壓平

想想看,要怎麼將這題改成區間問題

樹壓平

會發現每次query某點時,從那點的in或exit往上找

就都會在父節點的in exit區間內


代表我們每次modify時改整個區間,就可以改道它subtree每個點的值

(舉例來說,改點1區間內全部的值,全部在點1 subtree 的點都會被改到)

 

因為某點的值 會影響到他subtree點的答案,

所以修改只要對他in和exit的區間做修改就好了

 

區間修改,單點查詢 -> BIT 套差分

樹壓平

扣:

#include <bits/stdc++.h>
#define endl '\n'
#define maxn 200005
#define int long long
using namespace std;

int n, q, a[maxn], bit[2*maxn]; vector<int> v[maxn];
int d[maxn], f[maxn], timer;
int i1, i2, i3;

inline int lb(int x){
    return x&(-x);
}

int query(int x){
    int cnt=0;
    while (x>0){
        cnt += bit[x];
        x -= lb(x);
    }
    return cnt;
}

void modify(int x, int v){
    while (x<=2*n){
        bit[x] += v;
        x += lb(x);
    }
}

void dfs(int x, int p){
    d[x] = ++timer;
    for (int i:v[x]){
        if (i==p) continue;

        dfs(i, x);
    }
    f[x] = ++timer;
}

signed main(){
    ios::sync_with_stdio(0); cin.tie(0);

    cin >> n >> q;
    for (int i=1; i<=n; ++i) cin >> a[i];
    for (int i=1; i<=n-1; ++i){
        cin >> i1 >> i2;
        v[i1].push_back(i2);
        v[i2].push_back(i1);
    }

    dfs(1, 0);
    for (int i=1; i<=n; ++i){
        modify(d[i], a[i]);
        modify(f[i]+1, -a[i]);
    }

    for (int i=1; i<=q; ++i){
        cin >> i1;
        if (i1 == 1){
            cin >> i2 >> i3;

            modify(d[i2], i3-a[i2]);
            modify(f[i2]+1, -(i3-a[i2]));
            a[i2] = i3;
        }else{
            cin >> i2;
            cout << query(d[i2]) << endl;
        }
    }
}

樹壓平

一些題目:(要答案來找我)

倍增法

Binary Lifting

假設我們今天要從一個點往root走,找到他往上n個點的結果。

我們如果一個一個走會非常的慢 O(n)

 

倍增法

但我們今天可以用倍增法,將這個問題降到 O(log n)

我們都學過一件事,一個數字都可以用二進位表示出來

我們今天只要將往上的數字,轉換成二進位,

並每次都走 2^x 步,就可以達到 O(log n)了

倍增法

我們可以先預處理每個點往上 2^x 步會到哪

倍增法

void dfs(int x, int p){
    up[x][0] = p; //x的前一個node
    
    //類似dp的觀念,我們往上 2^n-1 兩次 就會是往上 2^n
    for (int i=1; i<=l; ++i){
        up[x][i] = up[up[x][i-1]][i-1]; //從x的前一個node慢慢轉移
    }

    for (int i:e[x]){
        if (x == p) continue;

        dfs(i, x);
    }
}

我們開一個陣列 ,           

up[i][j]代表第 i node它往上 2^j 個node
並且用dfs去預處理

 

這裡的               

up[n][\lceil \log_2 n \rceil]
l = \lceil \log_2 n \rceil

倍增法

模板題 試著自己寫寫看query的部分

倍增法

扣:

query 只要把k轉成二進位,然後往上找就好了
如果往上找的node=0,就沒有這個parent,就回傳 -1

#include <bits/stdc++.h>
#define endl '\n'
#define maxn 200005
#define lmaxn 20
using namespace std;

//ceil(log2(200005)) = 18
int n, q, l, up[maxn][lmaxn]; vector<int> e[maxn];

void dfs(int x, int p){
    up[x][0] = p;
    for (int i=1; i<=l; ++i){
        up[x][i] = up[up[x][i-1]][i-1];
    }

    for (int i:e[x]){
        if (x == p) continue;

        dfs(i, x);
    }
}

int query(int x, int k){
    bool f = true;
    for (int i=0; i<=l; ++i){
        if (k==0) break;

        if (k%2 == 1){
            if (up[x][i] == 0){
                f = false;
                break;
            }else{
                x = up[x][i];
            }
        }

        k /= 2;
    }
    if (!f) return -1;
    else return x;
}

int main(){
    ios::sync_with_stdio(0); cin.tie(0);

    cin >> n >> q;
    l = 18;
    for (int i=2; i<=n; ++i){
        int a; cin >> a;
        e[a].push_back(i);
    }

    dfs(1, 0);
    for (int i=1; i<=q; ++i){
        int a, b; cin >> a >> b;
        cout << query(a, b) << endl;
    }
}

LCA

lca 稱作 最低共同祖先

顧名思義,就是求兩點他們最低的共同祖先

 

會發先這題用倍增法蠻好解的

LCA

void dfs(int x, int p){
    up[x][0] = p;
    tin[x] = ++timer; //紀錄x in 的時間
    for (int i=1; i<=l; ++i){
        up[x][i] = up[up[x][i-1]][i-1];
    }

    for (int i:e[x]){
        if (i == p) continue;

        dfs(i, x);
    }

    tout[x] = ++timer; //紀錄x out 的時間
}

紀錄dfs的戳記,等一下會用到

bool is_parent(int u, int v){
    //check if u is v's parent
    return tin[u] <= tin[v] && tout[u] >= tout[v];
}

int lca(int u, int v){
    if (is_parent(u, v)) return u;
    if (is_parent(v, u)) return v;

    for (int i=l; i>=0; --i){
        //不斷的往上找,不要超過他們的LCA
        if (!is_parent(up[u][i], v)) u = up[u][i];
    }
    
    //因為找到的是LCA的前一個,所以要再往上一層
    return up[u][0];
}

我們只要選一點,一直往上走,

直到他確定是另一點的祖先

用時間戳記來判斷

LCA

模板題

LCA

扣:

#include <bits/stdc++.h>
#define endl '\n'
#define maxn 200005
#define lmaxn 20
using namespace std;

//ceil(log2(200005)) = 18
int n, q, l, up[maxn][lmaxn]; vector<int> e[maxn];
int tin[maxn], tout[maxn], timer;

void dfs(int x, int p){
    up[x][0] = p;
    tin[x] = ++timer;
    for (int i=1; i<=l; ++i){
        up[x][i] = up[up[x][i-1]][i-1];
    }

    for (int i:e[x]){
        if (i == p) continue;

        dfs(i, x);
    }

    tout[x] = ++timer;
}

bool is_parent(int u, int v){
	//check if u is v's parent
    return tin[u] <= tin[v] && tout[u] >= tout[v];
}

int lca(int u, int v){
    if (is_parent(u, v)) return u;
    if (is_parent(v, u)) return v;

    for (int i=l; i>=0; --i){
        if (!is_parent(up[u][i], v)) u = up[u][i];
    }

    return up[u][0];
}

int main(){
    ios::sync_with_stdio(0); cin.tie(0);

    cin >> n >> q;
    l = 18;
    for (int i=2; i<=n; ++i){
        int a; cin >> a;
        e[a].push_back(i);
    }

    dfs(1, 1);

    for (int i=1; i<=q; ++i){
        int v, u; cin >> v >> u;
        cout << lca(v, u) << endl;
    }
}

LCA

一些題目:(要答案來找我)

樹剖

Heavy Light Decomposition

樹重心剖分

Centroid Decomposition

ok 你學完進階圖論了

其實還有flow 但我不會

Made with Slides.com