LCA算法總結

LCA問題(Least Common Ancestors,最近公共祖先問題),是指給定一棵有根樹T,給出若干個查詢LCA(u, v)(一般查詢數量較大),每次求樹T中兩個頂點u和v的最近公共祖先,即找一個節點,同時是u和v的祖先,而且深度儘量大(儘量遠離樹根)。
LCA問題有不少解法:線段樹、Tarjan算法、跳錶、RMQ與LCA互相轉化等。ios

一 LCA問題

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 }
LCA1

3、RMQ算法

每當「進入」或回溯到某個結點時,將這個結點的深度存入數組E最後一位。同時記錄結點i在數組中第一次出現的位置(事實上就是進入 結點i時記錄的位置),記作R[i]。若是結點E[i]的深度記作D[i],易見,這時求LCA(T,u,v),就等價於求E[RMQ(D,R[u],R [v])],

相關文章
相關標籤/搜索