1. 程式人生 > >關於LCA演算法-暴力&倍增&tarjan&樹連刨分&RMQ

關於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;
}