学习笔记——LCA

LCA

LCA,即最近公共祖先,在图论中应用比较广泛。

LCA的定义如下:给定一个有根树,若节点z同时是节点x和节点y的祖先,则称 z 是 x,y 的公共祖先;在 x,y 的所有公共祖先当中深度最大的称为 x,y 的最近公共祖先。下面给出三个最近公共祖先的例子:
在这里插入图片描述

显然,从上面的例子可以得出, LCA(x,y)即为 x,y 到根节点的路径的交汇点,也是 x 到 y 的路径上深度最小的节点。

向上标记法求LCA

求LCA最直接的方法,单次查询的时间复杂度最坏为 O(n)(看起来好像还挺快的,不过什么题会只有一次查询呢)
查询方式是从一个点向上搜,到和另一个点深度相同的地方就一起搜直到搜到。代码简单因为太慢了没人用我也懒得打了。
我见没人打

树上倍增求LCA(在线算法)

稍微思考一下,向上标记法之所以求LCA慢,是因为这dd一次只能爬一格,那么我们一次爬很多格不就OK了,根据二进制拆分有了这个算法。

设F(x,y)表示x的 2 y 2^y 2y辈父节点,就是x向上走 2 y 2^y 2y到的节点,如果这个节点不存在那么特判为0。
我们可以建二维数组F[n][m]

往根节点走 2 y 2^y 2y步就是走两个 2 y − 1 2^{y-1} 2y1,这实质上是一个动态规划,深度就是此时的阶段。

有F[x][y]=f[f[x][k-1]][k-1],

就是x的 2 y − 1 2^{y-1} 2y1辈父节点再往上走 2 y − 1 2^{y-1} 2y1.

当我们要求两点的 LCA 时, 先让它们到同一高度. 这个过程我们使用二进制拆分来加速. 比如当两点高度相差 5 时, ( 5 ) 10 = ( 101 ) 2 (5)_{10}=(101)_2 (5)10=(101)2, 那么我们就让高度较小的那个节点先往上爬 2 2 2^2 22=4 步, 再往上 2 0 2^0 20=1 步. 此时两点即在同一高度.

如果爬到同一高度后两点相同, 显然这个点就是它们的 LCA, 直接返回即可.

如果两点不同, 就一起往上爬. 这是一个无限逼近的过程, 直到找到它们的 LCA 的子节点为止.

预处理O(nlogn),每次查询O(logn)

例题在洛谷P1084
模板题
外国友人模板题

/喜闻乐见抄的代码

#include<iostream>
#include<cstdio>
#include<queue>
#include<cmath>
using namespace std;
const int N=6e5;
int n,m,s,t,tot=0,f[N][20],d[N],ver[2*N],Next[2*N],head[N];
queue<int> q;
void add(int x,int y)
{
    ver[++tot]=y,Next[tot]=head[x],head[x]=tot;
}//邻接表存边操作。由于只求LCA时不关心边权,因此可以不存边权
void bfs()
{
    q.push(s);
    d[s]=1;//将根节点入队并标记
    while(q.size())
    {
        int x=q.front();q.pop();//取出队头
        for(int i=head[x];i;i=Next[i])
        {
            int y=ver[i];
            if(d[y])
                continue;
            d[y]=d[x]+1;
            f[y][0]=x;//初始化,因为y的父亲节点就是x
            for(int j=1;j<=t;j++)
                f[y][j]=f[f[y][j-1]][j-1];//递推f数组
            q.push(y);
        }
    }
}
int lca(int x,int y)
{
    if(d[x]>d[y])
        swap(x,y);
    for(int i=t;i>=0;i--)
        if(d[f[y][i]]>=d[x])
            y=f[y][i];//尝试上移y
    if(x==y)
        return x;//若相同说明找到了LCA
    for(int i=t;i>=0;i--)
        if(f[x][i]!=f[y][i])
        {
            x=f[x][i],y=f[y][i];
        }//尝试上移x、y并保持它们不相遇
    return f[x][0];//当前节点的父节点即为LCA
}
int main()
{
    cin>>n>>m>>s;
    t=log2(n)+1;
    for(int i=1;i<n;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y),add(y,x);
    }
    bfs();
    while(m--)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        printf("%d\n",lca(a,b));
    }
    return 0;
}

Tarjan求LCA(离线)

众所周知,Tarjan是精心设计的dfs,通过dfs的遍历特性巧妙的合并已查询点,从而实现寻找LCA。
有集合之间的关系所以用到并查集

算法思路

1.任选一个点为根节点,从根节点开始。

2.遍历该点u所有子节点v,并标记这些子节点v已被访问过。

3.若是v还有子节点,返回2,否则下一步。

4.合并v到u上。

5.寻找与当前点u有询问关系的点v。

