對LCA、樹上倍增、樹鏈剖分(重鏈剖分&長鏈剖分)和LCT(Link-Cut Tree)的學習
LCA
what is LCA & what can LCA do
,即最近公共祖先
在一棵樹上,兩個節點的深度最淺的公共祖先就是 (自己可以是自己的祖先)
很簡單,我們可以快速地進入正題了
下圖中的樹,和的為2,和的是1(1也算1的祖先)
除了是直接求兩點的的模板題,沒有題目會叫你打個模板
那麼LCA能幹嘛?
其實的用途很廣,感覺用最多的像是這樣子的題
-
給你一棵樹
(或者是一個圖求完MST,這很常見)和樹上的兩點 -
然後找出這兩點之間的路徑
這時我們可以藉助這兩點的LCA作為中轉點來輕鬆地解決這類題目
how to discover LCA
還是這棵樹
和的是1,這兩點間的路徑是下圖示紅的路徑
怎麼求LCA呢?
method one
暴力dfs一遍,時間複雜度
侮辱智商
(逃)
method two
模擬,從兩點開始一起向上跳一步,跳過的點標記一下,時間複雜度
侮辱智商*2
(逃)
method three
用dfs記錄每個點的深度,求時先調至統一深度,再一起向上跳
如果這棵樹是一個剛好分成兩叉
侮辱智商*3
上面的都是小學生都會的東西,不必講了
想真正用有意義的,接下來的內容才有點意思
求有一種非常容易理解的方法就是tarjan演算法預處理離線解決,時間複雜度,飛速
仍感覺離線演算法不適用於所有題目
(比如某些良心出題人喪病地要你強制線上,GG)
反正線上演算法可以解決離線能做出來所有的題
所以,我不講求了,推薦一篇 寫的很棒的blog,自行腦補一下
接下來的幾個線上演算法才是搞的門檻
ST(RMQ)演算法(線上)求LCA
這種方法的思想,就是將LCA問題轉化成RMQ問題
- 如果不會問題——演算法的就戳這裡吧
可以轉化成RMQ問題?
這裡有棵樹
如何預處理呢?
我們用dfs遍歷一次,得到一個序(兒子節點回到父親節點還要再算一遍)
序就是這樣的1->2->4->7->4->8->4->2->5->2->6->9->6->10->6->2->1->3->1
(一開始在向兒子節點走,到了葉子節點就向另一個兒子走,最後回到)
預處理的時間複雜度為
dfs序妙啊
設表示在序當中第一次出現的位置,表示的深度
如果求和的,r[x]~r[y]這一段區間內一定有,而且一定是區間中深度最小的那個點
(比如上面的dfs序中,第一個和第一個之間的序列裡的深度最小的點是,而正是和的!)
-
遍歷以為根的樹時,不遍歷完所有以為根的樹的節點是不會回到的
-
還有就是明顯地,想到達x再到y,必須上溯經過它們的LCA(兩個點之間有且只有一條路徑)
-
所以的深度一定最小
直接用RMQ——ST表維護這個東西,求出來的最小值的點即為
matters need attention
-
序的長度是,用處理出、和序之後直接套上裸的
(線段樹?差不多的……別在意) -
設表示dfs序中j~j+2^i-1的點當中,depth值最小的是哪個點 即可
-
那麼單次詢問時間複雜度
-
完美
code
宣告一下就是我也是看別人blog學的RMQ解決LCA,所以code我就直接copy過來了
code有問題,不能怪我
還有一點就是這人的code真提莫醜
#include <cstdio>
#include <cstring>
#include <cmath>
using namespace std;
int n,_n,m,s;//_n是用來放元素進dfs序裡,最終_n=2n-1
struct EDGE
{
int to;
EDGE* las;
}e[1000001];//前向星存邊
EDGE* last[500001];
int sx[1000001];//順序,為dfs序
int f[21][1000001];//用於ST演算法
int deep[500001];//深度
int r[500001];//第一次出現的位置
int min(int x,int y)
{
return deep[x]<deep[y]?x:y;
}
void dfs(int t,int fa,int de)
{
sx[++_n]=t;
r[t]=_n;
deep[t]=de;
EDGE*ei;
for (ei=last[t];ei;ei=ei->las)
if (ei->to!=fa)
{
dfs(ei->to,t,de+1);
sx[++_n]=t;
}
}
int query(int l,int r)
{
if (l>r)
{
//交換
l^=r;
r^=l;
l^=r;
}
int k=int(log2(r-l+1));
return min(f[k][l],f[k][r-(1<<k)+1]);
}
int main()
{
scanf("%d%d%d",&n,&m,&s);
int j=0,x,y;
for (int i=1;i<n;++i)
{
scanf("%d%d",&x,&y);
e[++j]={y,last[x]};
last[x]=e+j;
e[++j]={x,last[y]};
last[y]=e+j;
}
dfs(s,0,0);
//以下是ST演算法
for (int i=1;i<=_n;++i)f[0][i]=sx[i];
int ni=int(log2(_n)),nj,tmp;
for (int i=1;i<=ni;++i)
{
nj=_n+1-(1<<i);
tmp=1<<i-1;
for (j=1;j<=nj;++j)
f[i][j]=min(f[i-1][j],f[i-1][j+tmp]);
}
//以下是詢問,對於每次詢問,可以O(1)回答
while (m--)
{
scanf("%d%d",&x,&y);
printf("%d\n",query(r[x],r[y]));
}
}
小結
基礎的知識你應該已經會了吧
的運用挺廣吧,感覺和線段樹的考頻有的一比,不掌握是會吃虧的
上面的是比較普通的方法求,其實樹上倍增也能求
接下來到喪心病狂 (其實很簡單) 的樹上倍增了
樹上倍增
又是什麼東東?
倍增這個東東嚴格來說就是種思想,只可意會不可言傳
倍增,是根據已經得到了的資訊,將考慮的範圍擴大,從而加速操作的一種思想
使用了倍增思想的演算法有
- 歸併排序
- 快速冪
- 基於ST表的RMQ演算法
- 樹上倍增找LCA等
- FFT、字尾陣列等高階演算法
- ……有倍增的麼……我可能學了假的
some advantages
樹上倍增和是比較相似的,都採用了二進位制的思想,所以時空複雜度低
其實樹上倍增和樹鏈剖分在樹題裡面都用的很多
-
兩個都十分有趣,但倍增有著顯而易見的優點——
-
比起樹鏈剖分,樹上倍增程式碼短,查錯方便,時空複雜度優(都是)
-
只是功能欠缺了一些不要太在意
即使如此,樹上倍增也能夠解決大部分的樹型題目
反正兩個都要學
樹上倍增(線上)求LCA
preparation
怎麼樣預處理以達到線上單次詢問的時間呢?
我們需要構造倍增陣列
-
設表示i節點的第2^j個祖先
-
注意陣列開到,因為樹上任意一點最多有個祖先
-
明顯地發現這個東西完全可以代替不路徑壓縮的並查集,
(若則說明的第祖先不存在)
然後,倍增的性質 (DP方程) 就清楚地出來了
-
用文字來表達它就是這樣的
-
i的第個父親是i的第個父親的第個父親
神奇不?
就是說暴力求的第個祖先的時間複雜度是,現在變成了
同時,用一個dfs處理出每個點的深度和,時間複雜度
這樣預處理的總的時間複雜度即為
code
procedure dfs(x,y:longint);
var
now:longint;
begin
now:=last[x];
while now<>0 do
begin
if b[now]<>y then
begin
anc[b[now],0]:=x;
depth[b[now]]:=depth[x]+1;
dfs(b[now],x);
end;
now:=next[now];
end;
end;
for j:=1 to trunc(ln(n)/ln(2)) do
for i:=1 to n do
anc[i,j]:=anc[anc[i,j-1],j-1];
query
請務必認認真真地閱讀以下內容否則樹上倍增你就學不會了
樹上倍增的運用中,最簡單最實用的就是求了
- 現在我們需要求出,怎麼用樹上倍增做?
還記得前面三種 腦殘 求的第三個方法嗎?
用dfs記錄每個點的深度,求LCA時先調至統一深度,再一起向上跳
其實樹上倍增運用的就是這個思想!只不過時間複雜度降至了飛快的
-
對於兩個節點和,我們先把和調至同一深度
-
若此時