樹鏈剖分,是很多樹上問題很好的解決方法. 比如說修改樹上路徑上點的權值.有些人說線段樹一定能解決,但是光用線段樹是不行的,這裏就可以用線段樹結合樹鏈剖分解決. 相信很多人還不知道樹鏈剖分是什麼算法,不廢話了,開始.
先來一道模板題,給定N個節點的一棵樹有K次查詢,每次查詢a和b的最近公共祖先(即LCA) (N<=1e5,K<=1e5)
首先暴力肯定會被卡掉O(N*K); 可以用倍增O(NlogN+KlogN),離線tarjan O(N+K) (常數較大且在強制在線時不行) LCA轉RMQ利用dfs序也可以解決 O(2NlogN+K) ,今天我介紹的樹鏈剖分雖然理論時間複雜度不夠優秀,但是實際運行時間最優. 倍增216ms,離線tarjan128ms,LCA轉RMQ260ms(在K遠遠大於N時此類問題它更優),而樹剖108ms
下面我來介紹樹剖的原理,它是把一棵樹的所有邊分成兩種鏈(輕鏈和重鏈),重鏈的定義是在根節點一定的情況之下,在此節點的兒子節點中,誰下方的子樹最大,就把那一條鏈定義成重鏈,此節點連接剩下子節點的邊都定義爲輕鏈. 這有什麼好處呢?請看圖.
此圖來自poj1330 剖分後就是如下情況,求LCA時遇到重鏈就一直到頂上,輕鏈就走一步. 圖畫的略醜,但意思表達出來了. 做題時,可用幾個數組將圖的意思表達出來. size[]表示所有點子樹的大小. fa[]表示此節點的父親是誰.top[]表示以此節點爲起點通過重鏈能到達最上面的點,dep[]表示此節點 深度,son[]表示此節點的兒子. 在這裏我要說一下,如果一個節點有多個兒子子樹個數相同時,那麼就隨便定義一個爲重鏈就好了 . 鏈式前向星連邊,兩次dfs,第一次維護size,fa,dep,son.第二次維護top,之後遵循之前的原則,遇到重鏈跳到頂端,遇到輕鏈跳一步.暴力向上跳就行了. 代碼如下:
#include<stdio.h>
int dep[100001];
int size[100001];
int son[100001];
int fa[100001];
int top[100001];
int head[100001];
int to[200001];
int next[200001],idx;
bool is[100001];
void addedge(int a,int b)
{
next[++idx]=head[a];
head[a]=idx;
to[idx]=b;
}
void dfs(int p)
{
size[p]=1;
for(int i=head[p];i;i=next[i])
{
if(to[i]!=fa[p])
{
fa[to[i]]=p;
dep[to[i]]=dep[p]+1;
dfs(to[i]);
size[p]+=size[to[i]];
if(size[to[i]]>size[son[p]])
son[p]=to[i];
}
}
}
void dfs2(int p,int t)
{
top[p]=t;
if(son[p])
dfs2(son[p],t);
for(int i=head[p];i;i=next[i])
{
if(to[i]!=fa[p]&&to[i]!=son[p])
dfs2(to[i],to[i]);
}
}
int lca(int x,int y)
{
while(top[x]!=top[y])
{
if(dep[top[x]]>dep[top[y]])
x=fa[top[x]];
else
y=fa[top[y]];
}
if(dep[x]<dep[y])
return x;
else
return y;
}
int main()
{
int Q,n,i,a,b,allfa,x,y;
scanf("%d%d",&n,&Q);
for(int i=1;i<n;i++)
{
scanf("%d%d",&a,&b);
fa[b]=a;
is[b]=true;
addedge(a,b);
addedge(b,a);
}
for(int i=1;i<=n;i++)
if(is[i]==0)
{
allfa=i;
break;
}
dfs(allfa);
dfs2(allfa,allfa);
for(int i=1;i<=Q;i++)
{
scanf("%d%d",&x,&y);
printf("%d\n",lca(x,y));
}
}
LCA的結果就是最後x和y較淺值.
樹鏈剖分還有許多應用,比如它的dfs序與線段樹的結合,再借用一下上張圖. 此序列爲: 8 4 10 16 3 12 11 2 15 7 1 14 13 5 9