最近公共祖先算法LCA筆記(樹上倍增法)


Update:
2019.7.15更新
萬分感謝[寧信]大佬,認認真真地審核了本文章,指出了超過五處錯誤捂臉,太尷尬了.
萬分感謝[寧信]大佬,認認真真地審核了本文章,指出了超過五處錯誤捂臉,太尷尬了.
萬分感謝[寧信]大佬,認認真真地審核了本文章,指出了超過五處錯誤捂臉,太尷尬了.
重要事情說三遍!!!!!

2019.7.16更新
筆記再次完善,感謝[Ichinose]大佬提出的好問題,並且修改了代碼部分的錯誤注釋.
筆記再次完善,感謝[Ichinose]大佬提出的好問題,並且修改了代碼部分的錯誤注釋.
筆記再次完善,感謝[Ichinose]大佬提出的好問題,並且修改了代碼部分的錯誤注釋.
重要事情說三遍

2019.8.7更新
感謝QQ大佬,wust-pyoxiao大佬指出問題。
感謝QQ大佬,wust-pyoxiao大佬指出問題。
感謝QQ大佬,wust-pyoxiao大佬指出問題。
重要事情說三遍!!!!!

更好的閱讀體驗,新博客開通,各位大佬收藏點贊評價好不好啊

最近公共祖先

概念定義

對於節點\(x,y\)而言,如果說\(z\)節點既是\(x\)的祖先節點,也是\(y\)的祖先節點.

那么我們認為\(z\)節點是\((x,y)\)的公共祖先節點.

公共祖先節點有很多,那么深度最大的節點,被稱之為最近公共祖先.記為LCA(x,y)

你也可以認為\(LCA(x,y)\)節點,是所有公共祖先節點中,離着\(x,y\)兩個點距離最近.

最近公共祖先.png

在這張圖片上面,我們舉幾個例子.

\[LCA(2,4)=2 \\\\ LCA(6,7)=3 \\\\ LCA(5,6)=1 \\\\ LCA(2,6)=1 \\\\ \]

樹上倍增法

初始思維

樹上倍增算法,是一個非常重要的算法,一般來說樹上的問題,很多時候都會運用到樹上倍增的算法思想.

我們知道,暴力算法,是一步,一步,一步非常踏實的算法.

但是我們知道,一步步走抵達終點太慢了,我們不得不學會連蹦帶跳.

每一次都比上一次多跳一倍的格子.

我們發現,兩個點的最近公共祖先,很多時候離他們很遠.

換一張升級版本的圖片

最近公共祖先2.png

我們發現

\[Lca(10,13)=1 \]

他們的公共祖先離他們比較遠.

我們可以分析一下.

\[dis(10,1)=3 \]

這個式子的意思是.

\[節點10和節點1,他們之間的距離是3. \]

也就是

\[dis(a,b)表示為節點a和節點b他們在樹上的距離. \]

假如說我們要用暴力方法的話.我們需要走三步,走一步要一格.

但是如果我們連蹦帶跳的話.

第一跳,我們走格.

第二跳,我們走格.

我們驚奇地我們發現,我們只需要跳兩次了.


倍增思想

既然如此的話,我們發現任意一個數字,都可以被划分成下面這個公式.

\[N=2^{p_1}+2^{p_2}+2^{p_3}+...+2^{p_k} \]

這就是我們的二進制划分的思想,任何一個數字都可以被二進制划分.

也可以這么理解,我們知道一個數有它的十進制表達,也有它的二進制表達.

我們所謂的划分,就是將一個十進制數,轉換為二進制表達.

再舉一個例子.

\[‭(3226)\_{10}=(‭110010011010‬)_{2} \]

我們可以這么認為.

\[3226=2^1+2^3+2^4+2^7+2^{10}+2^{11} \]

二進制表示下,計數位置從0開始.

之前有富有學習經驗的大佬說代碼部分.

	for(int k=lg[deep[x]]-1; k>=0; k--) //從大到小,枚舉我們所需要的長度.2^(log(deep[x]))~1
		if(fa[x][k]!=fa[y][k])//如果發現x,y節點還沒有上升到最近公共祖先節點
		{
			x=fa[x][k];//當然要跳躍
			y=fa[y][k];//當然要跳躍
		}

