最近公共祖先 倍增算法

Posted V-sama

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了最近公共祖先 倍增算法相关的知识,希望对你有一定的参考价值。

求最近公共祖先(Lowest Common Ancestor,LCA)
例题:洛谷P3379 【模板】最近公共祖先(LCA)
https://www.luogu.com.cn/problem/P3379

基本思路就是先用倍增把两点升到同一深度,然后倍增来找最近公共祖先。
其中fa数组是关键

#include<iostream>
#include<vector>
#define forup(i,l,r) for(int i=l;i<=r;i++)
#define fordown(i,l,r) for(int i=r;i>=l;i--)
using namespace std;
const int N =5e5+5;
int dep[N],fa[N][20];//fa[u][i]指从u向上跳2^i个结点到达的祖先结点 
//2^20大约为一百万,具体为1048576 
vector<int> child[N];//结点所连的结点(这里包括了父节点) 
int read()

	int n=0;
	char m=getchar();
	while(m<\'0\'||m>\'9\') m=getchar();
	while(m>=\'0\'&&m<=\'9\')
		n=(n<<1)+(n<<3)+(m^\'0\');
		m=getchar();
	
	return n;

void dfs(int father,int u)//作用是初始化dep和fa数组 

	dep[u]=dep[father]+1;
	fa[u][0]=father;
	forup(i,1,19)//跳2^i个相当于是先跳2^(i-1)到fa[u][i-1],然后再往上跳2^(i-1)到fa[u][i] 
	
		fa[u][i]=fa[fa[u][i-1]][i-1];
	
	for(int v:child[u])//对子节点进行访问 
	
		if(v!=father) dfs(u,v);
	

int lca(int u,int v)

	if(u==v) return u;//特判 
	if(dep[u]<dep[v]) swap(u,v);
	fordown(i,0,19)
	
		if(dep[fa[u][i]]>=dep[v])//dep的差=2^x1+2^x2+...+2^xn,故一定可以让u跳到和v同层的地方 
		
			u=fa[u][i]; 
		
	
	if(u==v) return u;//特判二度 
	fordown(i,0,19)
	
		if(fa[u][i]!=fa[v][i])//让u,v跳到最近公共祖先的下一个,原理同上一个for 
		
			u=fa[u][i]; v=fa[v][i];
		
	
	return fa[u][0];

int main()

	int n,m,root;
	n=read(),m=read(),root=read();
	forup(i,1,n-1)//建树
	
		int u,v;
		u=read(),v=read();
		child[v].push_back(u);
		child[u].push_back(v);
	
	dfs(0,root);//作用是初始化dep和fa数组 
	forup(i,1,m)//查询 
	
		int u,v;
		u=read(),v=read();
		cout<<lca(u,v)<<endl;
	
	return 0;

另外,当深度对应的结点数不那么明朗的时候比如不清楚n是2的多少次方,或者需要优化的时候,可以预处理一个lg数组来判断需要最多跳多少层,注意lg里面是深度或深度差
递推公式为lg[i]=lg[i-1]+((1<<(lg[i-1]+1))==i)

#include<iostream>
#include<vector>
#define forup(i,l,r) for(int i=l;i<=r;i++)
#define fordown(i,l,r) for(int i=r;i>=l;i--)
using namespace std;
const int N =5e5+5;
int dep[N],fa[N][20];//fa[u][i]指从u向上跳2^i个结点到达的祖先结点 
//2^20大约为一百万,具体为1048576 
int lg[N];//lg为log2(n)
vector<int> child[N];//结点所连的结点(这里包括了父节点) 
int read()

	int n=0;
	char m=getchar();
	while(m<\'0\'||m>\'9\') m=getchar();
	while(m>=\'0\'&&m<=\'9\')
		n=(n<<1)+(n<<3)+(m^\'0\');
		m=getchar();
	
	return n;

void dfs(int father,int u)//作用是初始化dep和fa数组 

	dep[u]=dep[father]+1;
	fa[u][0]=father;
	forup(i,1,lg[dep[u]])//跳2^i个相当于是先跳2^(i-1)到fa[u][i-1],然后再往上跳2^(i-1)到fa[u][i] 
	
		fa[u][i]=fa[fa[u][i-1]][i-1];
	
	for(int v:child[u])//对子节点进行访问 
	
		if(v!=father) dfs(u,v);
	

int lca(int u,int v)

	if(u==v) return u;//特判 
	if(dep[u]<dep[v]) swap(u,v);
	while(dep[u]>dep[v]) 
		u=fa[u][lg[dep[u]-dep[v]]];//2^lg始终小于等于dep的差 
	
	if(u==v) return u;//特判二度 
	fordown(i,0,lg[dep[u]])
	
		if(fa[u][i]!=fa[v][i])//让u,v跳到最近公共祖先的下一个
		
			u=fa[u][i]; v=fa[v][i];
		
	
	return fa[u][0];

int main()

	int n,m,root;
	n=read(),m=read(),root=read();
	forup(i,2,n)
	
		lg[i]=lg[i-1]+((1<<(lg[i-1]+1))==i);//在此之前i都是处于[2^lg[i-1],2^lg[i])之间的数,即2^lg[i-1]*2>i 
	 
	forup(i,1,n-1)
	
		int u,v;
		u=read(),v=read();
		child[v].push_back(u);
		child[u].push_back(v);
	
	dfs(0,root);//作用是初始化dep和fa数组 
	forup(i,1,m)//查询 
	
		int u,v;
		u=read(),v=read();
		cout<<lca(u,v)<<endl;
	
	return 0;

至于为什么不直接用c++自带的log函数,因为log比20快但是比lg数组慢

参考:https://www.cnblogs.com/dx123/p/16320461.html
https://blog.csdn.net/weixin_45697774/article/details/105289810
次方表:https://blog.csdn.net/weixin_44827418/article/details/106355287

图论-最近公共祖先-在线树上倍增

有关概念:
  最近公共祖先(LCA,Lowest Common Ancestors):对于有根树T的两个结点u、v,最近公共祖先表示u和v的深度最大的共同祖先。

  树上倍增是求LCA的在线算法(对于每一个询问输入后即计算)
思路:
  fa[i][j]表示编号为j的结点从下往上的第2i个祖先
  即fa[0][j]表示j的父结点,递推式为fa[i][j]=fa[i-1][fa[i-1][j]],即j的第2i个祖先为j的第2i-1个祖先的第2i-1个祖先
  另:不存在第2i个祖先(即2i超过j的深度)时,f[i][j]指向根结点
  求LCA的过程分两步,都类似于逐步逼近,先使指针从深度较深的结点向上逼近直至与另一结点深度相同,再让两个指针同时向上逼近LCA,即求出结果

样例推导:

求17、13,16、6,1、15的LCA

指针1指向13,指针2指向17

i=0时,指针2移至fa[i][17],即12,使得深度与指针1相同

i=1时,fa[i][12]==fa[i][13],两指针同时移至5,LCA(17,13)=5

指针1指向6,指针2指向16

i=1时,指针2移至fa[i][16],即8,逼近指针1的深度

i=0时,指针2移至fa[i][8],即5,使得深度与指针1相同

i=0时,fa[i][5]==fa[i][6],两指针同时移至2,LCA(16,6)=2

指针1指向1,指针2指向15

i=2时,指针2移至fa[i][15],即1,使得深度与指针1相同

发现指针1和指针2重合,LCA(1,15)=1

不知大家发现没有,指针1开始一直指向深度较小的结点,这个在程序中有体现,主要是减小代码量

 1 #include<cstdio>
 2 #include<algorithm>
 3 using namespace std;
 4 struct edge
 5 {
 6     int v,next,val;
 7 }e[100005];
 8 int n,m,heads[50005],fa[17][50005],dis[50005],dep[50005],cnt;
 9 void add(int u,int v,int val)
10 {
11     e[++cnt].next=heads[u];
12     heads[u]=cnt;
13     e[cnt].v=v;
14     e[cnt].val=val;
15 }
16 void dfs(int u)//预处理dep和fa[0][j] 
17 {
18     for(int i=heads[u];i;i=e[i].next)
19     {
20         if(e[i].v!=fa[0][u])
21         {
22             dep[e[i].v]=dep[u]+1;
23             fa[0][e[i].v]=u;
24             dis[e[i].v]=dis[u]+e[i].val;
25             dfs(e[i].v);
26         }
27     }
28 }
29 int LCA(int u,int v)
30 {
31     if(dep[u]>dep[v])swap(u,v);
32     for(int i=16;~i;i--)
33         if(dep[fa[i][v]]>=dep[u])
34             v=fa[i][v];
35     if(u==v)return u;
36     for(int i=16;~i;i--)
37         if(fa[i][u]!=fa[i][v])
38         {
39             u=fa[i][u];
40             v=fa[i][v];
41         }
42     return fa[0][u];
43 }
44 int main()
45 {
46     scanf("%d",&n);
47     for(int i=1;i<n;i++)
48     {
49         int x,y,z;
50         scanf("%d%d%d",&x,&y,&z);
51         add(x,y,z);
52         add(y,x,z);
53     }
54     dep[1]=fa[0][1]=1;
55     dfs(1);
56     for(int i=1;i<=16;i++)
57         for(int j=1;j<=n;j++)
58             fa[i][j]=fa[i-1][fa[i-1][j]];
59     scanf("%d",&m);
60     while(m--)
61     {
62         int x,y;
63         scanf("%d%d",&x,&y);
64         printf("%d\\n",dis[x]+dis[y]-2*dis[LCA(x,y)]);//两点之间路径长度 
65     }
66     return 0;
67 }

 

以上是关于最近公共祖先 倍增算法的主要内容,如果未能解决你的问题,请参考以下文章

图论-最近公共祖先-在线树上倍增

最近公共祖先LCA详细解法

最近公共祖先(LCA)问题

lca(最近公共祖先(在线)) 倍增法详解

倍增求LCA

最近公共祖先(LCA)---tarjan算法