--- title: 輕重鍊剖分 tags: 7th 教學 slideOptions: theme: black transition: 'slide' --- # 輕重鍊剖分 #### Author:H1de_on_bruH ## 路徑問題: 假設有個節點為$N$個的樹 每個節點上都有一個點權 有$Q$次詢問 每次詢問$u$跟$v$路徑上所有點的權和 且$N,Q\le 10^5$,我們該怎麼在一秒內執行完? ### LCA 我們可以用倍增法先$O(N\log N)$預處理 每次詢問花$O(\log N)$查詢 這樣總體的複雜度是$O((N+Q)\log N)$ 在這樣的資料範圍下可以很漂亮的解決這個問題 ## 動態路徑問題 跟剛剛的情境完全一樣 只是我們在詢問的時候多加了一個動作 `修改節點權值` 那麼倍增法就毫無用武之地了 每次修改都要花$O(N\log N)$ **倍增法:完敗\\|/** ## 輕重鍊剖分 aka 樹鍊剖分 想像一下 我們把這棵樹分割成一條一條的鍊 如果鍊條少一點 我們就可以在鍊上套BIT、線段數等資料結構 讓他在複雜度不會爛掉的情況下維護節點權值 ![](https://i.imgur.com/KAuFUNk.png) 那到底怎麼切 才會是最好的呢? 我們先定義一些名詞 `重兒子:表示在所有子節點中 擁有的子節點樹最多的那一個` `重邊:由該節點連到他的重兒子的那個邊就叫重邊` `輕兒子:除了重兒子以外的其他節點` `輕邊:連到輕兒子的邊` 我們一開始切這棵樹時 從根開始往下走 直接走向重兒子 這樣子走到底後就會有一條鍊 那其他剩下來的子樹再做一樣的事 這樣子就可以把這棵樹分成一條一條的鍊 至於為甚麼這樣會是好的 因為每次往輕邊走後子樹的大小必定$\le$原來的$\frac{1}{2}$ 從任兩點的LCA分兩邊往下走只需要走過$O(\log N)$條 所以查詢路徑和的複雜度為$O(\log^2 N)$ ### 樹鍊剖分的實現 ```cpp= vector<int>path[N]; int son[N],num[N],p[N],dep[N],top[N],dfn[N],cnt; void dfs1(int now) { son[now]=-1; num[now]=1; for(int i:path[now]) { if(!dep[i]) { dep[i]=dep[now]+1; p[i]=now; dfs1(i); num[now]+=num[i]; if(son[now]==-1||num[i]>num[son[now]])son[now]=i; } } } ``` 我們先尋訪一次這棵樹 把上面各個節點的子樹大小、重兒子、深度等算出來 然後第二次尋訪時構造出鍊 並把鍊上的點編號打好 ```cpp= int cnt=0; void dfs2(int now,int t) { top[now]=t;//標示鍊的頂端的節點 cnt++; dfn[now]=cnt;//表示該節點在鍊上的編號 if(son[now]==-1)return; dfs2(son[now],now); for(int i:path[now]) if(i!=p[now]&&i!=son[now]) dfs2(i,i); } ``` 這樣我們就成功的用c++切開這棵樹了~~ ## 例題: ### [LibreOJ #10138](https://loj.ac/p/10138) 經典的樹剖題 直接開一棵線段樹就可以應付了 查詢的核心就是每次要找深度最深的那個點 讓他往最頂跳 跳到最頂時把路徑上的和加起來 那直到兩個點都在同一條鍊時 就把兩個點之間的值加起來 就會是答案了 最大值的做法也相同 :::spoiler sample code ```cpp= #include<bits/stdc++.h> #define int long long #define endl "\n" #define FOR(i,x,y) for(int i=x;i<y;i++) #define INF 1e16 #define Yuubari_chan_kawaii ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); using namespace std; const int N=2e5+5; int n,dfn[N],son[N],top[N],num[N],dep[N],p[N]; vector<int>path[N]; struct node { int mx,sum; }seg[N<<2]; void update(int x,int l,int r,int qx,int val) { if(l==r) { seg[x].mx=seg[x].sum=val; return; } int mid=(l+r)>>1; if(qx<=mid)update(x<<1,l,mid,qx,val); else update(x<<1|1,mid+1,r,qx,val); seg[x].mx=max(seg[x<<1].mx,seg[x<<1|1].mx); seg[x].sum=seg[x<<1].sum+seg[x<<1|1].sum; } int big(int x,int l,int r,int ql,int qr) { if(ql<=l&&r<=qr)return seg[x].mx; int mid=(l+r)>>1; int res=-INF; if(ql<=mid)res=max(res,big(x<<1,l,mid,ql,qr)); if(mid<qr)res=max(res,big(x<<1|1,mid+1,r,ql,qr)); return res; } int ask(int x,int l,int r,int ql,int qr) { if(ql<=l&&r<=qr)return seg[x].sum; int mid=(l+r)>>1; int res=0; if(ql<=mid)res+=ask(x<<1,l,mid,ql,qr); if(mid<qr)res+=ask(x<<1|1,mid+1,r,ql,qr); return res; } void dfs1(int now) { son[now]=-1; num[now]=1; for(auto i:path[now]) { if(!dep[i]) { dep[i]=dep[now]+1; p[i]=now; dfs1(i); num[now]+=num[i]; if(son[now]==-1||num[i]>num[son[now]])son[now]=i; } } } int cnt; void dfs2(int now,int t) { top[now]=t; cnt++; dfn[now]=cnt; if(son[now]==-1)return; dfs2(son[now],t); for(auto i:path[now]) if(i!=p[now]&&i!=son[now]) dfs2(i,i); } int path_big(int x,int y) { int res=-INF; while(top[x]!=top[y]) { if(dep[top[x]]<dep[top[y]])swap(x,y); res=max(res,big(1,1,n,dfn[top[x]],dfn[x])); x=p[top[x]]; } if(dfn[x]>dfn[y])swap(x,y); res=max(res,big(1,1,n,dfn[x],dfn[y])); return res; } int path_sum(int x,int y) { int res=0; while(top[x]!=top[y]) { if(dep[top[x]]<dep[top[y]])swap(x,y); res+=ask(1,1,n,dfn[top[x]],dfn[x]); x=p[top[x]]; } if(dfn[x]>dfn[y])swap(x,y); res+=ask(1,1,n,dfn[x],dfn[y]); return res; } void solve() { cin>>n; FOR(i,0,n-1) { int a,b;cin>>a>>b; path[a].push_back(b); path[b].push_back(a); } dep[1]=1; dfs1(1); dfs2(1,1); FOR(i,1,n+1) { int now;cin>>now; update(1,1,n,dfn[i],now); } int q;cin>>q; while(q--) { string now;cin>>now; if(now=="CHANGE") { int x,y;cin>>x>>y; update(1,1,n,dfn[x],y); } else if(now=="QMAX") { int x,y;cin>>x>>y; cout<<path_big(x,y)<<endl; } else { int x,y;cin>>x>>y; cout<<path_sum(x,y)<<endl; } } } signed main() { Yuubari_chan_kawaii solve(); } ``` ::: ### [LibreOJ #2125](https://loj.ac/p/2125) 也是裸題 這邊我們看到他需要維護一個子樹的加值 我們只需要對每個節點維護一個值 表示所有其子節點$dfn$的最大值 表為$mx$ 那麼對一個點$i$而言$dfn_i$到$mx_i$這個連續的區間裡所有點都是$i$的子節點 我們一樣可以用線段樹來做維護 利用懶人標記的技巧可以讓時間複雜度達到$O(N\log N+Q\log N\log N)$ ## 感謝各位 祝大家都能夠在比賽隨手刻出來 ![](https://i.imgur.com/0Afzred.jpg)