6.若是v已经被访问过了,则可以确认u和v的最近公共祖先为v被合并到的父亲节点a。

是不是有些抽象,那么来看我找的一个超棒的算法演示

链接

具体实现也是从里面抄的

#include<bits/stdc++.h>
using namespace std;
int n,k,q,v[100000];
map<pair<int,int>,int> ans;//存答案
int t[100000][10],top[100000];//存储查询关系
struct node{
    int l,r;
};
node s[100000];
/*并查集*/
int fa[100000];
void reset(){
    for (int i=1;i<=n;i++){
        fa[i]=i;
    }
}
int getfa(int x){
    return fa[x]==x?x:getfa(fa[x]);
}
void marge(int x,int y){
    fa[getfa(y)]=getfa(x);
}
/*------*/
void tarjan(int x){
    v[x]=1;//标记已访问
    node p=s[x];//获取当前结点结构体
    if (p.l!=-1){
        tarjan(p.l);
        marge(x,p.l);
    }
    if (p.r!=-1){
        tarjan(p.r);
        marge(x,p.r);
    }//分别对l和r结点进行操作
    for (int i=1;i<=top[x];i++){
        if (v[t[x][i]]){
            cout<<getfa(t[x][i])<<endl;
        }//输出
    }
}
int main(){
    cin>>n>>q;
    for (int i=1;i<=n;i++){
        cin>>s[i].l>>s[i].r;
    }
    for (int i=1;i<=q;i++){
        int a,b;
        cin>>a>>b;
            t[a][++top[a]]=b;//存储查询关系
            t[b][++top[b]]=a;
    }
    reset();//初始化并查集
    tarjan(1);//tarjan 求 LCA
}

U1S1 Tarjan是真的强,在学习SCC的时候就认识了他。时间复杂度 O ( n α ( n ) ) O(nα(n)) O(nα(n)),就是并查集的时间复杂度。

树链剖分求LCA

原文链接

假如两条加粗的路径AA’,BB’为重链,则求A、B的LCA时,只需取链顶深度较大的B’并将B跳到B’的父节点C,C在链AA’上,于是就能很快得到A、B的LCA为C,而不需要一次次往上寻找。
在这里插入图片描述
链的选取我们用树链剖分,于是有了下面抄的代码

#include <cstdio>
#include <cstdlib>
#define maxm 200010
struct edge{int to,len,next;}E[maxm];
int cnt,last[maxm],fa[maxm],top[maxm],deep[maxm],siz[maxm],son[maxm],val[maxm];
void addedge(int a,int b,int len=0)
{
    E[++cnt]=(edge){b,len,last[a]},last[a]=cnt;
}
void dfs1(int x)
{
    deep[x]=deep[fa[x]]+1;siz[x]=1;
    for(int i=last[x];i;i=E[i].next)
    {
        int to=E[i].to;
        if(fa[x]!=to&&!fa[to]){
            val[to]=E[i].len;
            fa[to]=x;
            dfs1(to);
            siz[x]+=siz[to];
            if(siz[son[x]]<siz[to])son[x]=to;
        }
    }
}
void dfs2(int x)
{
    if(x==son[fa[x]])top[x]=top[fa[x]];
    else top[x]=x;
    for(int i=last[x];i;i=E[i].next)if(fa[E[i].to]==x)dfs2(E[i].to);
}
void init(int root){dfs1(root),dfs2(root);}
int query(int x,int y)
{
    for(;top[x]!=top[y];deep[top[x]]>deep[top[y]]?x=fa[top[x]]:y=fa[top[y]]);
    return deep[x]<deep[y]?x:y;
}
int n,m,x,y,v;
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<n;i++)
    {
        scanf("%d%d",&x,&y);addedge(x,y,v);addedge(y,x,v);
    }
    init(1);
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d",&x,&y);
        printf("%d\n",query(x,y));
    }
    return 0 ;
}

LCA->RMQ

如RMQ问题蒟蒻并不会的正解所示,LCA可以转化为约束RMQ求解。

来自这里!

我们深度遍历树时,我们没遇到一个未访问过的节点就将其存入到数组中,同时记录下来的还有它的深度;

当我们完成了对每一个节点u的所有分支访问后,会回溯到该节点u,我们也将回溯时遇到的节点存入数组中,同时记录下来的还有它的深度。

这样我们就能够用数组清楚的记录下来深度遍历时访问节点的完整路径流程。另外我们还用一个数组记录好每个节点第一次访问到的序,便于我们后续操作。

在这里插入图片描述
下图中,其中i表示次序,node[i]表示先后访问的节点(包括回溯时的),depth[i]表示第i个访问的节点的深度(包括回溯时访问的)

在这里插入图片描述
下图中i表示节点,R[i]表示相应的节点第一次被访问的次序


