這是和重鏈剖分(dsu on tree,http://www.cnblogs.com/zzqsblog/p/6146916.html)一類的trick,不過它的適用范圍與dsu on tree不同,它適用於涉及到深度的查詢。
例1 k-th ancestor query
https://zhuanlan.zhihu.com/p/25984772
有一棵n個點的有根樹,現在要求用O(nlogn)的時間預處理,O(1)詢問某個點的第k個祖先。
首先我們來定義一下長鏈剖分是什么。它與重鏈剖分十分類似,但是原來重兒子是大小最大的兒子,現在重兒子(其實不是“重”兒子,大概是preffered child)是往下延伸到葉子節點那條鏈最長的兒子。
這個長鏈剖分的過程同重鏈剖分的過程類似,只要一遍dfs即可。
我們先證明一個性質:任意一個點的k級祖先所在鏈的鏈長一定大於等於k。這個證明十分簡單,因為它到祖先長度就為k了,如果它到祖先不是重鏈,鏈長肯定不會更短。
求出長鏈剖分之后我們對於每條重鏈的鏈頭,設這條重鏈長度為len,記錄一下這條重鏈每個深度的點,然后記錄鏈頭往上len個點是哪些(顯然不會影響復雜度,頂多*2)。
我們同樣預處理倍增跳的數組,但是我們還要記錄一下1~n每個數的high-bit(最高位的1在哪)。
我們現在考慮向上跳k,那么我們先跳k的最高位r,然后還要跳k-r這么高。考慮這個點往上r的祖先,那么它所在重鏈鏈長>=r,注意到k-r<r,那么我們只要在鏈頭向上向下跳就可以了。
(其實這個例題和下面兩個例題沒啥關系,只是都用到了長鏈剖分而已)
例2 秘術(http://cogs.pro/cogs/problem/problem.php?pid=2652)
題意:給一棵樹,每個點有兩個權值a和b,現在要求一條長度為m的簡單路徑,使得Σai/Σbi最小。
先吐槽一句:看到數據范圍只有3w的時候我是震驚的……不應該是20w嗎……
首先這是一個01分數規划的題目,按照套路,考慮二分答案,我們二分一個p,我們就要判Σai/Σbi<=p是否可行,那么就是要判∑(ai-pbi)<=0是否可行。那么我們設ci=ai-pbi,我們就要找所有長度為m的鏈中ci和最小的。
當然我們可以點分治,這是一個十分基礎的點分治,這里不談,但是這樣就是兩個log了(二分還有一個log),而且也不是很好寫。
我們考慮一個暴力dp的做法。記f[i][j]為i往下長度為j的鏈ci和最小為多少,那么我們可以暴力進行轉移,每次考慮一個兒子p,對於它往下的每個長度s,可以用f[p][m-1-s]+f[i][s]更新答案,然后用f[p][s]+i的點權來更新f[i][s+1]。
我們發現第一個兒子並不需要合並,如果進行合並的話太浪費了。考慮每個點i的第一個兒子g,f[i]開始就是f[g]右移一格,前面補一個0,然后全部加上i的點權。
那么我們可以發現這個第一個兒子肯定是長鏈剖分里的重兒子最優啊。
在繼續之前,首先我們先解決內存分配上的問題,這個f數組如果對每個點都開復雜度當然不對。
正確的分配姿勢大概是這樣:
void all(int x,int f=0) { pos[x]=++P; if(son[x]) all(son[x],x); for esb(x,e,b) if(b!=son[x]&&b!=f) all(b,x); }
我們對每個點x,先分配一個f[x][0],然后再對重兒子遞歸分配,然后再對輕兒子遞歸分配。
考慮遞歸dp之后重兒子遞歸上來之后重兒子的f就沒用了,然后我們可以讓這些內存為f[x]所用,而且重兒子是最深的,內存肯定正好夠用,而且正好重兒子的f就緊挨着f[x][0],那么就(自動)實現了“右移一格”這個操作!
“加上i的點權”這個操作,如果直接for一遍復雜度顯然是不對的,那么我們可以在每個點x上打一個tag,讓f[x]+tag[x]表示真實的f[x]。
接下來的事情就比較簡單了,對於所有輕兒子我們暴力更新f[x]即可。
我們來證明一波這個做法的復雜度,考慮我們每次都用的都是每個兒子的重鏈更新dp數組,那么如果一條向下的重鏈被用到了(算進了復雜度),那么就說明它不是它的父親的重鏈,它的祖先就再也不會用到它了,所以復雜度就是O(n)的了。(每個點最多被用到O(1)次)
下面這份代碼在cogs上跑得最快qwq
#include <iostream> #include <stdio.h> #include <math.h> #include <string.h> #include <time.h> #include <stdlib.h> #include <string> #include <bitset> #include <vector> #include <set> #include <map> #include <queue> #include <algorithm> #include <sstream> #include <stack> #include <iomanip> using namespace std; #define pb push_back #define mp make_pair typedef pair<int,int> pii; typedef long long ll; typedef double ld; typedef vector<int> vi; #define fi first #define se second #define FO(x) {freopen(#x".in","r",stdin);freopen(#x".out","w",stdout);} #define Edg int M=0,fst[SZ],vb[SZ],nxt[SZ];void ad_de(int a,int b){++M;nxt[M]=fst[a];fst[a]=M;vb[M]=b;}void adde(int a,int b){ad_de(a,b);ad_de(b,a);} #define Edgc int M=0,fst[SZ],vb[SZ],nxt[SZ],vc[SZ];void ad_de(int a,int b,int c){++M;nxt[M]=fst[a];fst[a]=M;vb[M]=b;vc[M]=c;}void adde(int a,int b,int c){ad_de(a,b,c);ad_de(b,a,c);} #define es(x,e) (int e=fst[x];e;e=nxt[e]) #define esb(x,e,b) (int e=fst[x],b=vb[e];e;e=nxt[e],b=vb[e]) #define VIZ {printf("digraph G{\n"); for(int i=1;i<=n;i++) for es(i,e) printf("%d->%d;\n",i,vb[e]); puts("}");} #define VIZ2 {printf("graph G{\n"); for(int i=1;i<=n;i++) for es(i,e) if(vb[e]>=i)printf("%d--%d;\n",i,vb[e]); puts("}");} #define SZ 555555 Edg int n,l[SZ],r[SZ],fa[SZ],son[SZ],dep[SZ],md[SZ]; int pos[SZ],P=0; ll rs[SZ]; ld a[SZ],b[SZ],g,rst[SZ]; void gs(int x,int f=0) { md[x]=dep[x]; for esb(x,e,b)if(b!=f) { dep[b]=dep[x]+1; gs(b,x); if(md[b]>md[son[x]]) son[x]=b; md[x]=max(md[x],md[b]); } } void all(int x,int f=0) { pos[x]=++P; if(son[x]) all(son[x],x); for esb(x,e,b) if(b!=son[x]&&b!=f) all(b,x); } ld minn=1e18,tag[SZ]; int m; void dfs(int x,int f=0) { if(!son[x]) { rst[pos[x]]=a[x]-b[x]*g;tag[x]=0; if(!m) minn=min(minn,rst[pos[x]]); return; } dfs(son[x],x); rst[pos[x]]=-tag[son[x]]; tag[x]=tag[son[x]]+a[x]-b[x]*g; for esb(x,e,b) { if(b==son[x]||b==f) continue; dfs(b,x); int sr=md[b]-dep[b]; for(int d=1;d-1<=sr&&d<=m;d++) { if(m-d<=md[x]-dep[x]) minn=min(minn,rst[pos[b]+d-1] +rst[pos[x]+m-d]+tag[x]+tag[b]); } for(int d=0;d<=sr;d++) rst[pos[x]+d+1]=min(rst[pos[x]+d+1], rst[pos[b]+d]+tag[b]+a[x]-::b[x]*g-tag[x]); } if(m<=md[x]-dep[x]) minn=min(minn,rst[pos[x]+m]+tag[x]); } char ch,B[1<<15],*S=B,*T=B; #define getc() (S==T&&(T=(S=B)+fread(B,1,1<<15,stdin),S==T)?0:*S++) #define isd(c) (c>='0'&&c<='9') int aa,bb;int F(){ while(ch=getc(),!isd(ch)&&ch!='-');ch=='-'?aa=bb=0:(aa=ch-'0',bb=1); while(ch=getc(),isd(ch))aa=aa*10+ch-'0';return bb?aa:-aa; } #define gi F() int main() { freopen("cdcq_b.in","r",stdin); freopen("cdcq_b.out","w",stdout); n=gi; m=gi-1; for(int i=1;i<=n;i++) a[i]=gi; for(int i=1;i<=n;i++) b[i]=gi; if(m==-2) { ld ans=1e18; for(int i=1;i<=n;i++) ans=min(ans,a[i]/b[i]); printf("%.2lf\n",ans); return 0; } for(int i=1;i<n;i++) adde(gi,gi); gs(1); all(1); ld l=0,r=5e10; while(r-l>5e-4) { ld p=(l+r)/2; g=p; minn=1e18; dfs(1); if(minn>0) l=p; else r=p; } if(r>4.5e10) puts("-1"); else printf("%.2lf\n",l); }
例3 看門人
這是很久很久以前wlp模擬賽里的題(我就是這么了解到長鏈剖分的)。
一棵n個點的有邊權的樹,對於每個點i輸入兩個數li和ri,那么你需要輸出經過點i的,不經過1到i的父親這條鏈的,長度在[li,ri]之間的,邊權和最長的簡單路徑。n<=1000000,用特殊技巧輸出。
好像這題和上一題沒啥區別,只不過m變成了[li,ri],那么每次我們就不能直接暴力更新,必須用一棵線段樹維護一發。而且這個題顯然不能點分治了。
#include <iostream> #include <stdio.h> #include <math.h> #include <string.h> #include <time.h> #include <stdlib.h> #include <string> #include <bitset> #include <vector> #include <set> #include <map> #include <queue> #include <algorithm> #include <sstream> #include <stack> #include <iomanip> using namespace std; #define pb push_back #define mp make_pair typedef pair<int,int> pii; typedef long long ll; typedef double ld; typedef vector<int> vi; #define fi first #define se second #define FO(x) {freopen(#x".in","r",stdin);freopen(#x".out","w",stdout);} #define Edg int M=0,fst[SZ],vb[SZ],nxt[SZ];void ad_de(int a,int b){++M;nxt[M]=fst[a];fst[a]=M;vb[M]=b;}void adde(int a,int b){ad_de(a,b);ad_de(b,a);} #define Edgc int M=0,fst[SZ],vb[SZ],nxt[SZ],vc[SZ];void ad_de(int a,int b,int c){++M;nxt[M]=fst[a];fst[a]=M;vb[M]=b;vc[M]=c;}void adde(int a,int b,int c){ad_de(a,b,c);ad_de(b,a,c);} #define es(x,e) (int e=fst[x];e;e=nxt[e]) #define esb(x,e,b) (int e=fst[x],b=vb[e];e;e=nxt[e],b=vb[e]) #define VIZ {printf("digraph G{\n"); for(int i=1;i<=n;i++) for es(i,e) printf("%d->%d;\n",i,vb[e]); puts("}");} #define VIZ2 {printf("graph G{\n"); for(int i=1;i<=n;i++) for es(i,e) if(vb[e]>=i)printf("%d--%d;\n",i,vb[e]); puts("}");} #define SZ 1234567 Edg int n,l[SZ],r[SZ],fa[SZ],fv[SZ],son[SZ],dep[SZ],md[SZ]; int pos[SZ],P=0; ll rs[SZ]; const int R=1048576; ll seg[SZ+SZ],*vv=seg+R; void upd(int x) { for(x+=R,x>>=1;x;x>>=1) seg[x]=max(seg[x+x],seg[x+x+1]); } ll query(int l,int r) { ll a=-1e18; for(l+=R-1,r+=R+1;l^r^1;l>>=1,r>>=1) { if(~l&1) a=max(a,seg[l^1]); if(r&1) a=max(a,seg[r^1]); } return a; } void gs(int x) { md[x]=dep[x]; for esb(x,e,b) { dep[b]=dep[x]+1; rs[b]=rs[x]+fv[b]; gs(b); if(md[b]>md[son[x]]) son[x]=b; md[x]=max(md[x],md[b]); } } void all(int x) { pos[x]=++P; vv[P]=rs[x]; upd(P); if(son[x]) all(son[x]); for esb(x,e,b) { if(b!=son[x]) all(b); } } ll ml[SZ]; void dfs(int x) { if(!son[x]) return; //gg //先考慮重鏈的作用,之后把重鏈與自己放在一起考慮 { dfs(son[x]); int L=l[x],R=min(r[x],md[x]-dep[x]); if(L<=R) //用重鏈更新答案 ml[x]=max(ml[x],query(pos[x]+L,pos[x]+R)-rs[x]); } //現在重鏈與自己已經一體了 //所占的位置是[pos[x],pos[x]+md[x]-dep[x]] for esb(x,e,b) { if(b==son[x]) continue; dfs(b); int sr=md[b]-dep[b]; for(int d=0;d<=sr;d++) { int L=max(l[x]-d-1,0),R=min(r[x]-d-1,md[x]-dep[x]); if(L>R) continue; ml[x]=max(ml[x],vv[pos[b]+d]+query(pos[x]+L,pos[x]+R)-rs[x]*2); } for(int d=0;d<=sr;d++) { int tg=pos[x]+d+1; vv[tg]=max(vv[tg],vv[pos[b]+d]); upd(tg); } } } int main() { FO(watchdog) scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d%d",l+i,r+i), ml[i]=-1; for(int i=2;i<=n;i++) scanf("%d%d",fa+i,fv+i), ad_de(fa[i],i); gs(1); all(1); dfs(1); ll ans=0,MOD=998244353; for(int i=1;i<=n;i++) ans=(ans*23333+ml[i]%MOD)%MOD; ans=(ans%MOD+MOD)%MOD; printf("%d\n",int(ans)); }
最后總結一波:樹上涉及到深度的維護題,用重鏈剖分就可以輕松解決。祝大家省選順利!