部分不利於初學者們理解.那么我們來認認真真地解析一下.

  1. 為什么要倒着循環,而且正着循環會出問題

我們來看一組樣例.多么的250

\[250=128+64+32+16+8+2 \\\\ 250=2+8+16+32+64+128 \]

我們將他們放到樹上.也就是節點a離着最近公共祖先有\(250\)個距離.

假如說我們是順着循環走的.我們必然走不到終點.

我們剛開始,走了\(2^0\)格,我們發現滿足條件,於是我們走了\(2^0\)個格子.

我們然后,走了\(2^1\)格,我們發現也滿足條件,\((a,b)\)節點還是沒有相遇,於是我們走了\(2^1\)個格子

我們接着,走\(2^2\)格子,我們驚奇地發現,也是滿足條件的,於是我們走了\(2^2\)個格子.

不停地走啊走,我們永遠都走不到終點.

\[1+2+4+8+16+32+64+128=255 \\\\ 250=1+2+4+8+16+32+64+64+32+16+8+2+1 \\\\ 但是如果順着循環走 \\\ 1+2+4+8+16+32+64 \quad 然后在128開始,就走不了一個格子了 \]

因為抵達終點的路徑,必須是二進制拆分下的路徑.

接下來我們分析一下為什么我們最后一遍循環完后,不是是Lca節點,而且Lca節點的兒子節點.

if(fa[x][k]!=fa[y][k]) //如果不相遇
	x=fa[x][k],y=fa[y][k];//我們才會跳躍.
那么我們實際能跳躍到的節點們,其實也就是a節點到Lca節點的兒子節點這一段上面的節點.
Lca節點,肯定fa[x][k]=fa[y][k].
但是我們fa[x][k]!=fa[y][k],所以Lca節點不能抵達.
但是Lca節點的兒子節點,肯定fa[x][k]!=fa[y][k].
所以Lca節點的兒子節點,是我們能夠跳躍的最大距離了.

倍增數組

既然如此的話,我們不妨設置

\[F[x][k]表示為x向上走(根節點)走2^k步,抵達的節點 \\\\ 若不存在該節點 F[x][k]=0 \\\\ F[x][0]表示為該節點的父節點 \\\ \]

我們知道二的冪次,是具有一個數學性質的.

\[2^k=2^{k-1}+2^{k-1} \quad 提取公因式\\\\ 2^k=2*2^{k-1} \quad 提取底數2\\\\ 2^k=2^{k} \quad 最終得到性質\\\\ \]

或者你可以這么認為.

\[2^k=2^{k} \quad 寫出恆等式 \\\\ 2^k=2^{1}*2^{k-1} \quad 指數分解一下 \\\\ 2^k=2^{k-1}+2^{k-1} \quad 乘法變成加法 \\\\ \]

我們將這個數學性質,帶入到我們的倍增數組,就會發現一個轉移方程.

\[f[x][k]=f[f[x][k-1]][k-1] \\\\ f[x][k-1]表示x向上爬2^{k-1}個節點 \\\\ 那么f[f[x][k-1]][k-1]表示為x向上爬2^{k-1}個節點,再向上爬2^{k-1}個節點 \\\\ x往上爬2^{k-1},然后再往上爬2^{k-1}個節點. \\\\ x+2^{k-1}+2^{k-1}=x+2^{k} \\\\ \]

倍增數組就這么迅速地解決了!

算法流程

我們知道LCA(x,y)表示為兩個節點的公共祖先.

也就是我們知道節點\(x\),和節點\(y\)總會在一個節點相遇.

也就是經過一系列跳躍過后的節點\(x\),和節點\(y\)深度必須是相同的.

  1. 節點x必須和節點y在同一深度
    根據這個條件,我們剛開始,顯然深度更加深的節點(在下面的節點),跳躍到和另外一個節點(在上面的節點),一樣的深度.

\[d[x]表示節點x的深度 \\\\ d[y]表示節點y的深度 \\\\ \]

我們不妨認為,節點\(x\)深度更加深,是屬於下面的節點.
如果x在上面,我們就交換x,y即可,反正要使得.

