前言
書上只講了重鏈剖分,菜雞也只會這一種,想看其他的是別想了。
要會樹鏈剖分,首先你需要了解一些概念。我們把乙個節點的所有兒子節點中子樹節點數最大的稱為重兒子,也就是size最大的子節點。size的定義我在講換根dp時說過,因此不再贅述。對於每個節點的重兒子,我們用 \(son[x]\) 來記錄它,父親節點到重兒子的那條邊稱為重邊,而連向其他兒子的邊則稱為輕邊。
可以試著想象一下,或者手畫一棵樹,就會發現,許多重邊連在一起,構成了一條鏈,這條邊的頂端節點,我們用 \(top[x]\) 來記錄,其中 \(x\) 是這條鏈上的任意乙個節點,也就是說,對於同一條鏈上的節點,它們的top的值都是一樣的。
還可以發現,每條鏈的頂點,隔著一條輕邊,就到了另一條鏈的節點上,也就是說,我們只要記錄每個節點的父節點,就可以讓它們轉移到另一條鏈上去。
我們可以想到,如果可以一次跳一條鏈,這樣求lca豈不比倍增要快得多?當兩個點跳到一條鏈上的時候,深度更小的那個自然就是lca了,而這需要的只是簡單的預處理罷了。
但是還有問題,我們不能一直跳,而且我們一次只能跳乙個點,選哪個好呢?
這時top陣列就有用了,兩個點的top值不相等,說明兩個點肯定還沒有跳到同一條鏈上,而我們只要在每次跳之前判斷一下x節點在的這條鏈的頂點和y這條鏈的頂點那個深度更大,每次跳深度更大的那個就是了。
樹鏈剖分的基本原理就是如此,通過將一條條重鏈剖分出來,以達到快速維護樹上資訊的目的。
你可能還有疑問,不就是求個lca,怎麼又可以維護樹上資訊了呢?
別急,先讓我們看一道模板題:樹的統計。
沒有什麼花裡胡哨的東西,講的很明白,就是要你維護樹上的資訊,然而資料規模之大,是普通的演算法所承受不了的。這時候,樹鏈剖分就會發揮它的奇效了。
首先看到這些操作,有沒有覺得很熟悉?沒錯,這正是線段樹裡面的慣常操作,只是現在不要求你維護序列,而要求你維護一棵樹,那我們是否可以把樹上的資訊轉換到一段序列上呢?
當然可以,我們只要以dfs序來構造乙個線段樹就可以了。dfs序,簡單點說,就是用dfs遍歷整棵樹時,節點出現的順序,我們只要在進入遞迴時和回溯時分別記錄一次就好了,放到樹鏈剖分裡就只要記錄進入遞迴時的順序即可。
接下來的就很簡單了,操作以模板題為例,我們可以仿造上面求lca的方法,讓兩個一次一次的跳,每次利用線段樹來查詢一條鏈上的資訊,最終迴圈結束,兩個點已經在同一條鏈上,那麼這時我們只要再查詢u點到v點的資訊,就可以了涵蓋兩個點間路徑上的所有節點。當然,記得先判斷一下u和v兩點的深度,小的下標更小,查詢時放前面。
說了這麼多,就來看看**吧。
先是預處理的
void dfs1(int x,int fa)
}void dfs2(int x,int fa)
for(int i=head[x];i;i=next[i])
}
要預處理的資訊有很多,在寫的時候一定要仔細,不要寫錯或寫漏,樹鏈剖分碼量一般都很大,寫錯乙個小細節可能就要花費你大量的除錯時間。
接下來是詢問的**
void ask(int x,int y)
為了方便,我的query函式沒有返回值,用全域性變數summ和maxn,放在query函式內,每次詢問時一起更新。
建樹部分
void build(int k,int l,int r)
int mid=(l+r)>>1;
build(k<<1,l,mid);
build(k<<1|1,mid+1,r);
sum[k]=sum[k<<1]+sum[k<<1|1];
max[k]=max(max[k<<1],max[k<<1|1]);
}
num記錄的是每個點的點權。
線段樹修改和查詢的**就不在此列出來了,有需要可以看我這道題的完整ac**。
**#include#include#include#includeusing namespace std;
const int n=3e4+10,m=1e5+24000;
int n,q,num[n];
int top[n],rev[m],seg[m],size[n],son[n];
int dep[n],father[n],sum[m],max[m];
int next[n<<1],ver[n<<1],tot,head[n<<1];
int summ,maxn;
void add(int x,int y)
inline void query(int k,int l,int r,int l,int r)
int mid=l+r>>1;
if(pos<=mid)change(k<<1,l,mid,v,pos);
if(mid>1;
build(k<<1,l,mid);
build(k<<1|1,mid+1,r);
sum[k]=sum[k<<1]+sum[k<<1|1];
max[k]=max(max[k<<1],max[k<<1|1]);
}void dfs1(int x,int fa)
}void dfs2(int x,int fa)
for(int i=head[x];i;i=next[i]) }
inline void ask(int x,int y)
inline int read()
while(ch>='0'&&ch<='9')
return x*f;
}int main()
{ n=read();
for(int i=1;i
總之,樹鏈剖分就是這麼簡單了。這裡還有一些樹上操作,以及一些注意事項,都是我做題時遇到的,一一列下。
最短路徑和路徑是一樣的,樹的邊是無向的,總共就n-1條,從x點到y點只有一條路徑,哪有什麼最短路與次短路之分……
修改一條鏈上的資訊和查詢一條鏈上的資訊方法是一樣的,換個函式名而已,應該不會有人不會吧……
略……關於樹的操作太多了,每道題都不一樣,記住一些慣常操作就行了,其他的靠你自己推也應該可以推出來。畢竟資料結構題還是考你**實現能力,思維能力要求不會太高。
我也不知道為什麼又要弄乙個大標題,其實也沒什麼好講的……
進入正題,樹鏈剖分中有些題它並不給你點權,而是給你邊權,其他的跟點權的題一樣,沒什麼區別。
那麼怎麼把邊權轉點權呢?我們用的辦法一般是讓邊權變成子節點的點權。沒錯,也就是說對於一條邊 \((u,v)\) ,假如u是v的父節點,我們就把這條邊的邊權當做v點的點權,反之亦然,也就是說,根節點無權。
這事其實很簡單,只要在dfs1函式中的迴圈裡加上這麼一條語句num[y]=w[i],一切就水到渠成了。
還有乙個要修改的地方就是當兩點跳到同一條鏈上時,迴圈外的語句變成這樣query(1,1,n,seg[x]+1,seg[y])
。
這其實也很容易想明白,因為x這個點是深度最小的那個點,而它的權是它的父節點連向它那條邊的權,顯然不在我們的路徑內。畢竟假設我們查詢時路徑上的點有t個,那麼自然也就只有t-1條邊要查詢/修改。
還有一點要注意的就是,如果題目要求你修改的是輸入的第k條邊,那麼你就要記錄下輸入的點,並在查詢時判斷一下哪個節點是子節點,因為我們第k條邊的邊權轉移到的是當中的兩點中的子節點上,不能修改錯誤。判斷兩點深度大小就可以實現了。
練手題:grass planting g
最後給出一道碼量極大的boss題:旅遊
樹鏈剖分學習筆記
寫 又犯了很sb的錯誤,線段樹寫錯了。好像每次都會把r l 1寫成l r 1,然後就只有20分。寫的比較醜,壓了壓之後190行。基本上是我打過的最長的乙個模板了 然後簡單介紹一下樹剖吧。樹鏈剖分,就是把樹剖分成鏈,然後用資料結構來維護這些鏈,使得詢問 修改的複雜度達到o logn o l ogn 不...
樹鏈剖分學習筆記
樹鏈剖分 mod estc oder modestcoder modest code r如果你是重兒子,你就在重路徑上。如果你是輕兒子,暴力沿著祖先向上爬最多log nlogn logn 次就可以遇到重路徑。或者到根 而樹上操作基本就是找祖先 也許有人喜歡我的碼風 include include d...
樹鏈剖分 學習筆記
前置知識 dfs 序,線段樹 我們可以回顧兩個問題 1.樹上從 s 到 t 的路徑,每個點權值加上 z 很簡單。遍歷整棵樹即可。2.求樹上 s 到 t 的權值和。lca 可做。可以利用 lca 的性質 dis s dis t 2 dis lca 做即可。時間複雜度 o n log n 但是把這兩個問...