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\)這兩個點的距離最近.
在這張圖片上面,我們舉幾個例子.
樹上倍增法
初始思維
樹上倍增算法,是一個非常重要的算法,一般來說樹上的問題,很多時候都會運用到樹上倍增的算法思想.
我們知道,暴力算法,是一步,一步,一步非常踏實的算法.
但是我們知道,一步步走抵達終點太慢了,我們不得不學會連蹦帶跳.
每一次都比上一次多跳一倍的格子.
我們發現,兩個點的最近公共祖先,很多時候離他們很遠.
換一張升級版本的圖片
我們發現
他們的公共祖先離他們比較遠.
我們可以分析一下.
這個式子的意思是.
也就是
假如說我們要用暴力方法的話.我們需要走三步,走一步要一格.
但是如果我們連蹦帶跳的話.
第一跳,我們走一格.
第二跳,我們走兩格.
我們驚奇地我們發現,我們只需要跳兩次了.
倍增思想
既然如此的話,我們發現任意一個數字,都可以被划分成下面這個公式.
這就是我們的二進制划分的思想,任何一個數字都可以被二進制划分.
也可以這么理解,我們知道一個數有它的十進制表達,也有它的二進制表達.
我們所謂的划分,就是將一個十進制數,轉換為二進制表達.
再舉一個例子.
我們可以這么認為.
二進制表示下,計數位置從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];//當然要跳躍
}
部分不利於初學者們理解.那么我們來認認真真地解析一下.
- 為什么要倒着循環,而且正着循環會出問題
我們來看一組樣例.多么的250
我們將他們放到樹上.也就是節點a離着最近公共祖先有\(250\)個距離.
假如說我們是順着循環走的.我們必然走不到終點.
我們剛開始,走了\(2^0\)格,我們發現滿足條件,於是我們走了\(2^0\)個格子.
我們然后,走了\(2^1\)格,我們發現也滿足條件,\((a,b)\)節點還是沒有相遇,於是我們走了\(2^1\)個格子
我們接着,走\(2^2\)格子,我們驚奇地發現,也是滿足條件的,於是我們走了\(2^2\)個格子.
不停地走啊走,我們永遠都走不到終點.
因為抵達終點的路徑,必須是二進制拆分下的路徑.
接下來我們分析一下為什么我們最后一遍循環完后,不是是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節點的兒子節點,是我們能夠跳躍的最大距離了.
倍增數組
既然如此的話,我們不妨設置
我們知道二的冪次,是具有一個數學性質的.
或者你可以這么認為.
我們將這個數學性質,帶入到我們的倍增數組,就會發現一個轉移方程.
倍增數組就這么迅速地解決了!
算法流程
我們知道LCA(x,y)表示為兩個節點的公共祖先.
也就是我們知道節點\(x\),和節點\(y\)總會在一個節點相遇.
也就是經過一系列跳躍過后的節點\(x\),和節點\(y\)的深度必須是相同的.
- 節點x必須和節點y在同一深度
根據這個條件,我們剛開始,顯然深度更加深的節點(在下面的節點),跳躍到和另外一個節點(在上面的節點),一樣的深度.
我們不妨認為,節點\(x\)深度更加深,是屬於下面的節點.
如果x在上面,我們就交換x,y即可,反正要使得.
- 利用二進制划分,使得節點\(x\)向上調整到,和節點\(y\)的同一深度.
也就是不停地嘗試讓節點\(x\)往上走k步.
如果說我們發現,節點\(x\)往上走\(k\)步,還是在\(y\)下面.
- 如果說上調的過程中,發現\(x=y\),說明LCA找到了.
往上面看圖片,你可以認為是節點2,和節點4的情況.節點2是節點4的父親節點. - 當\(x,y\)節點他們的深度一致的時候,兩個節點都向上跳躍同樣高度,並且需要保證兩個節點不相遇
為什么要跳躍同一高度?
之前我們就說了,兩個節點必須保證同一高度.
為什么要保證兩個節點不相遇,題目不是要我們找到最近公共祖先嗎?
這是為了保證最近這個性質.
我們發現滿足兩個節點不相遇的,深度最淺的兩個節點.也就是在最近公共祖先節點下面,離最近公共祖先節點,最近的節點.
就是最近公共祖先節點的兩個兒子節點.
那么這兩個兒子節點,他們的父親節點,就必然是最近公共祖先節點.
怎么向上跳躍?其實和之前跳躍是一樣的.
也就是不停地嘗試讓節點\(x,y\)往上走k步.
- 然后最后我們輸出\(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\)條道路所花費的費用。
數據范圍
輸入樣例:
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\)條邊的樹上,有三個節點,要你求出這個三個點抵達一個匯聚點的最少代價.
算法解析
這道題目的核心點,就是它是由三個點構成的最短路.
為什么,它同於一般的題目,難道不是讓我們直接求出三個點的最近公共祖先?
匯聚點為什么不是
如果你真的是這么想,腦海里面只有A,B選項,那么你應該慶幸,出題人比較良心喪心病狂留下的唯一良知,他給你提出了一個樣例,告訴你為什么不是這樣.
因為文化課考試的時候,題目都是A,B,C或者再來一個D的單項選擇題.
\(3\)人分別在\(4,5,6\)三個節點上面.
仔仔細細地觀察一下,我們發現這道題目的匯聚點,應該是5,而不是4.
- 假如說我們按照樓上這個錯誤思路,我們的三點的最近公共祖先節點,應該是4.
- 但是最少花費,顯然是在\(5\)號節點.
我們的思路居然是錯誤的!!!
它到底錯誤在了哪里.
我們要分析一下,這道題目,為什么選擇的是5,而不是4?
選擇\(4\),那么\(1\)號小朋友不需要行動.
選擇\(5\),那么\(2,3\)號小朋友匯聚在點\(5\)后,都不需要行動.
我們可以這么現實化這道題目.
\(2,3\)號小朋友他們是互相的知己一對狗男女,所以說,他們想要先在一起.發朋友圈,秀恩愛
所以\(2,3\)號小朋友他們會先聚集在一起
花費代價為
此時我們面臨兩大選擇.
- \(1\)號同學孤身一人走到2,3號同學相遇的地方.
- \(2,3\)號同學一起
手拉手和\(1\)號同學相遇.再秀一次恩愛,虐一下單身狗1號
假如說\(1\)號同學,與\(2,3\)號同學相隔\(L\)個距離.
我們將會發現,兩大選擇,會產生兩大代價.
然而顯然一號選擇是最好不過的了.
同樣的距離,一個人是一個人走,一個是兩個人走,那么必然一個人走消耗卡路里更少.
那么2,3號匯聚的話,他們會花費
之后一號過來找他們,距離消耗了:
這時候一號到了\((x,y,z)\)公共祖先,然后一號朝\(Lca(y,z)\)走
那么消耗總值就是
- \(1,2\)先在一起
- \(2,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;
}