\[d[x]>=d[y] \]

  1. 利用二進制划分,使得節點\(x\)向上調整到,和節點\(y\)的同一深度.
    也就是不停地嘗試讓節點\(x\)往上走k步.

\[k=2^{log_n},..,2^1,2^0 \]

如果說我們發現,節點\(x\)往上走\(k\)步,還是在\(y\)下面.

\[x=F[x][k] \quad 還沒有抵達同一高度,我們還需要往上走 \]

  1. 如果說上調的過程中,發現\(x=y\),說明LCA找到了.
    往上面看圖片,你可以認為是節點2,和節點4的情況.節點2是節點4的父親節點.
  2. \(x,y\)節點他們的深度一致的時候,兩個節點都向上跳躍同樣高度,並且需要保證兩個節點不相遇
    為什么要跳躍同一高度?
    之前我們就說了,兩個節點必須保證同一高度.
    為什么要保證兩個節點不相遇,題目不是要我們找到最近公共祖先嗎?
    這是為了保證最近這個性質.
    我們發現滿足兩個節點不相遇的,深度最淺的兩個節點.也就是在最近公共祖先節點下面,離最近公共祖先節點,最近的節點.
    就是最近公共祖先節點的兩個兒子節點.
    那么這兩個兒子節點,他們的父親節點,就必然是最近公共祖先節點.
    怎么向上跳躍?其實和之前跳躍是一樣的.
    也就是不停地嘗試讓節點\(x,y\)往上走k步.

\[假如說F[x][k]!=F[y][k] \quad 也就是沒有相遇 \\\\ x=F[x][k] \quad 還沒有抵達同一高度,我們還需要往上走 \\\\ y=F[y][k] \quad 此時x,y節點還沒有相遇.也需要往上走 \\\\ \]

  1. 然后最后我們輸出\(F[x][0]\),也就是x節點的父節點,我們的最近公共祖先.

代碼解析

#include <bits/stdc++.h>
using namespace std;
int n,m,s,x,y,tot=0;
const int N=500005,M=1000005;//N存儲節點總數,M存儲邊的總數
int head[N],edge[M],Next[M];
int deep[N],fa[N][22],lg[N];
//deep[i]是i號節點的深度
//lg是log數組
void add(int x,int y)//鏈式前項星加邊
{
	edge[++tot]=y;//存儲節點
	Next[tot]=head[x];//鏈表
	head[x]=tot;//標記節點位置
	return ;
}
void dfs(int x,int y)
{
	deep[x]=deep[y]+1;//x是y的兒子節點,所以要+1
	fa[x][0]=y;//fa[x][0]表示x的父親節點,而y是x的父親節點.
	for(int i=1; (1<<i)<=deep[x]; i++) //2^i<=deep[x]表示不能跳出去了,最多跳到根節點上面
		fa[x][i]=fa[fa[x][i-1]][i-1];//狀態轉移 2^i=2^(i-1)+2^(i-1)
	for(int i=head[x]; i; i=Next[i]) //遍歷所有的出邊
		if(edge[i]!=y)//因為是無向圖,所以要避免回到父親節點上面去了
			dfs(edge[i],x);//訪問兒子節點,並且標記自己是父親節點
	return ;//返回
}
int LCA(int x,int y)
{
	if(deep[x]<deep[y])//強制要求x節點是在下方的節點
		swap(x,y);//交換,維持性質
	while(deep[x]>deep[y])//當我們還沒有使得節點同樣深度
		x=fa[x][lg[deep[x]-deep[y]]-1];//往上面跳躍,deep[x]-deep[y]是高度差.-1是因為lg數組是Log值大1
	if(x==y)//發現Lca(x,y)=y
		return x;//返回吧,找到了...
	for(int k=lg[deep[x]]-1; k>=0; k--) //從大到小,枚舉我們所需要的長度.2^(log(deep[x]))~1
		if(fa[x][k]!=fa[y][k])//如果發現x,y節點還沒有上升到最近公共祖先節點
		{
			x=fa[x][k];//當然要跳躍
			y=fa[y][k];//當然要跳躍
		}
	return fa[x][0];//必須返回x的父親節點,也就是Lca(x,y)
}
int main()
{
	scanf("%d%d%d",&n,&m,&s);//n個節點,m次詢問,s為根節點
	for(int i=1; i<n; i++) //n-1條邊
	{
		scanf("%d%d",&x,&y);//讀入邊
		add(x,y);//建立邊
		add(y,x);//建立無向圖
	}
	dfs(s,0);//從根節點,開始建立節點之間的跳躍關系,根節點的父親節點沒有,故選擇0
	for(int i=1; i<=n; i++)
		lg[i]=lg[i-1]+(1<<lg[i-1]==i);//處理log數組的關系,lg[x]=log(x)+1,請記得最后使用要-1
	for(int i=1; i<=m; i++)
	{
		scanf("%d%d",&x,&y);//讀入需要查詢的節點
		printf("%d\n",LCA(x,y));//輸出查詢的結果
	}
	return 0;
}

