最近公共祖先(\(\rm Least\,Common\,Ancestors\)),简记为 \(\rm LCA\)。顾名思义就是一棵树中的某两个节点的公共的祖先中离他们最近,即深度最大的那个

举个例子:最近公共祖先_数据

上图中 \(8\)\(6\) 的 LCA 就是 \(1\)

那么怎么求 LCA 呢?

1. 向上标记法

思路十分简单。

我们现在要求 \(\operatorname{LCA(x,y)}\)。首先从 \(x\) 向上到根节点 \(rt\),经过的每一个节点都打上标记。然后从 \(y\) 向上到 \(rt\),遇到的第一个有标记的节点就是 \(LCA\)

代码实现很简单,就不给了。

2. 同步前进法

\(\operatorname{LCA(x,y)}\) 的具体方法为:

  1. 先让 \(x\) 的深度大于或等于 \(y\)。这相当于数学上的一个假设:\(dep(x)\ge dep(y)\)\(dep\) 代表深度)
  2. 不停地将 \(x\) 往上跳,直到 \(x\)\(y\) 深度相等。
  3. 特判:若 \(x=y\)\(LCA\) 就是 \(x\) 了。
  4. 否则在保证 \(fa(x)\ne fa(y)\)\(fa\) 代表父亲) 的情况下 \(x\)\(y\) 同时向上跳一格。
  5. \(LCA\) 就是 \(x\) 的父亲。

以上面的 \(8\)\(6\) 为例:

  1. \(dep(8)>dep(6)\),无需交换。
  2. \(8\) 跳到 \(4\)\(dep(4)=dep(6)\)
  3. \(4\ne6\)
  4. \(fa(4)=2\ne fa(6)=3\)\(4\) 跳到 \(2\)\(6\) 跳到 \(3\)\(fa(2)=1=fa(3)\),停止。
  5. \(\operatorname{LCA(8,6)}=fa(2)=1\)

在此之前还需 dfs 一遍求出 \(dep\)\(fa\)

void dfs(int u, int father)
{
	fa[u] = father;
	dep[u] = dep[father] + 1; //u深度即father深度加1
	for (int i = head[u]; i; i = e[i].nxt)
	{
		int v = e[i].to;
		if (v != father)
		{
			dfs(v, u);
		}
	}
}

int lca(int x, int y)
{
	if (dep[x] < dep[y])
	{
		swap(x, y); //让x的深度大于或等于y
	}
	while (dep[x] > dep[y])
	{
		x = fa[x]; //向上跳
	}
	if (x == y)
	{
	  	return x; //特判
	}
	while (fa[x] != fa[y])
	{
		x = fa[x]; //同时跳
		y = fa[y];
	}
	return fa[x]; //LCA是父亲
}

但以上两种方法时间复杂度均为 \(\operatorname{O}(nm)\)……

然后洛谷 A 了。

对于 \(100\%\) 的数据,\(N\le500000\)\(M\le500000\)

数据太水了啊啊啊!!!

出数据的真良心。。。

3. 倍增

上面暴力算法显然过慢,所以我们要使用倍增算法,也是一种空间换时间的策略。

先预处理出 \(lg\) 数组,用来保存 \(\left\lfloor log_2x\right\rfloor\)

for (int i = 2; i <= n; i++)
{
	lg[i] = lg[i >> 1] + 1;
}

\(fa(x)(i)\)\(x\) 的第 \(2^i\) 级祖先,dfs 时要算 \(fa(u)(i)\)

LCA 的第 \(2\) 步,\(x\) 直接向上跳 \(lg(dep(x)-dep(y))\) 格。第 \(4\) 步时,我们遍历 \(i=lg(dep(x))+1\sim0\),每次 \(x\)\(y\) 同时向上跳 \(2^i\) 格。

void dfs(int u, int father)
{
	fa[u][0] = father; //u的第1级祖先就是父亲
	dep[u] = dep[father] + 1;
	for (int i = 1; i <= lg[dep[u]] + 1; i++)
	{
		fa[u][i] = fa[fa[u][i - 1]][i - 1];
	} //算fa(u)(i)
	for (int i = head[u]; i; i = e[i].nxt)
	{
		int v = e[i].to;
		if (v != father)
		{
			dfs(v, u);
		}
	}
}

int lca(int x, int y)
{
	if (dep[x] < dep[y])
	{
		swap(x, y);
	}
	while (dep[x] > dep[y])
	{
		x = fa[x][lg[dep[x] - dep[y]]];
	}
	if (x == y)
	{
	  	return x;
	}
	for (int i = lg[dep[x]] + 1; i >= 0; i--)
	{
		if (fa[x][i] != fa[y][i])
		{
			x = fa[x][i];
			y = fa[y][i];
		}
	}
	return fa[x][0];
}

复杂度分析:

时间 空间
暴力 预处理 \(\operatorname{O}(n)\),每次询问 \(\operatorname{O}(n)\) \(\operatorname{O}(n)\)
倍增 预处理 \(\operatorname{O}(n\log n)\),每次询问 \(\operatorname{O}(\log n)\) \(\operatorname{O}(n\log n)\)

P3379 【模板】最近公共祖先(LCA)

暴力:最近公共祖先_i++_02

倍增:最近公共祖先_最短距离_03

用 LCA 求树上两点间的最短距离

还是以这张图为例主要是我懒 最近公共祖先_数据

\(8\)\(5\)

预处理出每个点到根节点的距离 \(dis(x)\),这里因为是无权边,\(dis(x)\) 直接就是 \(dep(x)\) 了。

先用 \(dis(8)\) 加上 \(dis(5)\),此时多加了 \(2\)\(dis(2)\),故 \(8\)\(5\) 的距离就应该是 \(dis(8)+dis(5)-2\times dis(2)\),即树上 \(x\)\(y\) 两点间的最短距离为 \(dis(x)+dis(y)-2\times dis(LCA)\)