關於LCA演算法-暴力&倍增&tarjan&樹連刨分&RMQ
LCA是 最近公共祖先(latest common ancestor),字面意思,就比如我和我叔叔家兒子的最近公共祖先就是我爺爺。我和我爸的公共祖先就是我爸
LCA大概5種寫法
1 暴力搜尋
2 樹上倍增
3 tarjan+並查集(離散操作,適合大量的query)
4 樹連刨分(這個俺不太會)
5 RMQ+尤拉序(和dfs序是不一樣的東東,可能有大佬也將其稱之為時間戳)
第3,4種等都不太明白。 先寫幾種簡單的。
用的是 POJ1330來試的程式碼
https://vjudge.net/problem/POJ-1330
關於尤拉序,dfs序,最好的講解
https://www.cnblogs.com/stxy-ferryman/p/7741970.html
這個大佬講的也不錯啊
https://www.cnblogs.com/JVxie/p/4854719.html
1 暴力版本。暴力版本啊,我感覺就和 兩個連結串列求交點一樣。只不過一個是線性表一個是樹,都是先移至同一層,然後guangguang一起往後guangguang
(不移到同一層在guangguang兩者就不能對在一起,對不在一起你怎麼找公共祖先)
(如圖,綠色先移到紫色(和紅色點同層哦),然後一起guangguang。ok)
#include <iostream>
#include<cstdio>
#include <vector>
#include <cstring>
/* 求LCA的
這個方法比較簡單,
*/
using namespace std;
const int maxn=1e5+5;
int height[maxn];
int fa[maxn];
int du[maxn];
vector<int>g[maxn];
void dfs(int u,int fat,int depth){
height[u]=depth;
fa[u]=fat;
for(int i=0;i<g[u].size();i++){
int to=g[u][i];
if(to==fat)continue ;
dfs(to,u,depth+1);
}//經過這樣的搜尋,我們就實現了從節點退到樹根的操作。
}
/*這個query就很直白了。我們還會驚奇的發現
這個和求 兩個連結串列的交點幾乎差不多。不同的是這裡記錄了層數。所以先讓他們層數一至。
再一直向後跑。這個方法真是very,very的nice。寫道這裡。
*/
int query(int u,int v){
int lens1=height[u];
int lens2=height[v];
while(lens1<lens2)lens2--,v=fa[v];
while(lens1>lens2)lens1--,u=fa[u];
while(u!=v){
u=fa[u],v=fa[v];
}
return u;
}
int main()
{ int T,m,a,b;
scanf("%d",&T);
while(T--){
scanf("%d",&m);
for(int i=0;i<maxn;i++)g[i].clear();
memset(du,0,sizeof(du));
for(int i=0;i<m-1;i++){
scanf("%d%d",&a,&b);
g[a].push_back(b);
du[b]++;
}
for(int i=1;i<=m;i++){
if(!du[i]) {dfs(i,-1,1);break;}
}
scanf("%d%d",&a,&b);
printf("%d\n",query(a,b));
}
return 0;
}
這複雜度超級高,O((m+q)*m)m是點,q是查詢次數。
2 樹上倍增
我以前一直以為樹上倍增就是拿ST表搞,ST表不也算倍增嘛。哪知道他連ST表都不如。。
還是上圖,就是加速 兩個點移動的速度,加一個log。複雜度O((m+q)*log(m))//你猜到了吧那個暴力版本的複雜度我是根據這個推的qwq
#include <iostream>
//#include <bits/stdc++.h>
#include <vector>
#include <cstring>
#include <cstdio>
using namespace std;
/* 上面那個演算法太慢了,一點也不符合
合習近平新時代社會主義思想。
我們可以發現一個有趣的問題
如果x和y的差距是x(x是可以拆分為二進位制的)
你這不是廢話麼。
所以我們可以用二進位制來優化。
這樣就logn了。
具體看碼ba
*/
const int maxn=1e5+4;
vector<int>g[maxn];
int height[maxn];
int du[maxn];
int fa[maxn][20];//2的20次方絕對夠了
void dfs(int u,int fat,int depth){
fa[u][0]=fat;
height[u]=depth;
//for(int i=1;i<=19;i++)
//fa[u][i]=fa[fat][i-1];
for (int i=1;i<20;i++)
fa[u][i]=fa[fa[u][i-1]][i-1];
for(int i=0;i<g[u].size();i++){
if(g[u][i]==fat)continue;
int to=g[u][i];
dfs(to,u,depth+1);
}
}
int LCA(int x,int y){
if(height[x]<height[y])swap(x,y);
for(int i=19;i>=0;i--){
if(height[x]-(1<<i)>=height[y]){
x=fa[x][i];
}
}
if(x==y)return x;//
for(int i=19;i>=0;i--){
if(fa[x][i]!=fa[y][i]){
x=fa[x][i];
y=fa[y][i];
}
}
return fa[x][0];
}
int main()
{ int T,m,a,b;
scanf("%d",&T);
while(T--){
scanf("%d",&m);
for(int i=0;i<maxn;i++)g[i].clear();
memset(du,0,sizeof(du));
memset(fa,0,sizeof(fa));
for(int i=0;i<m-1;i++){
scanf("%d%d",&a,&b);
g[a].push_back(b);
du[b]++;
}
for(int i=1;i<=m;i++){
if(!du[i])
{dfs(i,-1,1);
break;}
}
scanf("%d%d",&a,&b);
/*for(int i=1;i<=m;i++){
cout<<height[i]<<endl;
}*/
printf("%d\n",LCA(a,b));
}
return 0;
}
RMQ+尤拉序
類比dfs序,在處理子樹的時候,如果我們過於老實,guangguang暴力,複雜度太高,可以用dfs序再打個時間戳。然後把樹之間的鄰接關係拍平然後用樹狀陣列或者線段樹處理,嘎嘎好用。
所以一開始我看到RMQ處理lca時就感到大概也可以這麼做,但是具體怎麼做,幼小的我並沒有什麼具體思路。。
想了想後發現用dfs序和時間戳並不能解決lca。因為dfs序是處理子樹,而lca問題要求尋找子樹(事到如今你應該發現LCA就是包含兩點的一顆最小的子樹的根了吧,),這個尋找就很迷,我總不能一個點一個點都來試吧。
RMQ演算法的思路是 用尤拉序來刻畫一個樹(尤拉序可以描述dfs時一個點到另一個點的簡單路!lca一定在簡單路里!,而dfs序並不可以,dfs序只可以保證找到子樹),並且lca肯定是 高度最低的(他肯定是個最低的。不可能有比他還低的或者和他一樣低的),我們就可以用用RMQ來查詢極值了!很nice
如圖,4和5之間lca(尤拉序中一個點會出現多次!並不只是訪問會出現,他還刻畫移動的路徑。我們確定的是第一次出現的時間點,因為這是訪問的時間點。)
事實上 4 3 1 5 正是4 到5 的簡單路。 找到最低的度數2,然後再尤拉序中找到對應點為3號點。ok
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <vector>
#include <cmath>
#include <cstring>
using namespace std;
/* 以前大概瞭解過一些rmq問題。
所以以前大概看了一下lca,就感覺用RMQ搞個表也能做。很直觀的感覺
但是具體如何實現卻沒有思考很多。
包括以前的dfs序的問題,都是搞一搞A了就不看了。。
但是因為是自己程式碼功夫不夠,所以有些線段樹的操作都是粘的,寫的很不好。
我開始想用dfs序+時間戳來解決。但是後來發現不可以。
因為 dfs序+時間戳是對子樹處理很方便(可以理解為把子樹拍扁用 樹狀陣列維護)
但是 在其結果中尋找特定子樹 確實很麻煩。
所以 使用了尤拉序,
尤拉序就是一條線,把dfs的順序給寫出來。
然後我們發現 a和b點的 LCA一定在這倆點出現的LCA上。
然後把尤拉序的結果拍平了 給ST維護。
*/
const int maxn=1e4+4;
int euler[maxn*3];
vector<int>g[maxn];
int dp[maxn*3][20];
int pos[maxn*3][20];
int siz;
int height[maxn];
bool vis[maxn];
int fir[maxn];
int du[maxn];
void dfs(int u,int length){
euler[siz]=u;
height[u]=length;
if(!vis[u]){
vis[u]=true,fir[u]=siz;
}
siz++;
for(int i=0;i<g[u].size();i++){
int to=g[u][i];
if(vis[to])continue;
dfs(to,length+1);
euler[siz]=u,siz++;
}
return;
}
void RMQ_ST(){
//相當於在 尤拉序列中求 min高度,並且維護編號。
for(int i=1;i<siz;i++){
dp[i][0]=height[euler[i]];
pos[i][0]=euler[i];
}
for(int j=1; j<=17; j++)
{
for(int i=1; i<siz; i++)
{
if(i+(1<<(j-1))>=siz)
{
break;
}
if(dp[i][j-1]>dp[i+(1<<(j-1))][j-1])
{
dp[i][j]=dp[i+(1<<(j-1))][j-1];
pos[i][j]=pos[i+(1<<(j-1))][j-1];
}
else
{
dp[i][j]=dp[i][j-1];
pos[i][j]=pos[i][j-1];
}
}
}
}
int LCA(int x,int y){
if(fir[x]>fir[y])swap(x,y);
int dx=fir[x];
int dy=fir[y];
int k=(int)(log((double)(dy-dx+1))/log(2.0));
int p;
if(dp[dx][k]>dp[dy-(1<<k)+1][k])
{
p=pos[dy-(1<<k)+1][k];
}
else
{
p=pos[dx][k];
}
return p;
}
int main()
{ int T,m,a,b;
scanf("%d",&T);
while(T--){
scanf("%d",&m);
memset(du,0,sizeof(du));
memset(fir,0,sizeof(fir));
memset(pos,0,sizeof(pos));
memset(du,0,sizeof(du));
for(int i=0;i<maxn;i++)g[i].clear();
memset(height,0,sizeof(height));
memset(vis,false,sizeof(vis));
memset(dp,0,sizeof(dp));
for(int i=0;i<m-1;i++){
scanf("%d%d",&a,&b);
g[a].push_back(b);
du[b]++;
}
siz=1;
for(int i=1;i<=m;i++){
if(!du[i]){
dfs(i,1);break;
}
}
RMQ_ST();
scanf("%d%d",&a,&b);
printf("%d\n",LCA(a,b));
}
return 0;
}