例題

題目描述

原題連接

Y島風景美麗宜人,氣候溫和,物產豐富。

Y島上有N個城市(編號\(1,2,…,N\)),有\(N-1\)條城市間的道路連接着它們。

每一條道路都連接某兩個城市。

幸運的是,小可可通過這些道路可以走遍Y島的所有城市。

神奇的是,乘車經過每條道路所需要的費用都是一樣的。

小可可,小卡卡和小YY經常想聚會,每次聚會,他們都會選擇一個城市,使得3個人到達這個城市的總費用最小。

由於他們計划中還會有很多次聚會,每次都選擇一個地點是很煩人的事情,所以他們決定把這件事情交給你來完成。

他們會提供給你地圖以及若干次聚會前他們所處的位置,希望你為他們的每一次聚會選擇一個合適的地點。

輸入格式

第一行兩個正整數,\(N\)\(M\),分別表示城市個數和聚會次數。

后面有\(N-1\)行,每行用兩個正整數\(A\)\(B\)表示編號為\(A\)和編號為\(B\)的城市之間有一條路。

再后面有\(M\)行,每行用三個正整數表示一次聚會的情況:小可可所在的城市編號,小卡卡所在的城市編號以及小YY所在的城市編號。

輸出格式

一共有\(M\)行,每行兩個數\(Pos\)\(Cost\),用一個空格隔開,表示第\(i\)次聚會的地點選擇在編號為\(Pos\)的城市,總共的費用是經過\(Cost\)條道路所花費的費用。

數據范圍

\[N \le 500000 \\\\ M \le 500000 \\\\ \]

輸入樣例:

6 4
1 2
2 3
2 4
4 5
5 6
4 5 6
6 3 1
2 4 4
6 6 6

輸出樣例:

5 2
2 5
4 1
6 0

解題報告

題意理解

不同於一般的LCA題目,這道題目是,在一棵\(n-1\)條邊的樹上,有三個節點,要你求出這個三個點抵達一個匯聚點最少代價.


算法解析

這道題目的核心點,就是它是由三個點構成的最短路.

為什么,它同於一般的題目,難道不是讓我們直接求出三個點的最近公共祖先?

匯聚點為什么不是

\[Lca(Lca(a,b),Lca(a,c)) \\\\ 或者 \\\\ Lca(Lca(a,c),Lca(b,c)) \\\\ 以上選項二選一 \]

如果你真的是這么想,腦海里面只有A,B選項,那么你應該慶幸,出題人比較良心喪心病狂留下的唯一良知,他給你提出了一個樣例,告訴你為什么不是這樣.

因為文化課考試的時候,題目都是A,B,C或者再來一個D的單項選擇題.

聚會1.png

\(3\)人分別在\(4,5,6\)三個節點上面.

仔仔細細地觀察一下,我們發現這道題目的匯聚點,應該是5,而不是4.

  1. 假如說我們按照樓上這個錯誤思路,我們的三點的最近公共祖先節點,應該是4.
  2. 但是最少花費,顯然是在\(5\)號節點.

我們的思路居然是錯誤的!!!

它到底錯誤在了哪里.

我們要分析一下,這道題目,為什么選擇的是5,而不是4?

選擇\(4\),那么\(1\)號小朋友不需要行動.

選擇\(5\),那么\(2,3\)號小朋友匯聚在點\(5\)后,不需要行動.


我們可以這么現實化這道題目.

