LCA问题(DFS+ST)

LCA问题:

        LCA问题是指在一棵二叉树中,求出两个结点的最近公共祖先。
        例如在下面这个简单的二叉树中:

图1

        F和E的LCA是B;
        D和C的LCA是A;
        D和E的LCA是B;
        A和B的LCA是A;

        这里介绍一种用ST表算法来解决此类问题的算法,不了解ST算法的可以先移步:ST算法
        在这种方法中,将树看成了一个无向图,两个点的最近公共祖先一定在他们两个点之间的最短路径上。

算法概述:

  1. 建树,在建树过程中求出每个点的深度。并求出这棵树的欧拉序列。
    • DFS序:对这棵树进行先序遍历,将遍历到的点保存下来,在回溯的过程中,不保存重复的点。例如上面那棵树的DFS序是:A->B->D->F->G->H->I->E->C。
    • 欧拉序;在回溯的过程中,将回溯到的点也保存下来,也就是说要保存已经遍历到的点。例如上面那棵树的欧拉序是:A->B->D->F->D->G->H->G->I->G->D->B->E->B->A->C->A。
    • 对于任意两个结点,在欧拉序中都可以找到他们第一次出现的位置,他们的LCA一定在这两个位置之间的结点中。例如上图中,要找F和E的LCA,F在欧拉序中第一次出现在位置4,E第一次出现在位置13,所以F和E的LCA一定出现在位置4和13之间的结点中(不包括端点F和E),即在D、G、H、I、B中。在这些点中,深度最小的点就是F和E的LCA。
  2. 将欧拉序列中的所有点的深度存到一个数组中。然后使用ST算法维护这个数组的最小值。
    • 假设保存欧拉序列的数组为e,而保存每个结点深度的数组为m,m[i]表示结点i的深度。则在ST初始化DP数组的表达式应该是DP[i][0]=e[ m[i] ]。在DP过程中,保存的是最小值。因为要找这段区间内的哪个点的深度最小。
  3. 对于每组查询,找出这两个结点在欧拉序中第一次出现的位置的下标,用这两个下标进行RMQ查询,查询的结果就是这两个点的LCA。
    • 找两个结点在欧拉序中第一次出现的位置时,如果遍历欧拉序查询就太慢了。可以用生成欧拉序的过程中,用一个数组来保存每个点在欧拉序中第一次出现的位置。

数据结构:

  • tree数组,用来保存树结构。
  • m数组,用来保存每个结点的深度。m[i]表示结点i的深度。
  • e数组,用来保存欧拉序列。
  • index数组,用来保存每个结点在欧拉序中第一次出现的位置。
  • 二维数组DP,用于ST算法维护区间。

算法实现:

        建树过程要完成四件事:

  • 建立二叉树,在这里为了节省时间和空间,使用数组来保存树结构。若父节点的下标是root,则左右子节点的下标分别是:root<<1和(root<<1)+1,。递归方式建立二叉树。
  • 求出每个点的深度。具体方法戳这里:求二叉树每个结点的深度
  • 保存这个二叉树的欧拉序列。欧拉序的求法是进行一次先序遍历,每遍历到一个点就将其加入欧拉序中,当从子节点回溯到父节点时,再将父节点加入到欧拉序中。而我们创建二叉树的过程其实与先序遍历的一样,所以就直接在创建二叉树的过程中求欧拉序。
  • 求出每个结点在欧拉序中第一次出现的位置。这是在求欧拉序的过程中完成的,用index数组来保存。将一个点 i 加入到欧拉序中时判断index[i],若不为0,则表示这不是他第一次出现了。否则将index[i]设成当前欧拉序的长度。代码如下:
//建树,求深度,求欧拉序列,求index数组 
void create(int root,int d)
{
	int a;
	scanf("%d",&a);
	//输入0表示是个空子树
	if(a!=0)
	{
		tree[root]=a;
		e.push_back(tree[root]);
		//记录该结点第一次出现的位置
		if(index[tree[root]]==0)
			index[tree[root]]=e.size()-1;
		//传进来的变量就是该结点的深度 
		m[a]=d;
		//左右子节点的深度加1,并在回溯的过程中将父节点再次加入欧拉序列 
		create(root<<1,d+1);
		if(tree[root<<1])
			e.push_back(tree[root]);
		create(root<<1|1,d+1);
		if(tree[root<<1|1])
			e.push_back(tree[root]);
	}
}

        在进行完这一步之后,我们得到了:完整的树结构,即tree数组;树中每个结点的深度,即m数组;数的欧拉序列,即e数组;树中每个结点在欧拉序中第一次出现的位置,即index数组。

        然后我们将e数组和m数组结合起来,用ST算法来维护。用最简单的模板即可,代码如下:

