天天看點

最近公共祖先

最近公共祖先(\(\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)

暴力:

最近公共祖先

倍增:

最近公共祖先

用 LCA 求樹上兩點間的最短距離

還是以這張圖為例主要是我懶

最近公共祖先