\(2,3\)號小朋友他們是互相的知己一對狗男女,所以說,他們想要在一起.發朋友圈,秀恩愛

所以\(2,3\)號小朋友他們會先聚集在一起

花費代價為

\[消耗距離=deep[b]+deep[c]-2 \times deep[Lca(b,c)] \]

聚會.png

此時我們面臨兩大選擇.

  1. \(1\)號同學孤身一人走到2,3號同學相遇的地方.
  2. \(2,3\)號同學一起手拉手\(1\)號同學相遇.再秀一次恩愛,虐一下單身狗1號

假如說\(1\)號同學,與\(2,3\)號同學相隔\(L\)個距離.

我們將會發現,兩大選擇,會產生兩大代價.

然而顯然一號選擇是最好不過的了.
同樣的距離,一個人是一個人走,一個是兩個人走,那么必然一個人走消耗卡路里更少.

那么2,3號匯聚的話,他們會花費

\[消耗=deep[y]+deep[z]-2 \times deep[Lca(y,z)] \]

之后一號過來找他們,距離消耗了:

\[deep[x]-deep[lca(x,lca(y,z))] \]

這時候一號到了\((x,y,z)\)公共祖先,然后一號朝\(Lca(y,z)\)

\[deep[Lca(y,z)]-deep[Lca(x,Lca(y,z))] \]

那么消耗總值就是

\[消耗總值=deep[y]+deep[z]-deep[Lca(y,z)]+deep[x]-2*deep[lca(x,Lca(y,z))] \]

  1. \(1,2\)先在一起
  2. \(2,3\)先在一起
  3. \(1,3\)先在一起

代碼解析

#include <bits/stdc++.h>
using namespace std;
const int N=500000+200,M=500000*2+100;
int n,m,s,lg[N],deep[N];
struct Lca
{
	int head[M],Next[M],edge[M],tot,fa[N][22];
	void init()
	{
		memset(head,0,sizeof(head));
		tot=0;
	}
	void add_edge(int a,int b)
	{
		edge[++tot]=b;
		Next[tot]=head[a];
		head[a]=tot;
		return ;
	}
	void dfs(int x,int y)
	{
		deep[x]=deep[y]+1;
		fa[x][0]=y;
		for(int i=1; (1<<i)<=deep[x]; i++)
			fa[x][i]=fa[fa[x][i-1]][i-1];
		for(int i=head[x]; i; i=Next[i])
			if (edge[i]!=y)
				dfs(edge[i],x);
		return ;
	}
	int LCA(int x,int y)
	{
		if (deep[x]<deep[y])
			swap(x,y);
		while(deep[x]>deep[y])
			x=fa[x][lg[deep[x]-deep[y]]-1];
		if (x==y)
			return x;
		for(int k=lg[deep[x]]-1; k>=0; k--)
			if (fa[x][k]!=fa[y][k])
			{
				x=fa[x][k];
				y=fa[y][k];
			}
		return fa[x][0];
	}
} g1;
int main()
{
	scanf("%d%d",&n,&m);
	g1.init();
	for(int i=1; i<n; i++)
	{
		int a,b;
		scanf("%d%d",&a,&b);
		g1.add_edge(a,b);
		g1.add_edge(b,a);
	}
	g1.dfs(1,0);
	for(int i=1; i<=n; i++)
		lg[i]=lg[i-1]+(1<<lg[i-1]==i);
	for(int i=1; i<=m; i++)
	{
		int x,y,z,c_x,c_y,c_z,dx,dy,dz;
		scanf("%d%d%d",&x,&y,&z);
		c_x=g1.LCA(x,y),dx=deep[x]+deep[y]-deep[c_x]+deep[z]-2*deep[g1.LCA(z,c_x)];
		c_y=g1.LCA(y,z),dy=deep[y]+deep[z]-deep[c_y]+deep[x]-2*deep[g1.LCA(x,c_y)];
		c_z=g1.LCA(x,z),dz=deep[x]+deep[z]-deep[c_z]+deep[y]-2*deep[g1.LCA(y,c_z)];
		if(dx>dy) 
		    dx=dy,c_x=c_y;
		if(dx>dz) 
		    dx=dz,c_x=c_z;
		printf("%d %d\n",c_x,dx);
	}
	return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM