最近公共祖先(LCA)

【概念】

       給定一棵樹,若節點 z 既是結點 x 的祖先,也是節點 y 的祖先,並且在 x 和 y 的祖先中深度最大,稱爲 x , y 的 最近公共祖先(Lowest Common Ancestors)。

【算法】

1.向上標記法

       從 x 向上走到根節點,並標記所有經過的點。

       從 y 向上走到根節點,第一次遇到的已標記的節點即爲 x 與 y 的 最近公共祖先 。

       對於每個詢問的時間複雜度爲 O(n)。

2.樹上倍增法

       設 f[i][j] 代表從編號爲 i 的節點向上走 2^j 步到達的節點編號爲 f[i][j] 。若該節點不存在,則令 f[i][j]=0 (令 f[i][j]=-1 也可以,但是需要注意以 f[i][j] 作下標可能越界)。

       若  x 與 y 不在同一深度,則利用二進制拆分思想 將 x 與 y 調整至同一深度 

       若 此時 x = y ,則已經找到 LCA, LCA(x,y)= x  。

       否則 用二進制拆分思想,將 x 與 y 同時向上調整,並保持 x!= y 。

-----------------------------------------------------------------------------------------------------------------------------------------------------------------

       設 x , y 向上移動D步所到達的節點爲他們最近公的共祖先。

       移動 x ,y 我們可以發現:

       當已經枚舉到第 k 位,通過一系列累加和(0/1*2^max_k+0/1*2^(max_k-1)+.....0/1*2^(k+1) )所得到的和爲D'。

       若 D'+2^k < D 則 x, y 向上走 D'+2^k 步到達的節點不是他們的公共祖先,即 f[x][k] != f[y][k] (x 與 y 在不斷往上更新)。

       若D'+2^k >= D 則 x, y 向上走D'+2^k 步到達的節點時他們的公共祖先,即 f[x][k] = f[y][k] 

       所以,當 f[x][k] != f[y][k] 時 x=f[x][k],y=f[y][k] (相當於D'+2^k) 我們保證了最後求的的 D' 比 D 小 1。

        時間複雜度爲 O((n+m)log n)

<代碼實現> 

void bfs(int x)
{
	queue<int> q;
	q.push(x);
	for(;!q.empty();)
	{
		int X=q.front();
		q.pop();
		for(int i=lin[X];i;i=e[i].next)
		{
			if(e[i].id==fa[X][0]) continue;//避免重複查詢
			q.push(e[i].id);
			d[e[i].id]=d[X]+1;//該節點深度爲父親節點的深度 +1
		}
	}
}//求每個節點的深度( d 記錄深度)
void lca(int x,int y)
{
	for(int i=1;i<=n;i++)
	 if(!fa[i][0])
	 {
	 	d[i]=0;
	    bfs(i);
	    for(int i=1;(1<<i)<n;i++)
	    {
	    	for(int j=1;j<=n;j++)
	    	{
	    		if(fa[j][i-1]<0) fa[j][i]=-1;//如果 f[j][i] 不成立(即 j 向上 2^i 沒有節點)
	    		else fa[j][i]=fa[fa[j][i-1]][i-1];// 否則 j 向上走 2^i 即爲向上走 2^(i-1)*2
			}
		}
	    break;
	 }//    
	if(d[x]>d[y]) x^=y^=x^=y;// y 的深度比 x 的深度大(方便操作)
	for(int D=d[y]-d[x],i=0;D;D>>=1,i++)
	 if(D&1)
	  y=fa[y][i];//二進制拆分往上挪
	if(x==y) 
	{
		cout<<x<<endl;
		return ;
	}
	int N=n;
	int k=0;
	for(;N;)
	{
		k++;
		N/=2;
	}// k 即爲最大向上走 2^k 步
	for(int i=k;i>=0;i--)
	 if(fa[x][i]!=fa[y][i])
	 {
	 	x=fa[x][i];
	 	y=fa[y][i];
	 }
	cout<<fa[x][0]<<endl;
	return ;
}

3.Tarjan算法(離線)

       Tarjan算法基本上使用並查集對“向上標記法”的優化。將 m 個詢問一次性讀入,統一計算。

       時間複雜度爲:O(n+m)

<代碼實現>

void Tarjan(int x)
{
	vis[x]=1;
	for(int i=lin[x];i;i=e[i].next)
	{
		if(vis[e[i].id]) continue;
		Tarjan(e[i].id);
		fa[e[i].id]=x;
	}//遍歷
	for(int i=l[x];i;i=q[i].next)
	{
		if(!vis[q[i].id]) continue;
		int X=getfa(q[i].id);
		ans[q[i].qu]=X;//q[i].qu 代表此時查詢的是第 qu[i].qu 個問題
	}//查詢
}
// 初始值:fa[i]=i

【例題】

1> Poj1330 Nearest Common Ancestors

      題目大意:給一個n個點的無根樹,詢問某兩個點的最近公共祖先。

       題解:LCA模板。

AC次數:2 。錯誤原因:數組開太小 (建無向邊時數組大小*2很重要)。

<倍增>

#include<bits/stdc++.h> 
using namespace std;
const int maxx=10000;
int n;
int lin[maxx+10]={};
int d[maxx+10]={};
int fa[maxx+10][22]={};
struct kk
{
	int id;
	int next;
} e[maxx*2+10]={};
int num=0;
int s,t;
void insert(int x,int y)
{
	e[++num].next=lin[x];
	lin[x]=num;
	e[num].id=y;
}
void bfs(int x)
{
	queue<int> q;
	q.push(x);
	for(;!q.empty();)
	{
		int X=q.front();
		q.pop();
		for(int i=lin[X];i;i=e[i].next)
		{
			if(e[i].id==fa[X][0]) continue;
			q.push(e[i].id);
			d[e[i].id]=d[X]+1;
		}
	}
}
void lca(int u,int v)
{
	for(int i=1;i<=n;i++)
	 if(!fa[i][0])
	 {
	 	d[i]=0;
	    bfs(i);
	    for(int i=1;(1<<i)<n;i++)
	    {
	    	for(int j=1;j<=n;j++)
	    	{
	    		if(fa[j][i-1]<0) fa[j][i]=-1;
	    		else fa[j][i]=fa[fa[j][i-1]][i-1];
			}
		}
	    break;
	 }
	if(d[u]>d[v]) u^=v^=u^=v;
	for(int D=d[v]-d[u],i=0;D;D>>=1,i++)
	 if(D&1)
	  v=fa[v][i];
	if(u==v) 
	{
		cout<<u<<endl;
		return ;
	}
	int N=n;
	int k=0;
	for(;N;)
	{
		k++;
		N/=2;
	}
	for(int i=k;i>=0;i--)
	 if(fa[u][i]!=fa[v][i])
	 {
	 	u=fa[u][i];
	 	v=fa[v][i];
	 }
	cout<<fa[u][0]<<endl;
	return ;
}
int main()
{
	int T;
	cin>>T;
	for(int I=1;I<=T;I++)
	{
		memset(fa,0,sizeof(fa));
		memset(lin,0,sizeof(lin));
	    num=0;
	    cin>>n;
	    for(int i=1;i<=n-1;i++)
	    {
	    	int x,y;
	    	cin>>x>>y;
	    	insert(x,y);
	    	insert(y,x);
	    	fa[y][0]=x;
		}
		cin>>s>>t;
		lca(s,t);
	}
	return 0;
}

2>HDU2586 How far away ?

       題目大意:給一個n個點的無根樹,有q個詢問,每個詢問兩個點,問兩點的距離。

       題解:樹上點距模板。求 x,y 的距離爲 dist[x]+dist[y]-2*dist[lca(x,y)] 。

       AC次數:1 。

<tarjan>

#include<bits/stdc++.h>
using namespace std;
const int max_p=40000,max_q=200;
int n,m;
int lin[max_p+10]={};
struct kk
{
	int id;
	int next;
	int v;
} e[max_p*2+10]={};
int l[max_p+10]={};
struct kkk
{
	int id;
	int next;
	int q;
} qu[max_q*2+10]={};
int num=0;
int fa[max_p+10]={};
int vis[max_p+10]={};
int ans[max_p+10]={};
int dis[max_p+10]={};
void insert(int x,int y,int v)
{
	e[++num].next=lin[x];
	lin[x]=num;
	e[num].id=y;
	e[num].v=v;
}
void insert2(int x,int y,int q)
{
	qu[++num].next=l[x];
	l[x]=num;
	qu[num].id=y;
	qu[num].q=q;
}
int getfa(int x){ return x==fa[x] ? x : fa[x]=getfa(fa[x]); }
void Tarjan(int x)
{
	vis[x]=1;
	for(int i=lin[x];i;i=e[i].next)
	{
		if(vis[e[i].id]) continue;
		dis[e[i].id]=dis[x]+e[i].v;
		Tarjan(e[i].id);
		fa[e[i].id]=x;
	}
	for(int i=l[x];i;i=qu[i].next)
	{
		if(vis[qu[i].id])
		{
			int X=getfa(qu[i].id);
			ans[qu[i].q]=dis[x]+dis[qu[i].id]-2*dis[X];
		}
	}
	return ;
}
int main()
{
	int T;
	cin>>T;
	for(int I=1;I<=T;I++)
	{
		cin>>n>>m;
		for(int i=1;i<=n;i++)
		 fa[i]=i,dis[i]=vis[i]=ans[i]=lin[i]=l[i]=0;
		num=0;
		for(int i=1;i<=n-1;i++)
		{
			int x,y,v;
			cin>>x>>y>>v;
			insert(x,y,v);
			insert(y,x,v);
		}
		num=0;
		for(int i=1;i<=m;i++)
		{
			int x,y;
			cin>>x>>y;
			insert2(x,y,i);
			insert2(y,x,i);
		}
		Tarjan(1);
		for(int i=1;i<=m;i++)
		 cout<<ans[i]<<endl;
	}
	return 0;
}

3.BZoj1787 緊急集合

       題目大意:給定一棵大小爲n的樹,有m組詢問,每組詢問給三個點x, y, z,求到這三個點距離和最小的點及最小距離和。

       題解:對於每兩個點進行求一次LCA。有兩種情況:如果每組的LCA相同,即輸出;否則輸出只有出現一次的LCA。

       AC次數:1 。

#include<bits/stdc++.h>
using namespace std;
const int maxx=500000;
int n,m;
int lin[maxx+10]={};
struct kk
{
	int id;
	int next;
	int v;
} e[maxx*2+10]={};
int l[maxx+10]={};
struct kkk
{
	int id;
	int next;
	int q;
	int t;
} qu[maxx*6+10]={};
int num=0;
int fa[maxx+10]={};
int vis[maxx+10]={};
int Vis[maxx+10][4]={};
int ans[maxx+10][4][2]={};
int dis[maxx+10]={};
void insert(int x,int y,int v)
{
	e[++num].next=lin[x];
	lin[x]=num;
	e[num].id=y;
	e[num].v=v;
}
void insert2(int x,int y,int q,int t)
{
	qu[++num].next=l[x];
	l[x]=num;
	qu[num].id=y;
	qu[num].q=q;
	qu[num].t=t;
}
int getfa(int x){ return x==fa[x] ? x : fa[x]=getfa(fa[x]); }
void Tarjan(int x)
{
	vis[x]=1;
	for(int i=lin[x];i;i=e[i].next)
	{
		if(vis[e[i].id]) continue;
		dis[e[i].id]=dis[x]+e[i].v;
		Tarjan(e[i].id);
		fa[e[i].id]=x;
	}
	for(int i=l[x];i;i=qu[i].next)
	{
		if(vis[qu[i].id]&&!Vis[qu[i].q][qu[i].t])
		{
			int X=getfa(qu[i].id);
			Vis[qu[i].q][qu[i].t]=1;
			ans[qu[i].q][++ans[qu[i].q][0][0]][0]=X;
			ans[qu[i].q][ans[qu[i].q][0][0]][1]=dis[x]+dis[qu[i].id]-2*dis[X];
		}
	}
	return ;
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++) fa[i]=i; 
	for(int i=1;i<=n-1;i++)
	{
		int x,y;
		cin>>x>>y;
		insert(x,y,1); insert(y,x,1);
	}
	num=0;
	for(int i=1;i<=m;i++)
	{
		int a,b,c;
		cin>>a>>b>>c;
		insert2(a,b,i,1); insert2(b,a,i,1);
		insert2(b,c,i,2); insert2(c,b,i,2);
		insert2(a,c,i,3); insert2(c,a,i,3);
	}
	Tarjan(1);
	for(int i=1;i<=m;i++)
	{
		int S=(ans[i][1][1]+ans[i][2][1]+ans[i][3][1])/2;
		if(ans[i][1][0]==ans[i][2][0])
		{
			if(ans[i][1][0]==ans[i][3][0])
	    		cout<<ans[i][2][0]<<' '<<S<<endl;
			else
			    cout<<ans[i][3][0]<<' '<<S<<endl;	
		}
		else
		{
			if(ans[i][1][0]==ans[i][3][0])
			    cout<<ans[i][2][0]<<' '<<S<<endl;
			else
			    cout<<ans[i][1][0]<<' '<<S<<endl;
		}
	}
	return 0;
}

4.UVA1354 Bond

       題目大意:n個城市,m條邊相連,每次詢問兩點之間最大值最小是多少。

       題解:最小生成樹+倍增。

      AC次數:6 。

#include<bits/stdc++.h>
using namespace std;
const int max_n=50000,max_m=100000;
int n,m,k,K,num;
int lin[max_n+10]={},d[max_n+10]={},vis[max_n+10]={},fa[max_n+10]={},f[max_n+10][22],g[max_n+10][22];
struct kk
{
	int id;
	int next;
	int v;
} e[max_n*2+10]={};
struct Kk
{
	int u;
	int v;
	int d;
} E[max_m+10]={};
void insert(int x,int y,int v)
{
	e[++num].next=lin[x];
	lin[x]=num;
	e[num].id=y;
	e[num].v=v;
}
int getfa(int x){ return x==fa[x] ? x : fa[x]=getfa(fa[x]) ; }
bool mycmp(Kk x,Kk y){ return x.d<y.d ; }
void kru()
{
	sort(E+1,E+m+1,mycmp);
	for(int i=1;i<=m;i++)
	{
		int x=getfa(E[i].u);
		int y=getfa(E[i].v);
		if(x!=y)
		{
			fa[x]=y;
			insert(E[i].u,E[i].v,E[i].d);
			insert(E[i].v,E[i].u,E[i].d);
		}
	}
	return ;
}
void dfs(int x)
{
	for(int i=lin[x];i;i=e[i].next)
	{
		if(!vis[e[i].id])
		{
			f[e[i].id][0]=x;
			g[e[i].id][0]=e[i].v;
			d[e[i].id]=d[x]+1;
			vis[e[i].id]=1;
			dfs(e[i].id);
		}
	}
}
int lca(int x,int y)
{
	int ans=0;
	if(d[x]>d[y]) x^=y^=x^=y;
	for(int D=d[y]-d[x],i=0;D;D>>=1,i++)
	 if(D&1) ans=max(ans,g[y][i]),y=f[y][i];
	if(x==y) return ans;
	for(int i=K;i>=0;i--)
	 if(f[x][i]!=f[y][i])
	  ans=max(ans,max(g[x][i],g[y][i])),x=f[x][i],y=f[y][i];
	ans=max(ans,max(g[x][0],g[y][0]));
	return ans;
}
void find()
{
	kru();
	vis[1]=1,d[1]=0;
	dfs(1);
	int N=n;
	for(;N;) N/=2,K++;
	for(int i=1;i<=K;i++)
	 for(int j=1;j<=n;j++)
	  f[j][i]=f[f[j][i-1]][i-1],g[j][i]=max(g[j][i-1],g[f[j][i-1]][i-1]);
}
void clen()
{
	num=0,K=0;
	for(int i=1;i<=n;i++)
	 vis[i]=0,fa[i]=i,lin[i]=0;
}
int main()
{
	for(int I=0;scanf("%d%d",&n,&m)!=EOF;I++)
	{
		if(I) cout<<endl;
		clen();
		for(int i=1;i<=m;i++) scanf("%d%d%d",&E[i].u,&E[i].v,&E[i].d);
		find();
		scanf("%d",&k);
		int s,t;
		for(int i=1;i<=k;i++) 
		{
		    scanf("%d%d",&s,&t);
		    int X=lca(s,t);
		    printf("%d\n",X);
		}
	}
	return 0;
}

5.次小生成樹

       題目大意:求嚴格的次小生成樹。

       題解:每次選擇一條邊加入,然後刪去這條邊兩端端點 x,y 的 LCA 的子樹裏的最大或次大邊(存在相同的情況)。

       AC次數:6 。(判斷錯誤,數組太小)

#include<bits/stdc++.h>
using namespace std;
const int max_n=100000,max_m=300000;
int n,m,num;
long long ans;
int lin[max_n+10],fa[max_n+10],vis[max_n+10],f[max_n+10][22],g[max_n+10][22][2],d[max_n+10],cnt[max_n+10];
struct kk
{
	int id;
	int next;
	int v;
} e[max_n*2+10]={};
struct Kk
{
	int u,v;
	int w;
} E[max_m+10]={};
bool mycmp(Kk x,Kk y){ return x.w<y.w ; }
void insert(int x,int y,int v){ e[++num].next=lin[x]; lin[x]=num; e[num].id=y; e[num].v=v; }
int getfa(int x){ return x==fa[x] ? x : fa[x]=getfa(fa[x]) ; }
void dfs(int x)
{
	for(int i=lin[x];i;i=e[i].next)
	 {
	 	if(e[i].id==f[x][0]) continue;
	 	f[e[i].id][0]=x;
	 	g[e[i].id][0][0]=e[i].v;
	 	d[e[i].id]=d[x]+1;
	 	dfs(e[i].id);
	 }
}
int lca(int t)
{
	int x=E[t].u,y=E[t].v;
	if(d[x]>d[y]) x^=y^=x^=y;
	for(int D=d[y]-d[x],i=0;D;D>>=1,i++)
	 if(D&1) y=f[y][i];
	if(x==y) return x;
	int k=0,N=n;
	for(;N;) N/=2,k++;
	for(int i=k;i>=0;i--)
	 if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
	return f[x][0];
}
int ches(int x,int y,int v)
{
	if(d[x]>d[y]) x^=y^=x^=y;
	int max1,max2;
	max1=max2=0;
	for(int D=d[y]-d[x],i=0;D;D>>=1,i++)
	 if(D&1){
	 	if(g[y][i][0]>max1)
	 	{
	 		max2=max(max1,g[y][i][1]);
	 		max1=g[y][i][0];
		 }
		else
		if(g[y][i][0]==max1)
			max2=max(max2,g[y][i][1]);
		else
			max2=max(max2,g[y][i][0]);
	 	y=f[y][i];
	 }
	if(max1!=v) return v-max1;
	else return v-max2;
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++) scanf("%d%d%d",&E[i].u,&E[i].v,&E[i].w);
	sort(E+1,E+m+1,mycmp);
	for(int i=1;i<=n;i++) fa[i]=i;
	int Cnt=0;
	for(int i=1;i<=m;i++)
	{
		int x=getfa(E[i].u);
		int y=getfa(E[i].v);
		if(x!=y)
		{
			fa[x]=y,ans+=(long long)E[i].w;
			insert(E[i].u,E[i].v,E[i].w);
			insert(E[i].v,E[i].u,E[i].w);
			cnt[i]++;
			Cnt++;
			if(Cnt>=n-1) break;
		}
	}
	f[1][0]=-1;
	dfs(1);
	for(int i=1;(1<<i)<n;i++)
	 for(int j=1;j<=n;j++)
	 if(f[j][i-1]<0) f[j][i]=-1;
	 else{
	 	f[j][i]=f[f[j][i-1]][i-1];
	 	if(g[j][i-1][0]==g[f[j][i-1]][i-1][0])
	 	{
	 		g[j][i][0]=g[j][i-1][0];
	 		g[j][i][1]=max(g[j][i-1][1],g[f[j][i-1]][i-1][1]);
		}
		else
		{
			if(g[j][i-1][0]>g[f[j][i-1]][i-1][0])
			{
				g[j][i][0]=g[j][i-1][0];
				g[j][i][1]=max(g[f[j][i-1]][i-1][0],g[j][i-1][1]);
			}
			else
			{
				g[j][i][0]=g[f[j][i-1]][i-1][0];
				g[j][i][1]=max(g[j][i-1][0],g[f[j][i-1]][i-1][1]);
			}
		}
	 }
	long long ad=E[m].w; 
	for(int i=1;i<=m;i++)
	 if(!cnt[i])
	 {
	 	int x=lca(i);
	 	ad=min(ad,(long long)ches(E[i].u,x,E[i].w));
	 	ad=min(ad,(long long)ches(E[i].v,x,E[i].w));
	 }
	printf("%lld\n",ans+ad);
	return 0;
}

(並不明顯的)小結:數組大小很重要!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章