LCA算法实际应用 PAT (Advanced Level) Practice 1151 LCA in a Binary Tree (30分) 四种方法完成题目+tarjan详细讲解!

1.题目

The lowest common ancestor (LCA) of two nodes U and V in a tree is the deepest node that has both U and V as descendants.

Given any two nodes in a binary tree, you are supposed to find their LCA.

Input Specification:

Each input file contains one test case. For each case, the first line gives two positive integers: M (≤ 1,000), the number of pairs of nodes to be tested; and N (≤ 10,000), the number of keys in the binary tree, respectively. In each of the following two lines, N distinct integers are given as the inorder and preorder traversal sequences of the binary tree, respectively. It is guaranteed that the binary tree can be uniquely determined by the input sequences. Then M lines follow, each contains a pair of integer keys U and V. All the keys are in the range of int.

Output Specification:

For each given pair of U and V, print in a line LCA of U and V is A. if the LCA is found and A is the key. But if A is one of U and V, print X is an ancestor of Y. where X is A and Y is the other node. If U or V is not found in the binary tree, print in a line ERROR: U is not found. or ERROR: V is not found. or ERROR: U and V are not found..

Sample Input:

6 8
7 2 3 4 6 5 1 8
5 3 7 2 6 4 8 1
2 6
8 1
7 9
12 -3
0 8
99 99

Sample Output:

LCA of 2 and 6 is 3.
8 is an ancestor of 1.
ERROR: 9 is not found.
ERROR: 12 and -3 are not found.
ERROR: 0 is not found.
ERROR: 99 and 99 are not found.

