浅谈LCA相关算法(tarjan,倍增)

其实LCA算法的理解也没那么麻烦

tarjan

这是一棵树,我们不难发现,9和12的公共祖先是3,且当我们走到12时,9一定被访问过了,3也一定被访问过了

然后tarjan的大脑就发现了一个问题:如果说我们可以知道访问过的点的祖先,那么图中,我们在dfs到节点12时不就可以直接认为9和12的公共祖先是在9之后遍历到的深度最小的点(即3)了吗?

(其实到了这一步,我们就可以发现两种做法,一种使用RMQ,另一种是tarjan,RMQ暂且不提)

因此,我们可以使用并查集来将已遍历过的点归到他们的祖先上,然后将问题存储起来,若问题中的两个节点都被访问过了,那么我们就可以直接返回先被遍历的那个节点的父节点即可

这其实就是tarjan的基本思想

本人自己打的tarjan如下,具体细节都有解释(luoguP3379码风奇特的奇特

#include<bits/stdc++.h>
using namespace std;
#define fre freopen("datain.txt","r",stdin);
//**********************************data
const int maxn=500000+5;
const int maxm=500000+5;
int n,m,s;
int f[maxn],head[maxn],cnt=0;
struct node
{
	int e;
	int next;
}edge[maxn<<1];
struct ans
{
	int question;//某一次询问的对象
	int id;//这个对象对应的序号
};
vector<ans>vec[maxn];//定义一个二维变长数组的方法
bool vis[maxn];
int output[maxm];
//**********************************function defination
inline int read();//快读
int find(int u);//并查集函数
void tarjan(int u,int fa);//tarjan函数(dfs)
void datasetting();//处理输入数据函数
void addl(int u,int v);//边表加边函数
//**********************************main
int main()
{
   fre
   datasetting();
   tarjan(s,-1);//根的fa是-1
   for(int i=0;i<m;i++)printf("%d\n",output[i]);输出ans
   return 0;
}
//**********************************function 
void addl(int u,int v)
{
	edge[cnt].e=v;
	edge[cnt].next=head[u];
	head[u]=cnt++;
}
void datasetting()
{
	memset(head,-1,sizeof(head));
	memset(vis,0,sizeof(vis));
	n=read();m=read();s=read();
	int u,v;
	for(int i=0;i<n-1;i++)
	{
		u=read();v=read();
		addl(u,v);
		addl(v,u);
	}
	for(int i=0;i<m;i++)
	{//为了让答案有序,我们需要将对应的询问节点与询问次序结合起来,于是就想到用结构体(在这里是ans)
		u=read();v=read();
		ans r;r.id=i;r.question=v;
		vec[u].push_back(r);
		r.question=u;
		vec[v].push_back(r);
	}
}
inline int read()
{
	int ans=0;
	char r=getchar();
	while(r<'0'||r>'9')r=getchar();
	while(r>='0'&&r<='9')
	{
		ans=ans*10+r-'0';
		r=getchar();
	}
	return ans;
}
int find(int u)
{
	return f[u]= f[u]==u ? u:find(f[u]);
    //问号表达式,若问号左边的条件成立,则返回冒号左边的值,否则返回冒号右边的值
}
void tarjan(int u,int fa)
{
	f[u]=u;//先将自己作为根节点
	for(int i=head[u];i!=-1;i=edge[i].next)//遍历
	{
		int v=edge[i].e;
		if(v==fa)continue;//这是为什么函数实参里要带一个fa的原因,可以避免从子走到父,保证dfs序
		tarjan(v,u);//向下dfs
		f[find(v)]=u;//在下面的询问都处理完了以后将下面的点并到自己身上来
	}
	vis[u]=true;//dfs标记
	int s=vec[u].size();
	for(int i=0;i<s;i++)//检查每一个关于u的询问
	{
		int v=vec[u][i].question;int e=vec[u][i].id;
		if(vis[v])output[e]=find(v);//若成立,在output数组里面(即答案数组)按顺序放入答案
	}
}
/**********************************
ID:Andrew_82
LANG:C++
PROG:LCA
**********************************/

倍增

以上是tarjan算法,接下来我们来看一下倍增算法

对与一棵树里的两个点,我们要找其最近公共祖先,最容易想到的方法就是将俩个点不断的向上追溯其祖先,直到找到第一个相交的祖先即可

就像这样

以下所有图片来自

http://www.cnblogs.com/yyf0309/p/5972701.html)

但是,一个一个的跳,也太慢了,我们可不可以大步大步的跳?

答案是可以的

设树的最大深度为d,对于每一个节点,最坏情况下与根节点的距离为就是d

然后,我们可以将d拆成不多于20个2的n次方的数相加的形式

每次跳 2 i {2}^{i} 2i步( 0 &lt; i &lt; = ln ⁡ d ln ⁡ 2 0&lt;i&lt;=\frac{\ln d}{\ln2} 0<i<=ln2lnd,i倒序遍历)

这样就可以避免缓慢的跳跃了

这也是倍增算法的原理

当然,要实现这一功能,我们需要一个表 f [ i ] [ j ] f[i][j] f[i][j],表示i号节点向上跳跃 2 j {2}^{j} 2j步所到达的点

这个表可以用以下递推式来求得:

f [ i ] [ j ] = f [ f [ i ] [ j − 1 ] ] [ j − 1 ] f[i][j]=f[f[i][j-1]][j-1] f[i][j]=f[f[i][j1]][j1]

即i向上 2 j {2}^{j} 2j,等价于i向上 2 j − 1 {2}^{j-1} 2j1步后继续向上 2 j − 1 {2}^{j-1} 2j1
其实这不难理解,毕竟:
2 j − 1 + 2 j − 1 = 2 1 × 2 j − 1 = 2 j − 1 + 1 = 2 j {2}^{j-1}+{2}^{j-1}={2}^{1} \times{2}^{j-1}={2}^{j-1+1}={2}^{j} 2j1+2j1=21×2j1=2j1+1=2j
不难看出,f[i][j−1]是i的上层节点,于是递推顺序应该是从上往下(dfs就满足这个特性),当上层的点的f值被算出来后,就可以放心的计算本层的f值了

下面给出代码(luoguP3379)

#include<bits/stdc++.h>
using namespace std;
int n,m,s,maxd=0,b;
const int maxn=500000+5,maxm=500000+5;
int dfn[maxn];//其实这里是指深度而非时间戳
int f[maxn][20];//倍增数组 
struct node{
	int e;
	int next;
}edge[maxm<<1];
int head[maxn],cnt=0;


inline int read()//快读
{
    int ans=0;
    char r;r=getchar();
    while(r<'0'||r>'9')r=getchar();
    while(r>='0'&&r<='9')
    {
        ans=ans*10+r-'0';
        r=getchar();
    }
    return ans;
}


void addl(int u,int v)//加边不说了
{
	edge[cnt].e=v;
	edge[cnt].next=head[u];
	head[u]=cnt++;
}


void datasetting()
{
	memset(head,-1,sizeof(head));
	memset(f,-1,sizeof(f));
	n=read();m=read();s=read();
	int u,v;
	for(int i=0;i<n-1;i++)//based on one
	{
		u=read();v=read();
		addl(u,v);
		addl(v,u);
	}
}


void dfs(int u)//主要目的是要求得最大深度和f[i][0](即每个点的father值),为后面的倍增数组做准备 
{
	if(f[u][0]!=-1)maxd=max(maxd,dfn[u]=dfn[f[u][0]]+1);//同时完成给子节点赋值和求最大深度两件事 
	for(int i=head[u];i!=-1;i=edge[i].next)
	{
		int v=edge[i].e;
		if(v==f[u][0])continue;
		f[v][0]=u;
		dfs(v);
	}
}


void Double()//求倍增数组,i的第2^j个父亲 是i的第2^(j-1)个父亲的第2^(j-1)个父亲。
{
	for(int j=1;j<=b;j++)
	{
		for(int i=1;i<=n;i++)
		{
			if(f[i][j-1]!=-1)f[i][j]=f[f[i][j-1]][j-1];
		}
	}
}


int LCA(int u,int v)//先让u,v处于同一高度,然后在逐次向上以倍增的方式提,直到出答案
{
	if(dfn[u]<dfn[v]){int weret=u;u=v;v=weret;}
	for(int i=b;i>=0;i--)if(f[u][i]!=-1&&dfn[f[u][i]]>=dfn[v])u=f[u][i];
	if(v==u)return v;
	for(int i=b;i>=0;i--)
	{
		if(f[u][i]!=f[v][i])
		{
			u=f[u][i];
			v=f[v][i];
		}
	}
	return f[v][0];
}


int main()
{
	freopen("datain.txt","r",stdin);
	datasetting();
	dfs(s);
	b=(int)(log(maxd)/log(2));//这是在对这棵树中最深的一个点的深度取2的对数
    //这个点向上走2^b层不会超过根节点的深度,而走2^(b+1)步则会
	Double();//其实最好不要用这个名字,因为容易忽略大写D
	int s,e;
	for(int i=0;i<m;i++)
	{
		s=read();e=read();
		printf("%d\n",LCA(s,e));
	}
	return 0;
}
/*************************************************
    ID: Andrew_82
	LANG: C++
	PROG: LCA
*************************************************/

loj#10130:

#include<bits/stdc++.h>
using namespace std;
#define loop(i,start,end) for(register int i=start;i<=end;++i)
#define clean(arry,num); memset(arry,num,sizeof(arry));
#define max(a,b) ((a>b)?a:b)
#define min(a,b) ((a<b)?a:b)
int n,q,cnt=0;
const int maxn=1e5+10;
struct node
{
	int e;
	int nxt;
}edge[maxn<<1];
int head[maxn];
int jump[maxn][30];
int dep[maxn];
inline int read()
{
	int _ans=0;bool _neg=false;char _r=getchar();
	while(_r>'9'||_r<'0'){if(_r=='-')_neg=true;_r=getchar();}
	while(_r>='0'&&_r<='9'){_ans=_ans*10+_r-'0';_r=getchar();}
	return (_neg)?-_ans:_ans;
}
inline void addl(int u,int v)
{
	edge[cnt].e=v;
	edge[cnt].nxt=head[u];
	head[u]=cnt;
	cnt++;
}
void dfs(int u,int fa)
{
	jump[u][0]=fa;
	for(int i=1;(1<<i)<=dep[u];++i)//在dfs模块里打表,代码量减少
	//用(1<<i)<=dep[u]来代替了求树的最大深度,更好理解和操作
		if(jump[u][i-1]!=-1)
			jump[u][i]=jump[jump[u][i-1]][i-1];
	for(int i=head[u];i!=-1;i=edge[i].nxt)
	{
		int v=edge[i].e;
		if(v==fa)continue;
		dep[v]=dep[u]+1;
		dfs(v,u);
	}
}
int lca(int x,int y)
{
	if(x==y)return x;//不要忘了这一句
	if(dep[x]<dep[y]){int t=x;x=y;y=t;}
	for(int i=20;i>=0;--i)
	{
		if(dep[jump[x][i]]>=dep[y])x=jump[x][i];
	}
	if(x==y)return x;//注意这一句也不能忘
	for(int i=20;i>=0;--i)
	{
		if(jump[x][i]!=jump[y][i])x=jump[x][i],y=jump[y][i];
	}//跳到LCA的下面
	return jump[x][0];
}
int main()
{
	#ifndef ONLINE_JUDGE
    freopen("datain.txt","r",stdin);
    #endif
    n=read();
    clean(head,-1);
    clean(jump,-1);
    clean(dep,0);
    loop(i,1,n-1)
    {
    	int x,y;
    	x=read(),y=read();
    	addl(x,y);
    	addl(y,x);
	}
	dfs(1,-1);
	q=read();
	loop(i,1,q)
	{
		int x,y;
		x=read();
		y=read();
		int LCA=lca(x,y);
		printf("%d\n",dep[x]+dep[y]-(dep[LCA]<<1));
	}
	return 0;
}

推荐题目:

CODEVS 2370 小机房的树

CODEVS 1036 商务旅行

METO CODE 223 拉力赛

HDU 2586 How far way?

ZOJ 3195 Design the city

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AndrewMe8211

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值