進階圖論
來上數學了 :P
這堂課預設你已學過以下知識:
基礎圖論 (大概到最短距離 MST那裡)
基礎dp (能知道dp有一些知識就好了)
BIT (用來解決樹壓平的區間問題)
線段樹 (只有樹頗才會用到 不用太擔心)
如果忘記基礎圖論的話:
講師介紹:

成電CKCSC 38th - 教學 + 網管

講師介紹:
興趣?(除了扣丁之外)







講師介紹:
學術力:
競程、演算法、網頁設計、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)
是一種數學問題,是將布林值附屬到變數中
並且找到一組可以讓函數成立的問題
舉例來說,底下為一個SAT問題:
2-SAT
什麼是2-SAT?
2-SAT 是 SAT 問題的一種限制
簡單來說,就是每個括號內都只會有兩個變數
舉例來說,底下為一個2-SAT問題:
2-SAT
CNF (conjuncitve normal form)
SAT問題通常給的形式會是 CNF,
簡單來說就是會是一群括號and起來
並且括號內的變數都取or
舉例來說,底下為一個以CNF表示的SAT問題:
2-SAT
SAT 問題是 NP-compelete
但是 2-SAT 有 O(n+m) 解法 (阿當然我們就是要認識他)
2-SAT
因為每個括號只有兩個變數,
我們先將題目假設為正確的,我們可以列出以下條件:
因為 或 我們一定要使一邊正確
1. 先觀察到以下特質:
2-SAT
我們可以將以上條件畫成一個有向圖,如下:
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
一些題目:(要答案來找我)

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 要轉移到點 v,siz[x]代表 x 之 subtree的大小
dp[x]代表題目所求,可以整理出以下式子:
前段代表 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,
我們只要從 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]) << " ";
}
}樹直徑
一些題目:(要答案來找我)

樹壓平
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去預處理
這裡的
倍增法
模板題 試著自己寫寫看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 但我不會
進階圖論
By MLGnotCOOL
進階圖論
- 88