参考(https://blog.csdn.net/lw277232240/article/details/77017517

LCA:在一棵没有环的树上,每个节点肯定有其父亲节点和祖先节点,而最近公共祖先,就是两个节点在这棵树上深度最大的公共的祖先节点。

换句话说,就是两个点在这棵树上距离最近的公共祖先节点。

所以LCA主要是用来处理当两个点仅有唯一一条确定的最短路径时的路径。

2.代码一

参考(https://blog.csdn.net/liuchuo/article/details/82560863

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<string>
#include<unordered_map>
using namespace std;

	int in[10002], pre[10002];
    unordered_map<int,int>pos;
	void lca(int l, int r, int root, int aa, int bb)
	{
		if (l > r)return;
		if (pos[pre[root]]>pos[aa] && pos[pre[root]] > pos[bb])
			lca(l, pos[pre[root]] - 1, root + 1, aa, bb);

		else if((pos[pre[root]]<pos[aa]&& pos[pre[root]] > pos[bb])||(pos[pre[root]]>pos[aa] && pos[pre[root]] < pos[bb]))
			printf("LCA of %d and %d is %d.
", aa, bb, pre[root]);

		else if(pos[pre[root]]<pos[aa] && pos[pre[root]] < pos[bb])
		 lca(pos[pre[root]] + 1, r, root+1+(pos[pre[root]]-l), aa, bb);

		else if(pos[aa]==pos[pre[root]])
			printf("%d is an ancestor of %d.
", aa, bb);
		else if (pos[bb] == pos[pre[root]])
			printf("%d is an ancestor of %d.
", bb, aa);

	}

	int main()
	{

		int m, n;
		scanf("%d %d",&m,&n);
		for (int i = 1; i <=n; i++)
		{
			scanf("%d",&in[i]);
			pos[in[i]] = i;
		}
		for (int i = 1; i <=n; i++)
			scanf("%d",&pre[i]);
		for (int i = 1; i <=m; i++)
		{
			int aa, bb;
			scanf("%d %d",&aa,&bb);
			if (pos[aa] == 0 && pos[bb]!=0)
				printf("ERROR: %d is not found.
", aa);
			else 	if (pos[aa] != 0 && pos[bb] == 0)
				printf("ERROR: %d is not found.
", bb);
			else	if (pos[aa] == 0 && pos[bb] == 0)
				printf("ERROR: %d and %d are not found.
", aa, bb);
			else 	if (pos[aa] != 0 && pos[bb] != 0)
				lca(1, n, 1, aa, bb);
		}
	}

 3.代码一讲解

1.思路

1.开始没有建树,因为只需要用index来进行左右子树的判断与控制。于是记录中序遍历中的值的下标于pos中

(开始是用的数组,但是这样是错的!因为数组的初始值为0,如果中序遍历中有值为0的话,就分不清0在中序遍历中的位置;之后用了map,但是出现测试点超时,将cin、cout都改为scanf、printf还不行,于是将map改为unordered_map,通过)

2.之后在递归中比较这三者:在中序遍历中根的位置坐标大小(暂时设为A)、在中序遍历中待寻找值aa的位置坐标大小(暂时设为B)、在中序遍历中待寻找值bb的位置坐标大小(暂时设为C):

如果A>B而且A>C:说明两个带寻找点都在根的左子树,于是递归左子树;

如果A<B而且A<C:说明两个带寻找点都在根的右子树,于是递归右子树;

如果A>B、A<C:说明一个在左子树,一个在右子树,输出根节点。

3.不存在的节点pos对应的坐标就为初值0,可以直接判断。 

void lca(int l, int r, int root, int aa, int bb)
	{

		if (pos[pre[root]]>pos[aa] && pos[pre[root]] > pos[bb])
			lca(l, pos[pre[root]] - 1, root + 1, aa, bb);
//上面是都在左子树的情况,l不变,r变为中序遍历中根位置的后一个,root位置加一

		else if((pos[pre[root]]<pos[aa]&& pos[pre[root]] > pos[bb])||(pos[pre[root]]>pos[aa] && pos[pre[root]] < pos[bb]))
			printf("LCA of %d and %d is %d.
", aa, bb, pre[root]);

//直接输出
		else if(pos[pre[root]]<pos[aa] && pos[pre[root]] < pos[bb])
		 lca(pos[pre[root]] + 1, r, root+1+(pos[pre[root]]-l), aa, bb);
//上面是都在右子树的情况,l变为根后面一个,r不变,root位置:(pos[pre[root]]-l)
//是中序遍历前面左子树的
//节点总个数,要找右子树就后移(pos[pre[root]]-l)个位置,+1是再移动一个根的位置

		else if(pos[aa]==pos[pre[root]])
			printf("%d is an ancestor of %d.
", aa, bb);
//如果没在上面输出,就说明两个点是上下紧紧连着的,于是在这里输出
		else if (pos[bb] == pos[pre[root]])
			printf("%d is an ancestor of %d.
", bb, aa);

	}

3.代码二

#include<iostream>
#include<cstdio>
using namespace std;
typedef struct node *tree;
struct node
{
	int data;
	tree left;
	tree right;
};
int in[10001], pre[10001];
tree creat(tree t,int pree[],int inn[],int n)
{
	int k;
	if (n <= 0)return 0;
	if (t == NULL)
	{
		t = (tree)malloc(sizeof(struct node));
		t->left = t->right = NULL;
	}
	t->data = pree[0];
	for ( k = 0; k < n; k++)
		if (inn[k] == t->data)
			break;
	t->left = creat(t->left,pree+1,inn,k);
	t->right = creat(t->right, pree +k+ 1, inn+k+1, n-k-1);
	return t;
}
bool find(int a,int n)
{
	for (int i = 0; i < n; i++)
	{
		if (a == pre[i])return true;
	}
	return false;
}

tree lca(tree t, int a,int b)
{
	if (!t)return NULL;
	if (t->data == a || t->data == b)
		return t;
	tree left=lca(t->left, a, b);
	tree right=lca(t->right, a, b);
	if (left != NULL&&right != NULL)
		return t;
	return left == NULL ? right : left;
}
int main()
{

	int m,n;
	cin >>m >> n;
	for (int i = 0; i < n; i++)
		cin >> in[i];
	for (int i = 0; i < n; i++)
		cin >> pre[i];
	tree t=NULL;
	 t = creat(t,pre,in,n);
	 for (int i = 0; i < m; i++)
	 {
		 int aa, bb;
		 cin >> aa >> bb;
		 bool af = find(aa, n);
		 bool bf = find(bb, n);
		 if (af == false && bf != false)
			 printf("ERROR: %d is not found.
", aa);
		 else if (af != false && bf == false)
			 printf("ERROR: %d is not found.
", bb);
		 else if (af == false && bf == false)
			 printf("ERROR: %d and %d are not found.
", aa, bb);
		 else if (af != false && bf != false)
		 {
			 tree temp = lca(t, aa,bb);
			 if (temp->data== aa)printf("%d is an ancestor of %d.
", aa, bb);
			 else if (temp->data == bb)printf("%d is an ancestor of %d.
", bb, aa);
			 else printf("LCA of %d and %d is %d.
", aa, bb, temp->data);
		 }
	 }
}

4.代码二思路

参考(https://blog.csdn.net/qq_41317652/article/details/82557975

1.通过中序遍历于先序遍历实实在在使用指针建树:

tree creat(tree t,int pree[],int inn[],int n)
{
	int k;
	if (n <= 0)return 0;
	if (t == NULL)
	{
		t = (tree)malloc(sizeof(struct node));
		t->left = t->right = NULL;
	}
	t->data = pree[0];//先序遍历中开始的为根节点
	for ( k = 0; k < n; k++)
		if (inn[k] == t->data)
			break;//找到中序遍历中的根节点
	t->left = creat(t->left,pree+1,inn,k);
//先序数组头指针加一,中序不变,总个数变为K
	t->right = creat(t->right, pree +k+ 1, inn+k+1, n-k-1);
//先序数组头指针加一后,再向后移动K(左子树的总结点个数)个位置,中序同理,但是注意个数要-k(少了左子树),再减一
	return t;
}

2.此题(二叉树情景下)的LCA算法

在二叉树中寻找两个数字,一旦找到就返回这个数字,

如果一个节点的left、right返回值都不为空,就说明两个数分别在这个节点的左右子树,于是返回这个节点;

如果只是一个不为空,说明两个节点是紧紧相连,在同一侧,这样也返回这个节点,之后在函数外进行判断。

tree lca(tree t, int a,int b)
{
	if (!t)return NULL;
	if (t->data == a || t->data == b)
		return t;
	tree left=lca(t->left, a, b);
	tree right=lca(t->right, a, b);
	if (left != NULL&&right != NULL)
		return t;
	return left == NULL ? right : left;
}

5.代码三

参考(https://blog.csdn.net/coderwait/article/details/100602057

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
struct node
{
	int data;
	int level;
	int father;
}pre[10001];
int in[10001];
void creat(int root, int left, int right, int father, int level)
{
	if (left > right)return;
	pre[root].father = father;
	pre[root].level = level;
	int i=0;
	while (in[i] != pre[root].data)i++;
	creat(root + 1, left, i - 1, root, level + 1);//注意这里的root
	creat(root +i+ 1-left, i+1, right, root, level + 1);//注意这里的root +i+ 1-left
}
int find(int a, int n)
{
	for (int i = 0; i < n; i++)
	{
		if (a == pre[i].data)return i;
	}
	return n;
}
int main()
{

	int m, n;
	cin >> m >> n;
	for (int i = 0; i < n; i++)
		cin >> in[i];
	for (int i = 0; i < n; i++)
		cin >> pre[i].data;
	creat(0, 0, n- 1, -1, 0);
	for (int i = 0; i < m; i++)
	{
		int aa, bb;
		cin >> aa >> bb;
		int af = find(aa, n);
		int bf = find(bb, n);
		if (af == n && bf != n)
			printf("ERROR: %d is not found.
", aa);
		else if (af != n && bf == n)
			printf("ERROR: %d is not found.
", bb);
		else if (af == n && bf == n)
			printf("ERROR: %d and %d are not found.
", aa, bb);
		else if (af != n && bf != n)
		{
			if (pre[af].level < pre[bf].level)
			{
				swap(af, bf);
			}
			while (pre[af].level != pre[bf].level)
			{
				af = pre[af].father;
			}
			while (pre[af].data != pre[bf].data)
			{
				af = pre[af].father;
				bf = pre[bf].father;
			}

			if (pre[af].data == aa)printf("%d is an ancestor of %d.
", aa, bb);
			else if (pre[af].data == bb)printf("%d is an ancestor of %d.
", bb, aa);
			else printf("LCA of %d and %d is %d.
", aa, bb, pre[af].data);
		}
	}
}

6.代码三思路

1.建树

使用静态建树,但是特点是加入了level层数、father父辈

void creat(int root, int left, int right, int father, int level)
//root 先序遍历的根节点位置,left、right是中序遍历的寻找范围,father初值为-1,level初值为0
{
	if (left > right)return;
	pre[root].father = father;
	pre[root].level = level;
	int i=0;
	while (in[i] != pre[root].data)i++;//寻找根节点
	creat(root + 1, left, i - 1, root, level + 1);
//注意这里的root向前移动一个位置,中序的right变为i-1,left不变,层数level加一
	creat(root +i+ 1-left, i+1, right, root, level + 1);
//注意这里的root +i+ 1-left,(i-left为左子树节点个数),+1为移动一个根节点位置
}

2.判断


			if (pre[af].level < pre[bf].level)//保证af为较深的一个(相等就无所谓了)
			{
				swap(af, bf);
			}
			while (pre[af].level != pre[bf].level)//让深得那个网上走找父亲,直到与浅的深度相等
			{
				af = pre[af].father;
			}
			while (pre[af].data != pre[bf].data)//同时向上走,直到二者汇聚于一点
			{
				af = pre[af].father;
				bf = pre[bf].father;
			}

7.代码四

参考(https://blog.csdn.net/alex1997222/article/details/86677221


#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<string>
using namespace std;
struct node
{
	int left;
	int right;
}list[1001000];
int in[10001], pre[10001];
bool visited[10001];
int nfindlist[1001000];
int  creat(int ins, int ine, int pres, int pree, int n)
{
	if (pres > pree) return -1;
	int i = 0;
	int inroot = -1;
	int preroot = pre[pres];
	for (i = ins; i <= ine; i++)
	{
		if (in[i] == preroot)
		{
			inroot = in[i];
			break;
		}
	}
	if (inroot == -1)return -1;
	int numk = i - ins;  //左子树有多少个元素
	list[inroot].left = creat(ins, i - 1, pres + 1, pree + numk, n);//注意这里的list[inroot].left中的inroot,而且numk是左子树的节点总个数,pree + numk要跳过这么多
	list[inroot].right = creat(i + 1, ine, pres + numk + 1, pree, n);//pres + numk+1加一是多一个根节点
	return inroot;
}

int find(int a, int n)
{
	for (int i = 0; i < n; i++)
	{
		if (a == pre[i])return i;
	}
	return n;
}

int nfind(int root)//并查集
{
	if (root == nfindlist[root])return root;
	else return nfind(nfindlist[root]);
}

int ancestor = -1;
void tarjan(int root, int a, int b, int father)
{
	if (root == -1)return;
	tarjan(list[root].left, a, b, root);
	tarjan(list[root].right, a, b, root);
	if (root == a)
	{
		if (visited[b])ancestor = nfind(b);
	}
	if (root == b)
	{
		if (visited[a])ancestor = nfind(a);
	}
	visited[root] = true;
	nfindlist[root] = father;//注意这句话!!

}
int main()
{

	int m, n;
	cin >> m >> n;
	for (int i = 0; i < n; i++)
		cin >> in[i];
	for (int i = 0; i < n; i++)
		cin >> pre[i];
	creat(0, n - 1, 0, n - 1, n);
	int root = pre[0];
	for (int i = 0; i < m; i++)
	{
		int aa, bb;
		cin >> aa >> bb;
		int af = find(aa, n);
		int bf = find(bb, n);
		if (af == n && bf != n)
			printf("ERROR: %d is not found.
", aa);
		else if (af != n && bf == n)
			printf("ERROR: %d is not found.
", bb);
		else if (af == n && bf == n)
			printf("ERROR: %d and %d are not found.
", aa, bb);
		else if (af != n && bf != n)
		{
			ancestor = root;
			memset(visited, 0, n);
			for (int i = 0; i <=10000; ++i)
			{
				nfindlist[i] = i;
			}
			tarjan(root, aa, bb, root);
			if (ancestor == aa)printf("%d is an ancestor of %d.
", aa, bb);
			else if (ancestor == bb)printf("%d is an ancestor of %d.
", bb, aa);
			else printf("LCA of %d and %d is %d.
", aa, bb, ancestor);
		}
	}
}

8.代码四思路

该思路为通用思路:LCA的离线算法:tarjan算法(但是本题中并未用离线实现,而是在线判断,即每次都要判断一次,但是没有超时)

1.tarjan算法解析

参考(https://blog.csdn.net/lw277232240/article/details/77017517,这里借用这位同学的解析,大家接着看方便,自己复习也方便,侵请告知自会删除谢谢!)

 1.步骤

Tarjan算法的基本思路:

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

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

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

      4.合并v到u上。

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

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

    遍历的话需要用到dfs来遍历(我相信来看的人都懂吧...),至于合并,最优化的方式就是利用并查集来合并两个节点。

2.模拟过程

假设我们有一组数据 9个节点 8条边 联通情况如下:

    1--2,1--3,2--4,2--5,3--6,5--7,5--8,7--9 即下图所示的树

    设我们要查找最近公共祖先的点为9--8,4--6,7--5,5--3;

    设f[]数组为并查集的父亲节点数组,初始化f[i]=i,vis[]数组为是否访问过的数组,初始为0; 

    下面开始模拟过程:

    取1为根节点,往下搜索发现有两个儿子2和3;

    先搜2,发现2有两个儿子4和5,先搜索4,发现4没有子节点,则寻找与其有关系的点;

    发现6与4有关系,但是vis[6]=0,即6还没被搜过,所以不操作

    发现没有和4有询问关系的点(在所有的测试集合中找有4的测试对,其实就是将所有的测试对都记录下一起测试以节省时间不用测一次找一次,这就是所说的离线形式了,返回此前一次搜索,更新vis[4]=1;

    

    表示4已经被搜完,更新f[4]=2,继续搜5,发现5有两个儿子7和8;

    先搜7,发现7有一个子节点9,搜索9,发现没有子节点,寻找与其有关系的点;

    发现8和9有关系,但是vis[8]=0,即8没被搜到过,所以不操作

    发现没有和9有询问关系的点了,返回此前一次搜索,更新vis[9]=1;

    表示9已经被搜完,更新f[9]=7,发现7没有没被搜过的子节点了,寻找与其有关系的点;

    发现5和7有关系,但是vis[5]=0,所以不操作;

    发现没有和7有关系的点了,返回此前一次搜索,更新vis[7]=1;

    

    表示7已经被搜完,更新f[7]=5,继续搜8,发现8没有子节点,则寻找与其有关系的点;

    发现9与8有关系,此时vis[9]=1,则他们的最近公共祖先为find(9)=5;

      (find(9)的顺序为f[9]=7-->f[7]=5-->f[5]=5 return 5;并查集)

    发现没有与8有关系的点了,返回此前一次搜索,更新vis[8]=1;

    表示8已经被搜完,更新f[8]=5,发现5没有没搜过的子节点了,寻找与其有关系的点;

    

    发现7和5有关系,此时vis[7]=1,所以他们的最近公共祖先为find(7)=5;

      (find(7)的顺序为f[7]=5-->f[5]=5 return 5;)

    又发现5和3有关系,但是vis[3]=0,所以不操作,此时5的子节点全部搜完了;

    返回此前一次搜索,更新vis[5]=1,表示5已经被搜完,更新f[5]=2;

    发现2没有未被搜完的子节点,寻找与其有关系的点;

    又发现没有和2有关系的点,则此前一次搜索,更新vis[2]=1;

    

    表示2已经被搜完,更新f[2]=1,继续搜3,发现3有一个子节点6;

    搜索6,发现6没有子节点,则寻找与6有关系的点,发现4和6有关系;

    此时vis[4]=1,所以它们的最近公共祖先为find(4)=1;

      (find(4)的顺序为f[4]=2-->f[2]=2-->f[1]=1 return 1;)

    发现没有与6有关系的点了,返回此前一次搜索,更新vis[6]=1,表示6已经被搜完了;

    

    更新f[6]=3,发现3没有没被搜过的子节点了,则寻找与3有关系的点;

    发现5和3有关系,此时vis[5]=1,则它们的最近公共祖先为find(5)=1;

      (find(5)的顺序为f[5]=2-->f[2]=1-->f[1]=1 return 1;)

    发现没有和3有关系的点了,返回此前一次搜索,更新vis[3]=1;

    

    更新f[3]=1,发现1没有被搜过的子节点也没有有关系的点,此时可以退出整个dfs了。

 2.建树

这里也是使用静态建树,但是与代码三也有不同

int  creat(int ins, int ine, int pres, int pree, int n)
{
	if (pres > pree) return -1;
	int i = 0;
	int inroot = -1;
	int preroot = pre[pres];
	for (i = ins; i <= ine; i++)
	{
		if (in[i] == preroot)
		{
			inroot = in[i];
			break;
		}
	}
	if (inroot == -1)return -1;
	int numk = i - ins;  //左子树有多少个元素
	list[inroot].left = creat(ins, i - 1, pres + 1, pree + numk, n);
//注意这里的list[inroot].left中的inroot,而且numk是左子树的节点总个数,pree + numk要跳过这么多
	list[inroot].right = creat(i + 1, ine, pres + numk + 1, pree, n);//pres + numk+1加一是多一个根节点
	return inroot;
}

3.并查集的find

int nfind(int root)//并查集
{
	if (root == nfindlist[root])return root;
	else return nfind(nfindlist[root]);
}

4.tarjan

int ancestor = -1;
void tarjan(int root, int a, int b, int father)
{
	if (root == -1)return;
	tarjan(list[root].left, a, b, root);
	tarjan(list[root].right, a, b, root);
//先保证找到叶子节点,即左右都找完,重点在回溯
	if (root == a)
	{
		if (visited[b])ancestor = nfind(b);
	}
	if (root == b)
	{
		if (visited[a])ancestor = nfind(a);
	}
	visited[root] = true;
	nfindlist[root] = father;//注意这句话!!

}

例如:

一直递归到14,发现root==a,于是并查集找9的祖先,9找到祖先是自己9;

之后到9发现root==b,于是并查集找14的祖先,因为从14到9的过程中经过了10、1;

在找完14回溯的时候,nfindlist[root] = father;就把14的祖先设为10;

10回溯的时候 nfindlist[root] = father;就把10的祖先设为1;

再从1开始向右找9:

这时14已经被遍历过(visited为true),可以结束算法,所以此时ancestor 为1,就是二者的最近公共祖先。

原文地址:https://www.cnblogs.com/Jason66661010/p/12788891.html