树的运用:求树上共同祖先LCA

注:求解LCA有至少7种做法,如果让我全部写出来,我会死掉的。这里我只讲朴素和倍增

注:抄别人的代码不是一个好习惯 我不会告诉你这个代码我可以弄了一点点失误进去

版权声明:倍增代码使用的是李白莘莘学子的代码,原文点这里


下面列举一下LCA的做法:朴素算法,倍增,RMQ,用欧拉序列转化为RMQ ,太监(tarjan)和动态树(看这里有惊喜
RMQ以后将分块时可能会去提(前提我记得)(180+行),太监其中有dfs序和邻接链表,不讲(90+行)。树剖以后讲到会提(很快就讲)

//朴素算法
#include<bits/stdc++.h>
#define MAXN 100100
using namespace std
int n,head[MAXN],dep[MAXN],cnt=0,q,fa[MAXN];
struct edge
{
	int nxt,to;
}e[MAXN];
void add(int u,int v)
{
	e[++cnt].nxt=head[u];
	e[cnt].to=v;
	head[u]=cnt;
}
void dep_ccl(int u,int f)//预处理fa[]数组及深度
{
	dep[u]=dep[f]+1;//原点深度等于他爸的深度加一
	fa[u]=f;
	for(int i=head[u];i!=0;i=e[i].nxt)//基操遍历
{
		int v=e[i].to;
		if(v!=f)dep_ccl(v,u);
	}	
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<n;i++)
	{
		int a,b;
		scanf("%d%d",&a,&b);
		add(a,b);
		add(b,a);
	}
	dfs(1,0);
	scanf("%d",&q);
	for(int i=1;i<=q;i++)
	{
		ans=0;
		int a,b;
		scanf("%d%d",&a,&b);
		while(a!=b)//LCA
		{
			if(dep[a]>=dep[b])a=fa[a];
			else b=fa[b];
		}
		cout<<a<<endl;
	}
	return 0;
}
//倍增
#include<include>//万能头 
#define MAXN 200200
using namespace std;
int n,m,s,cnt=0,head[MAXN],dep[MAXN],f[MAXN][23];
int a,a;
struct edge{
    int next,to;
}e[4*MAXN]
void e_add(int u,int v)//链式前向星存图 
{
    cnt++;
    e[cnt].next=head[u];e[cnt].to=v;head[u]=cnt;
    e[++cnt].next=head[v];e[cnt].to=u;head[v]=cnt;
}
void dfs(int u,int father)//对应深搜预处理f数组 
{
    dep[u]=dep[father]+1;
    for(int i=1;(1<<i)<=dep[u];i++)//预处理f数组 
    {
        f[u][i]=f[f[u][i-1]][i-1];
    }
    for(int i=head[u];i;i=e[i].next)//遍历树 
    {
        int v=e[i].to;
        if(v==father)continue;//双向图需要判断是不是父亲节点 
        f[v][0]=u;
        dfs(v,u);
    }
}
int lca(int x,int y)
{
    if(dep[x]<dep[y])swap(x,y);
    for(int i=20;i>=0;i--)//从大到小枚举使x和y到了同一层 
    {
        if(dep[f[x][i]]>=dep[y])x=f[x][i];
        if(x==y)return x;
    }
    for(int i=20;i>=0;i--)//从大到小枚举 
    {
        if(f[x][i]!=f[y][i])//尽可能接近 
        {
            x=f[x][i];y=f[y][i];
        } 
    } 
    return f[x][0];//f[y][0]也ok 
}
int main(){
    scanf("%d%d%d",&n,&m,&s);
    for(int i=1;i<n;i++)
    {
        scanf("%d",&a1);scanf("%d",&a2);
        e_add(a1,a2);//链式前向星存图
    }
    dfs(s,0);//预处理 
    for(int i=1;i<=m;i++)
    {
        scanf("%d %d",&a1,&a2);
        printf("%d\n",lca(a1,a2));//求两个节点的LCA 
    }
} 

首先,是我们的朴素算法O(n^2)。
首先,将树上每个节点的深度预处理出来,还有他们的霸霸预处理出来。
方法:爆搜。开两个函数变量:u(遍历到的节点)和f(u他爸)。将整棵树遍历一遍。
遍历过程中,将fa[u](即u他的fuqin节点)赋值为f,dep[u](即u的深度)赋值为dep[f]+1(即u他爸的深度,以前遍历出来过)


然后在主函数中,定义两个指针a,b,最开始的时候指向要求LCA的那两个点。
每次让更深的那个指针往上跳一个点。即a=fa[a]||b=fa[b]
然后在两指针指向同一个点时,这个点就是他们的LCA,原因你自己想想。这不是废话吗


倍增,就是在朴素算法的基础上,呈倍 增上去。同样,有一个fa数组,需要预处理出来。
其中fa[i][j]表示i的第\(2^j\)个祖先。利用fa的定义(即fa[i][j]表示i的第\(2^j\)个祖先)用循环预处理出fa数组。
其中有个地方做一点点解释

for(int i=20;i>=0;i--)//从大到小枚举使x和y到了同一层 
    {
        if(dep[f[x][i]]>=dep[y])x=f[x][i];
        if(x==y)return x;
    }
    for(int i=20;i>=0;i--)//从大到小枚举 
    {
        if(f[x][i]!=f[y][i])//尽可能接近 
        {
            x=f[x][i];y=f[y][i];
        } 
    } 

这里i为什么是20到1呢?先看代码
我们发现f的第二位的下标都是i,意味着一定是求某个数的\(2^i\)个祖先。
如果你担心,你尽可以开31->1甚至63->1,不过一般题目不会给那么大的数据,一般是2^20左右的,这就是这样来的。


posted @ 2020-10-08 08:47  riced  阅读(141)  评论(0编辑  收藏  举报