那么不难发现,对于任意两个节点u、v,我们根据R[u]和R[v]找到其第一次出现的次序fu、fv,可以发现,u和v的最近祖depth[fu]到depth[fv]中深度最小的那个值对应的节点(我们假设fu<fv)。所以对于求最近祖先问题我们可以转化为求depth[fu]和depth[fv]之间最小的值问题,然后根据这个最小的值对应的下标x到node数组中找打node[x],这个node[x]就是节点u和节点v的最近公共祖先。

举个例子,如上图中,对于节点5和节点10,二者对应的R[5]和R[10]分别为5和14,然后我们找到depth[5]和depth[14]之间最小的深度是1,期下标值为6,然后我们根据这个下标,找到node[6]的值为3,那么它俩的最近公共祖先就是节点3。

代码实现

#include<iostream>
#include<math.h>
#include<vector>
using namespace std;
vector<int> tree[105];                        //假设有105个节点的树
int R[105];                                   //用于深度遍历时每个节点第一次出现的次序
int depth[10005];                             //记录深度遍历时经过的节点的深度的值,一个节点课重复记录
int node[10005];                              //记录深度遍历时先后经过的节点,可重复记录
int count=1;                                  //记录好次序,count从1开始
int ns;                                       //树中节点的个数
 
void inputTree()						      //输入树
{
	cin>>ns;								  //树的顶点数
	for(int i=1;i<=ns;i++)					  //初始化树,顶点编号从0开始
		tree[i].clear();
 
	for(i=1;i<ns;i++)						  //输入n-1条树边
	{
		int x, y; 
		cin>>x>>y;							  //x->y有一条边
		tree[x].push_back(y); 
	}
}
 
 
/*
深度遍历为上述几个数组初始化,为RMQ提供数据
*/
void dfs(int u,int deep){
	node[count]=u;                            //记录好次序
	depth[count]=deep;
 
	if(!R[u])                                 //如果这个节点还未出现过
		R[u]=count;                           //则记录第一次出现的顺序
	
	count++;
 
	for(int i=0;i<tree[u].size();i++){
		if(!R[tree[u][i]])                    //如果还未访问过,则访问
			dfs(tree[u][i],deep+1);
 
		//记录好回溯时的顺序
		node[count]=u;
		depth[count]=deep;
		count++;
	}
}
 
 
/*************************ST算法的实现***********************************/
int ST[10005][30];                              //用于动态规划的st数组
int n;                                          //元素的个数,就是上面的count
//初始化ST数组
void initST(){
	for(int i=0;i<n;i++)                        //长度为2^0(也就是长度为1)的最小值就是本身
		ST[i][0]=depth[i];
}
 
//求2的次幂函数
int getTwo(int i){
	int res=1;
	while(i>0){
		res*=2;
		i--;
	}
	return res;
}
 
//计算完整的ST表
void calST(){
	int logs=(int)(log(n)/log(2));              //求出最多向上递归的次数
 
	for(int j=1;j<=logs;j++){                   //因为j=0的情况在initST中已经算了
		for(int i=0;i<n;i++){
			int tmp=getTwo(j-1);
			if(i+tmp+1<n)                       //即两段都存在
				ST[i][j]=ST[i][j-1]>ST[i+tmp][j-1]?ST[i+tmp][j-1]:ST[i][j-1];//二者当中取相对小的
			else
				ST[i][j]=ST[i][j-1];            //只剩下前半段,则只用前半段
		}
	}
}
 
/********************************ST算法实现结束*********************************/
 
void init(){
	memset(R,0,sizeof(R));
	dfs(1,0);
 
	n=count;
	initST();
	calST();
}
/*
求节点u和节点v的最近祖先
*/
int RMQ(int u,int v){
	u=R[u];                                     //得到第一次出现的次序
	v=R[v];
 
	if(u>v){                                    //保证u小
		int tmp=u;
		u=v;
		v=tmp;
	}
 
	int logs=(int)(log(v-u+1)/log(2));          //区间长度最大2的多少次幂
 
	int res=ST[u][logs]>ST[v-getTwo(logs)+1][logs]?ST[v-getTwo(logs)+1][logs]:ST[u][logs];
 
	for(int i=u;i<v;i++)
		if(depth[i]==res)
			break;
 
	return node[i];
}
 
int main(){
	inputTree();
 
    init();
 
	int m;
	int u,v;
	cin>>m;
 
	for(int i=0;i<m;i++){
		cin>>u>>v;
		cout<<RMQ(u,v)<<endl;
	}
	/*
	测试用例
	13
	1 2 
	1 3 
	1 4 
	3 5 
	3 6 
	3 7 
	6 8 
	6 9 
	7 10 
	7 11
	10 12 
	10 13
	*/
	return 0;
}
 
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值