LCA問題有多種求法,例如倍增,Tarjan。node
本篇博文講解如何使用Tarjan求LCA。c++
若是你還不知道什麼是LCA,不要緊,本文會詳細解釋。算法
在本文中,由於我懶爲方便理解,使用二叉樹進行示範。
數組
LCA是樹上最近公共祖先問題。函數
最近公共祖先就是樹上有兩個結點,找一個結點,是他們的公共祖先,而且離他們兩個結點最近。優化
例如這是一棵樹:ui
樹上 4,7 兩個結點的 LCA 就是 2 了。spa
1 雖然也是他們的公共祖先,但並非最近的。3d
再舉個例子,8,5 的祖先是 5。8,6 的祖先是 1。blog
在開頭已經說過了,LCA 問題有多種求法。本文要介紹的是相對簡單的 Tarjan 求 LCA。
注意:Tarjan 求 LCA 是一種離線的算法,也就是說它一遍求出全部須要求的點的 LCA,而不是須要求哪兩個點再去求。
Tarjan 求 LCA 須要用到並查集,如下是本人使用的並查集模板。
int fa[100000]; void reset(){ for (int i=1;i<=100000;i++){ fa[i]=i; } } int getfa(int x){ return fa[x]==x?x:getfa(fa[x]); } void marge(int x,int y){ fa[getfa(y)]=getfa(x); }
因爲 Tarjan 是在遍歷到目標點的時候得出答案並輸出,那麼若是你不輸出,就須要使用一些東西來記錄它(通常不用)。
關於記錄
除非你以後須要 LCA 的結果再作一些操做,不然不須要記錄,直接在 DFS 中輸出便可。
我使用的是 STL 中的 Map 和 Pair,由於 LCA 是求兩個點,Pair 正好能夠知足一對數據。而 Map 的哈希機制能夠實現 O(1) 查找。
遍歷每個結點並使用並查集記錄父子關係。
Tarjan 是一種 DFS 的思想。咱們須要從根結點去遍歷這棵樹。
當遍歷到某一個結點(稱之爲 x) 時,你有如下幾點須要作的。
1將當前結點標記爲已經訪問。
2遞歸遍歷全部它的子節點(稱之爲 y),並在遞歸執行完後用並查集合並 x 和 y。
3遍歷與當前節點有查詢關係的結點(稱之爲 z)(便是須要查詢 LCA 的另外一些結點),若是 z 已經訪問,那麼 x 與 z 的 LCA 就是 $getfa(z)$(這個是並查集中的查找函數),輸出或者記錄下來就能夠了。
這是僞代碼
void tarjan(int x){ //在本代碼段中,s[i]爲第i個子節點 , t[i]爲第i個和當前節點有查詢關係的結點。 vis[x]=1;//標記已經訪問,vis是記錄是否已訪問的數組 for (i=1;i<=子節點數;i++){//枚舉子節點 (遞歸併合併) tarjan(s[i]); marge(x,s[i]);//並查集合並 } for (i=1;i<=有查詢關係的結點數;i++){ if (vis[t[i]]){ cout<<x<<"和"<<t[i]<<"的LCA是"<<getfa(t[i])<<endl;//若是t[i]已經訪問了輸出(getfa是並查集查找函數) } } }
核心代碼就這麼一點?對,就這麼一點。
若是你還不理解,那麼能夠跳轉到最後一章看圖解演示。
爲了接下來的講解,下面咱們明確一下讀入方式,不一樣的讀入方式能夠本身變通一下。
第一行兩個數 n 和 q,表示結點數和查詢數。
接下來 n 行每行兩個數,表示左子結點和右子結點編號,如沒有則是 -1。
接下來 q 行每行兩個數,表示查詢的兩個結點編號。
例如上圖的樹,讀入爲
9 5 2 3 4 5 -1 6 -1 -1 7 8 -1 9 -1 -1 -1 -1 -1 -1 5 4 7 4 7 8 9 3 8 6
我在這裏用的方法是二維數組。
int t[100000][10],top[100000]; //t[i][j]表示編號爲i的結點,第j個和它有查詢關係的點的編號 //top[i]表示編號爲i的結點與它有查詢關係的點的數量
注意:須要雙向存儲關係。例如結點 2 和 3,不只要更新t[2],還要更新t[3]。
讀入代碼長這樣:
for (int i=1;i<=q;i++){ cin>>a[i]>>b[i]; t[a[i]][++top[a[i]]]=b[i]; t[b[i]][++top[b[i]]]=a[i]; }
固然若是你想要優化下空間那麼把這個數組變成vector也是沒問題的。
這就沒了...
#include<bits/stdc++.h> using namespace std; int n,k,q,v[100000]; map<pair<int,int>,int> ans;//存答案 int t[100000][10],top[100000];//存儲查詢關係 struct node{ int l,r; }; node s[100000]; /*並查集*/ int fa[100000]; void reset(){ for (int i=1;i<=n;i++){ fa[i]=i; } } int getfa(int x){ return fa[x]==x?x:getfa(fa[x]); } void marge(int x,int y){ fa[getfa(y)]=getfa(x); } /*------*/ void tarjan(int x){ v[x]=1;//標記已訪問 node p=s[x];//獲取當前結點結構體 if (p.l!=-1){ tarjan(p.l); marge(x,p.l); } if (p.r!=-1){ tarjan(p.r); marge(x,p.r); }//分別對l和r結點進行操做 for (int i=1;i<=top[x];i++){ if (v[t[x][i]]){ cout<<getfa(t[x][i])<<endl; }//輸出 } } int main(){ cin>>n>>q; for (int i=1;i<=n;i++){ cin>>s[i].l>>s[i].r; } for (int i=1;i<=q;i++){ int a,b; cin>>a>>b; t[a][++top[a]]=b;//存儲查詢關係 t[b][++top[b]]=a; } reset();//初始化並查集 tarjan(1);//tarjan 求 LCA }
#include<bits/stdc++.h> using namespace std; int n,k,q,v[100000]; map<pair<int,int>,int> ans;//存答案 int t[100000][10],top[100000];//存儲查詢關係 int a[100000],b[100000]; struct node{ int l,r; }; node s[100000]; /*並查集*/ int fa[100000]; void reset(){ for (int i=1;i<=n;i++){ fa[i]=i; } } int getfa(int x){ return fa[x]==x?x:getfa(fa[x]); } void marge(int x,int y){ fa[getfa(y)]=getfa(x); } /*------*/ void tarjan(int x){ v[x]=1; node p=s[x]; if (p.l!=-1){ tarjan(p.l); marge(x,p.l); } if (p.r!=-1){ tarjan(p.r); marge(x,p.r); } for (int i=1;i<=top[x];i++){ if (v[t[x][i]]){ pair<int,int> tmp,tmp1;//用pair配合map來存儲答案 tmp=make_pair(x,t[x][i]); tmp1=make_pair(t[x][i],x);//兩個pair的目的是例如3 2這種數據若是搜到3纔有答案那麼進時的順序不止是(3,2),還有(2,3),方便輸出結果時查詢 ans[tmp]=getfa(t[x][i]); ans[tmp1]=getfa(t[x][i]); cout<<"#"<<ans[tmp]<<endl; } } } int main(){ cin>>n>>q; for (int i=1;i<=n;i++){ cin>>s[i].l>>s[i].r; } for (int i=1;i<=q;i++){ cin>>a[i]>>b[i]; t[a[i]][++top[a[i]]]=b[i]; t[b[i]][++top[b[i]]]=a[i]; } reset(); tarjan(1); for (int i=1;i<=q;i++){ pair<int,int> tmp; tmp=make_pair(b[i],a[i]); cout<<a[i]<<"-"<<b[i]<<":"<<ans[tmp]<<endl; } }