LCA問題(Least Common Ancestors,最近公共祖先問題),是指給定一棵有根樹T,給出若干個查詢LCA(u, v)(一般查詢數量較大),每次求樹T中兩個頂點u和v的最近公共祖先,即找一個節點,同時是u和v的祖先,而且深度儘量大(儘量遠離樹根)。
LCA問題有不少解法:線段樹、Tarjan算法、跳錶、RMQ與LCA互相轉化等。ios
LCA問題的通常形式:給定一棵有根樹,給出若干個查詢,每一個查詢要求指定節點u和v的最近公共祖先。算法
LCA問題有兩類解決思路:數組
一個LCA的例子以下。好比節點1和6的LCA爲0。ide
2、Tarjan算法測試
Tarjan算法是離線算法,基於後序DFS(深度優先搜索)和並查集。ui
算法從根節點root開始搜索,每次遞歸搜索全部的子樹,而後處理跟當前根節點相關的全部查詢。spa
算法用集合表示一類節點,這些節點跟集合外的點的LCA都同樣,並把這個LCA設爲這個集合的祖先。當搜索到節點x時,建立一個由x自己組成的集合,這個集合的祖先爲x本身。而後遞歸搜索x的全部兒子節點。當一個子節點搜索完畢時,把子節點的集合與x節點的集合合併,並把合併後的集合的祖先設爲x。由於這棵子樹內的查詢已經處理完,x的其餘子樹節點跟這棵子樹節點的LCA都是同樣的,都爲當前根節點x。全部子樹處理完畢以後,處理當前根節點x相關的查詢。遍歷x的全部查詢,若是查詢的另外一個節點v已經訪問過了,那麼x和v的LCA即爲v所在集合的祖先。3d
其中關於集合的操做都是使用並查集高效完成。code
算法的複雜度爲,O(n)搜索全部節點,搜索每一個節點時會遍歷這個節點相關的全部查詢。若是總的查詢個數爲m,則總的複雜度爲O(n+m)。blog
好比上面的例子中,前面處理的節點的順序爲4->7->5->1->0->…。
當訪問完4以後,集合{4}跟集合{1}合併,獲得{1,4},而且集合祖先爲1。而後訪問7。若是(7,4)是一個查詢,因爲4已訪問過,因而LCA(7,4)爲4所在集合{1,4}的祖先,即1。7訪問完以後,把{7}跟{5}合併,獲得{5,7},祖先爲5。而後訪問5。若是(5,7)是一個查詢,因爲7已訪問過,因而LCA(5,7)爲7所在集合{5,7}的祖先,即5。若是(5,4)也是一個查詢,因爲4已訪問過,則LCA(5,4)爲4所在集合{1,4}的祖先,即1。5訪問完畢以後,把{5,7}跟{1,4}合併,獲得{1,4,5,7},而且祖先爲1。而後訪問1。若是有(1,4)查詢,則LCA(1,4)爲4所在集合{1,4}的祖先,爲1。1訪問完以後,把{1,4,5,7}跟{0}合併,獲得{0,1,4,5,7},祖先爲0。而後剩下的2後面的節點處理相似。
接下來提供一個完整算法實現。
使用鄰接表方法存儲一棵有根樹。並經過記錄節點入度的方法找出有根樹的根,方便後續處理。
const int mx = 10000; //最大頂點數 int n, root; //實際頂點個數,樹根節點 int indeg[mx]; //頂點入度,用來判斷樹根 vector<int> tree[mx]; //樹的鄰接表(不必定是二叉樹) void inputTree() //輸入樹 { scanf("%d", &n); //樹的頂點數 for (int i = 0; i < n; i++) //初始化樹,頂點編號從0開始 tree[i].clear(), indeg[i] = 0; for (int i = 1; i < n; i++) //輸入n-1條樹邊 { int x, y; scanf("%d%d", &x, &y); //x->y有一條邊 tree[x].push_back(y); indeg[y]++;//加入鄰接表,y入度加一 } for (int i = 0; i < n; i++) //尋找樹根,入度爲0的頂點 if (indeg[i] == 0) { root = i; break; } }
使用vector數組query存儲全部的查詢。跟x相關的全部查詢(x,y)都會放在query[x]的數組中,方便查找。
vector<int> query[mx]; //全部查詢的內容 void inputQuires() //輸入查詢 { for (int i = 0; i < n; i++) //清空上次查詢 query[i].clear(); int m; scanf("%d", &m); //查詢個數 while (m--) { int u, v; scanf("%d%d", &u, &v); //查詢u和v的LCA query[u].push_back(v); query[v].push_back(u); } }
而後是並查集的相關數據和操做。
int father[mx], rnk[mx]; //節點的父親、秩 void makeSet() //初始化並查集 { for (int i = 0; i < n; i++) father[i] = i, rnk[i] = 0; } int findSet(int x) //查找 { if (x != father[x]) father[x] = findSet(father[x]); return father[x]; } void unionSet(int x, int y) //合併 { x = findSet(x), y = findSet(y); if (x == y) return; if (rnk[x] > rnk[y]) father[y] = x; else father[x] = y, rnk[y] += rnk[x] == rnk[y]; }
再就是Tarjan算法的核心代碼。
在調用Tarjan以前已經初始化並查集給每一個節點建立了一個集合,而且把集合的祖先賦值爲本身了,於是這裏不用給根節點x單首創建。
int ancestor[mx]; //已訪問節點集合的祖先 bool vs[mx]; //訪問標誌 void Tarjan(int x) //Tarjan算法求解LCA { for (int i = 0; i < tree[x].size(); i++) { Tarjan(tree[x][i]); //訪問子樹 unionSet(x, tree[x][i]); //將子樹節點與根節點x的集合合併 ancestor[findSet(x)] = x;//合併後的集合的祖先爲x } vs[x] = 1; //標記爲已訪問 for (int i = 0; i < query[x].size(); i++) //與根節點x有關的查詢 if (vs[query[x][i]]) //若是查詢的另外一個節點已訪問,則輸出結果 printf("%d和%d的最近公共祖先爲:%d\n", x, query[x][i], ancestor[findSet(query[x][i])]); }
下面是主程序,再加一個樣例輸入輸出做爲測試。
int main() { inputTree(); //輸入樹 inputQuires();//輸入查詢 makeSet(); for (int i = 0; i < n; i++) ancestor[i] = i; memset(vs, 0, sizeof(vs)); //初始化爲未訪問 Tarjan(root); /*前面例子相關的一個輸入輸出以下: 8 0 1 0 2 0 3 1 4 1 5 5 7 3 6 7 1 4 4 5 4 7 5 7 0 5 4 3 1 6 7和4的最近公共祖先爲:1 5和4的最近公共祖先爲:1 5和7的最近公共祖先爲:5 1和4的最近公共祖先爲:1 6和1的最近公共祖先爲:0 3和4的最近公共祖先爲:0 0和5的最近公共祖先爲:0 */ }
下面是完整模板:
1 /* 2 Problem: 3 OJ: 4 User: S.B.S. 5 Time: 6 Memory: 7 Length: 8 */ 9 #include<iostream> 10 #include<cstdio> 11 #include<cstring> 12 #include<cmath> 13 #include<algorithm> 14 #include<queue> 15 #include<cstdlib> 16 #include<iomanip> 17 #include<cassert> 18 #include<climits> 19 #include<functional> 20 #include<bitset> 21 #include<vector> 22 #include<list> 23 #define F(i,j,k) for(int i=j;i<=k;++i) 24 #define M(a,b) memset(a,b,sizeof(a)) 25 #define FF(i,j,k) for(int i=j;i>=k;i--) 26 #define maxn 10001 27 #define inf 0x3f3f3f3f 28 #define maxm 4001 29 #define mod 998244353 30 //#define LOCAL 31 using namespace std; 32 int read(){ 33 int x=0,f=1;char ch=getchar(); 34 while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} 35 while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} 36 return x*f; 37 } 38 int n,m; 39 int root; 40 struct EDGE 41 { 42 int from; 43 int to; 44 int value; 45 int next; 46 }e[maxn]; 47 int head[maxn],tot,in[maxn]; 48 inline void addedge(int u,int v) 49 { 50 tot++; 51 e[tot].from=u; 52 e[tot].to=v; 53 e[tot].next=head[u]; 54 head[u]=tot; 55 } 56 vector<int> qq[maxn]; 57 inline void input() 58 { 59 cin>>n>>m;M(head,-1); 60 F(i,1,n-1){int u,v;cin>>u>>v;addedge(u,v);in[v]++;} 61 F(i,0,n-1)if(in[i]==0){root=i;break;} 62 F(i,1,m){int u,v;cin>>u>>v;qq[u].push_back(v);qq[v].push_back(u);} 63 return; 64 } 65 int fa[maxn],rank[maxn]; 66 inline void init() 67 { 68 F(i,0,n-1) fa[i]=i,rank[i]=0; 69 } 70 inline int find(int u) 71 { 72 if(u!=fa[u]) fa[u]=find(fa[u]); 73 return fa[u]; 74 } 75 inline void Union(int x,int y) 76 { 77 x=find(x);y=find(y); 78 if(x==y) return; 79 if(rank[x]>rank[y]) fa[y]=x; 80 else fa[x]=y,rank[y]+=rank[x]==rank[y]; 81 } 82 int dfn[maxn]; 83 bool vis[maxn]; 84 inline void tarjan(int u) 85 { 86 for(int i=head[u];i!=-1;i=e[i].next) 87 { 88 cout<<e[i].to<<endl; 89 tarjan(e[i].to); 90 Union(u,e[i].to); 91 dfn[find(u)]=u; 92 } 93 vis[u]=true; 94 for(int i=0;i<qq[u].size();i++) 95 if(vis[qq[u][i]]) cout<<u<<" and "<<qq[u][i]<<" 's LCA is : "<<dfn[find(qq[u][i])]<<endl; 96 } 97 int main() 98 { 99 std::ios::sync_with_stdio(false);//cout<<setiosflags(ios::fixed)<<setprecision(1)<<y; 100 #ifdef LOCAL 101 freopen("data.in","r",stdin); 102 freopen("data.out","w",stdout); 103 #endif 104 input();init(); 105 F(i,0,n) dfn[i]=i; 106 M(vis,false); 107 cout<<endl<<root<<endl; 108 F(i,1,tot) cout<<e[i].from<<" "<<e[i].to<<" "<<e[i].next<<endl;cout<<endl; 109 tarjan(root); 110 return 0; 111 }
3、RMQ算法
每當「進入」或回溯到某個結點時,將這個結點的深度存入數組E最後一位。同時記錄結點i在數組中第一次出現的位置(事實上就是進入 結點i時記錄的位置),記作R[i]。若是結點E[i]的深度記作D[i],易見,這時求LCA(T,u,v),就等價於求E[RMQ(D,R[u],R [v])],