LCA详解

19 阅读3分钟

LCA(最近公共祖先):即在有根树中,两个节点 u 和 v 的公共祖先中距离最近的那个

求解 LCA 的算法:

①:

预处理复杂度为 O( n ),查询复杂度为 O( n )
预处理:记录各个节点的深度与父亲节点
查询:如果节点 w 是 u 和 v 的共同祖先时,让 u 和 v 中较深的一方向上走,使 u 和 v 的深度相同,然后一起向上走,直到两个节点相遇时停止

代码如下(有注释):

vector<int> G[MAX_V]; //图的邻接表表示
int root; //根节点编号
int parent[MAX_V]; //记录父亲节点
int depth[MAX_V]; //记录深度
void dfs(int v, int p, int d) // v 是当前节点, p 是父亲节点, d 是当前深度
{
	parent[v] = p;
	depth[v] = d;
	for(int i = 0; i < G[v].size(); i++)
	{
		if(G[v][i] != p) dfs(G[v][i], v, d + 1);
	}
}
int lca(int u, int v)
{
	while(depth[u] > depth[v]) u = parent[u];
	while(depth[v] > depth[u]) v = parent[v];
	while(u != v)
	{
		u = parent[u];
		v = parent[v];
	}
}

②:(利用倍增二分)

核心思路:我们一个个向上找太慢了,我们可以考虑跨 1,2,4,8,16… 这种,想一下假如我们要向上走 n 步,我们按 n 的二进制行进,例如 7 ,我们就走 4 2 1,这样就以 logn 的复杂度处理了
预处理复杂度为 O( n logn ),查询复杂度为 O( logn )
预处理:记录各节点的深度与父亲节点 O( n ),然后用倍增二分预处理,dp处理一下 O( n logn ),即记录一下从各个点,向上移动 2 的 0次方,1次方,2次方…后到达的点,超过了根就是-1
即 parent [ k ] [ v ] 意思为 点v 向上移动 2 的 k 次方后到达的点,
所以 parent [ k + 1 ] [ v ] = parent [ k ] [ parent [ k ] [ v ] ];

查询:如果节点 w 是 u 和 v 的共同祖先时,让 u 和 v 中较深的一方向上走,使 u 和 v 的深度相同,然后一起向上走,直到两个节点相遇时停止,向上走的时候利用倍增二分搜索 O( logn )

注意我们找的是共同祖先的下面一个点,如果我们直接找最近的公共祖先,不管二进制从小还是从大遍历都会出错,可以自己想想很容易想出反例

代码如下:

vector<int> G[MAX_V];
int root;
int parent[MAX_LOG_V][MAX_V];
int depth[MAX_V];
void dfs(int v, int p, int d)
{
	parent[0][v] = p;
	depth[v] = d;
	for(int i = 0; i < G[v].size(); i++)
	{
		if(G[v][i] != p) dfs(G[v][i], v, d + 1);
	}
}
void init(int V)
{
	dfs(root, -1, 0);
	for(int k = 0; k + 1 < MAX_LOG_V; k++)
	{
		for(int v = 0; v < V; v++)
		{
			if(parent[k][v] < 0) parent[k+1][v] = -1; //根点上面为 -1
			else parent[k+1][v] = parent[k][parent[k][v]];
		}
	}
}
int lca(int u, int v)
{
	if(depth[u] > depth[v]) swap(u, v);
	for(int k = 0; k < MAX_LOG_V; k++)
	{
		if((depth[v] - depth[u]) >> k & 1)
		{
			v = parent[k][v];
		}
	}
	if(u == v) return u;
	for(int i = MAX_LOG_V - 1; k >= 0; k--)
	{
		if(parent[k][u] != parent[k][v])
		{
			u = parent[k][u];
			v = parent[k][v];
		}
	}
	return parent[0][u];
}

③:(利用RMQ)

核心思路:我们找 u 和 v 的最近公共祖先,其实就是找 dfs 中访问 u 之后到访问 v 之前所经过的顶点中离根最近的那个,一定范围求最值——用RMQ
预处理复杂度为 O( n ),查询复杂度为 O( logn )
预处理:按从根 dfs 访问的顺序得到 顶点序列vs[i] 和对应的深度 depth[i] ,对于每个顶点 v ,记其在 vs 中首次出现的下标为 id[v]
查询:vs[ id [ u ] <= i <= id [ v ] 中令 depth( i ) 最小的 i ]

(这个思路真的很妙)
在这里插入图片描述

代码如下:

vector<int> G[MAX_V];
int root;
int vs[MAX_V * 2 - 1];
int depth[MAX_V * 2 - 1];
int id[MAX_V];
void dfs(int v, int p, int d, int k)
{
	id[v] = k;
	vs[k] = v;
	depth[k++] = d;
	for(int i = 0; i < G[v].size(); i++)
	{
		if(G[v][i] != p)
		{
			dfs(G[v][i], v, d + 1, k);
			vs[k] = v;
			depth[k++] = d;
		}
	}
}
void init(int V)
{
	int k = 0;
	dfs(root, -1, 0, k);
	rmq_init(depth, V * 2 - 1);
}
int lca(int u, int v)
{
	return vs[query(min(id[u], id[v]), max(id[u], id[v]) + 1)]; 
}

(这篇感觉自己写的有点小水,主要是太简单了,不知道还能在哪加上自己的理解)