LCA最近公共祖先的离线算法(Tarjan)和在线算法(ST)

最近公共祖先简称LCA(Lowest Common Ancestors),所谓LCA,是当给定一个有根树T时,对于任意两个结点u、v,找到一个离根最远的结点x,使得x同时是u和v的祖先,x 便是u、v的最近公共祖先。

首先说一下小数据时的暴力求解:

      简单粗暴的方法当然是你先将一个人的祖先全都标记出来,然后顺着另一个的父亲一直向上找,直到找到第一个被标记过的结点,便是它们的最近公共祖先结点了。


一、离线的tarjan算法:

tarjan算法是有名的利用dfs在图中寻找强连通分量的算法。算法的基本思想为:任选一节点开始进行深度优先搜索dfs(若dfs结束后人有未访问的结点,则再从中任选一点再次进行)。搜索过程已访问的结点不再访问。搜索树的若干子树构成了图的强连通分量。


应用到我们要解决的LCA问题上,对于新搜索岛的一个结点u,先创建由u构成的集合,在对u的每颗子树进行搜索,每次搜完一颗子树,就把子树合并到u的集合里,这时候子树中所有的结点的最近公共组先就是u了。


还不懂的话可以去hihocoder看一下 最近公共祖先第二讲!!!

贴一下代码


#include <iostream>
#include <vector>
#define N 10005
using namespace std;

int father[N], lca[N][N];
vector<int>edge[N];
int n;  // n 表示结点的数量

//给树加边
void add_edge(int u, int v){
	edge[u].push_back(v);
}

void make_set(int x){
	father[x] = x;
}

int find_set(int x)
{
	if (x == father[x])return father[x];
	return father[x] = find_set(father[x]);
}

void union_set(int x, int y){
	x = find_set(x), y = find_set(y);
	//一定要注意调用union_set时谁是代表元素,这里第一个参数 x 是代表元素
	father[y] = x;
}

//tarjan算法
void dfs(int u){
	//给搜索到的点建立一个集合
	make_set(u);

	//遍历所有节点 u 的儿子结点
	for (int i = 0; i < edge[u].size(); i++){
		//初始时father数组全是 -1 表示未被遍历过,遍历之后便记录该点所在集合的代表元素值
		int v = edge[u][i];
		if (father[v] == -1){
			dfs(v);
			//注意这里与调用union_set(v,u);的区别
			union_set(u, v);
		}
	}

	for (int i = 1; i <= n; i++){
		if (father[i] != -1){
			lca[u][i] = lca[i][u] = find_set(i);
		}
	}
}



二、在线的ST算法:

ST算法是用在RMQ问题上的经典算法。这里把树预处理成一个数组就可以像RMQ问题那样套用RT算法了。


第一个问题: 怎么把树预处理成一个数组?

    从树的根节点开始进行深度优先搜索,每次经过某一个点——无论是从它的父亲节点进入这个点,还是从它的儿子节点返回这个点,都按顺序记录下来。这样,就把一棵树转换成了一个数组。

第二个问题: 这个数组跟LCA有什么关系。

    找到树上两个节点的最近公共祖先,无非就是找到这两个节点最后一次出现在数组中的位置所囊括的一段区间中深度最小的那个点。那么问题就来了,为什么是两个节点最后一次出现呢??这是因为最后一次出现的时候便是这个离开这个点的时候。从第一个点离开(返回它的父亲节点),到从第二个点离开(返回它的父亲节点)的这一段路程,一定会经过‘最近公共祖先’这一个点!而这个点就是深度最小的点。


举个例子很容易理解。


LCA最近公共祖先的离线算法(Tarjan)和在线算法(ST)_第1张图片


对于上面的一棵树。我们会遍历得到一个node数组

index 0 1 2 3 10 11 12 13 14 15 16 17 18 19 20 21 22 23
node   1 2 1 3 4 6 4 7 4 3 5 8 9 8 5 3 1            
deep   0 1 0 1 2 3 2 3 2 1 2 3 4 3 2 1 0            
last   17 16 9 15 6 8 14 13                            

其中node数组记录的是点的编号,deep记录的是node中相对应点的深度,last记录的是该点在node数组中出现的最后位置

例如我们要求结点6,9的LCA,,我们从last中知道6,9在node中出现的最后位置为6,13 。然后我们在deep中6和13之间找到最小的值是1对应node里面的3。所以6,9的LCA是3。

下面贴下代码


#include <iostream>
#include <algorithm>
#include <vector>
#define N 100010
using namespace std;

vector<int>edge[N];
int last[N], node[2 * N], deep[2 * N];
int dp[2 * N][20];
int tol = 0;

void add(int u, int v){
	edge[u].push_back(v);
}

//dfs把树预处理成node数组
void dfs(int s, int dep){
	node[++tol] = s;
	deep[tol] = dep;
	last[s] = tol;

	for (int i = 0; i < (int)edge[s].size(); i++){
		dfs(edge[s][i], dep + 1);
		//每次dfs完一颗子树就要返回父亲结点,,再开始dfs下一颗子树

		//返回父亲结点
		node[++tol] = s;
		deep[tol] = dep;
		last[s] = tol;
	}
}

//RMQ
void RMQ(){

	for (int i = 1; i <= tol; i++)dp[i][0] = i;

	for (int j = 1; j < 20; j++){
		for (int i = 1; i <= tol; i++){
			int tmp = i + (1 << (j - 1));
			if (tmp <= tol){
				if (deep[dp[i][j - 1]] < deep[dp[tmp][j - 1]]){
					dp[i][j] = dp[i][j - 1];
				}
				else dp[i][j] = dp[tmp][j - 1];
			}
		}
	}
}

//返回[l, r]之间最小值的位置
int queryMinPos(int l, int r){
	int  base = 0;
	while ((1 << (base + 1)) < (r - l + 1)) base++;
	if (deep[dp[l][base]] < deep[dp[r + 1 - (1 << base)][base]])
		return dp[l][base];
	return dp[r + 1 - (1 << base)][base];
}

int LCA(int x, int y){
	x = last[x], y = last[y];
	//注意这里的 x 和 y 的大小是不能确定的
	return queryMinPos(min(x, y), max(x, y));
}

你可能感兴趣的:(LCA最近公共祖先的离线算法(Tarjan)和在线算法(ST))