//ST算法过程 
int  dp[30][20];
void init()
{
	length=e.size()-1;
	//将dp数组初始值设为欧拉序中每个点的深度值。 
	for(int i=1;i<=length;i++)
		dp[i][0]=m[e[i]];
	for(int j=1;(1<<j)<=length;j++)
		for(int i=1;i+(1<<j)<=length+1;i++)
			dp[i][j]=min(dp[i][j-1],dp[i+(1<<j-1)][j-1]);
}
int query(int a,int b)
{
	if(a>b)
		return min(m[e[a-1]],m[e[b+1]]);
	int k=(int)(log((double)(b-a+1))/log(2.0));
	return min(dp[a][k],dp[b-(1<<k)+1][k]);
}

        最后就是处理每一组查询,用index数组快速得到RMQ问题的区间,然后直接输出查询到的值。而在查询时,区间不能包含端点,即两个要查询的点不能包含在区间内,区间要去头去尾。假设有一组查询是F和E,则传给query函数的区间起点和终点应该是index[F]+1和index[E]-1。这里有几个特殊情况需要注意:

  • 若index[F]为3,index[E]为4。传入查询函数的参数是4和3,即起点>终点,就发生错误了。而这种情况说明在欧拉序中,F和E中间就没有别的点,说明这两个点就是父子关系。那么LCA就是两者中深度较小的那个。
  • 若index[F]为4,index[E]为3。即若F比E后出现,那么我们就将index[F]和index[E]交换一下,使之与ST的查询函数适配。
  • 若输入了两个一样的点,则直接返回该点。

C++代码:

#include<iostream>
#include<vector>
#include<cmath>
using namespace std;
//树结构 
int tree[100];

//各节点的深度 
int m[100];

//欧拉序列 
vector<int> e;
int length;

//每个结点在欧拉序中第一次出现的位置
vector<int> index(100,0);

//ST算法过程 
int  dp[30][20];
void init()
{
	length=e.size()-1;
	//将dp数组初始值设为欧拉序中每个点的深度值。 
	for(int i=1;i<=length;i++)
		dp[i][0]=m[e[i]];
	for(int j=1;(1<<j)<=length;j++)
		for(int i=1;i+(1<<j)<=length+1;i++)
			dp[i][j]=min(dp[i][j-1],dp[i+(1<<j-1)][j-1]);
}
int query(int a,int b)
{
	if(a>b)
		return min(m[e[a-1]],m[e[b+1]]);
	int k=(int)(log((double)(b-a+1))/log(2.0));
	return min(dp[a][k],dp[b-(1<<k)+1][k]);
}

//建树,求深度,求欧拉序列,求index数组 
void create(int root,int d)
{
	int a;
	scanf("%d",&a);
	//输入0表示是个空子树
	if(a!=0)
	{
		tree[root]=a;
		e.push_back(tree[root]);
		//记录该结点第一次出现的位置
		if(index[tree[root]]==0)
			index[tree[root]]=e.size()-1;
		//传进来的变量就是该结点的深度 
		m[a]=d;
		//左右子节点的深度加1,并将其加入欧拉序列 
		create(root<<1,d+1);
		if(tree[root<<1])
			e.push_back(tree[root]);
		create(root<<1|1,d+1);
		if(tree[root<<1|1])
			e.push_back(tree[root]);
	}
}

int main()
{
	e.push_back(0);
	create(1,1);
	init();
	int a,b;
	while(cin>>a>>b)
	{
		if(a==b)
		{
			printf("%d\n",a);
			continue;
		}
		a=index[a];
		b=index[b];
		if(a>b)
			swap(a,b);
		printf("%d\n",query(a+1,b-1));
	}
	return 0;
}

对于下面这个图:

图1

        为了方便输入,假设A为1,B为2,C为3.......建图时我们应该输入:1 2 4 6 0 0 7 8 0 0 9 0 0 5 0 0 3 0 0。给出几组查询:F和E、F和C、D和E等,运行结果如下:

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值