LCA和RMQ的一些事情(最近公共祖先與區間最值查詢)

原創戳這裏

本來想詳細寫寫LCA和RMQ的東西的,但是覺得積累得還不夠而且比較懶就不寫了。鑑於都是超經典問題,網上和大量書籍都是很好的學習材料,所以就不想說了

這裏只簡單說說原理,說說代碼實現上面的一些細節和注意問題,並且給出模板

 

下面寫提供幾個學習LCA和RMQ的博客,都很通熟易懂,向博主致敬

http://dongxicheng.org/structure/lca-rmq/

這個應該是講得最好的,且博主還有很多其他文章,可以讀讀,感覺認真讀了這篇,都不太需要看別的資料了,百度和谷歌搜索的第一位都是他,好東西大家一起學習

http://scturtle.is-programmer.com/posts/30055

這個博客講LCA的Tarjan算法個人覺得是比較好的,我看這篇文章,看了1個小時就搞懂了LCA的Tarjan,謝謝博主。可以認真閱讀,並且看懂裏面附帶的那個圖

其餘的博客,就請百度和谷歌了,能找到很多,都很好

 

一:LCA和RMQ是可以相互轉化的

往往都只是提到了LCA可以轉化爲RMQ去求解,其實RMQ也能轉化爲LCA去求解,RMQ怎麼轉LCA,可以看2007年的國家隊論文,裏面有介紹,非常好懂。不過個人覺得,RMQ轉LCA,可以學習這個思想,但是實際應用中最好不要,多此一舉的感覺,求解RMQ的算法很多,不必要用LCA去求解

所以下面講講LCA轉RMQ的實現方法(只講實現方法,具體的原理不講,可以看書百度,不過看了實現過程,原理大概也懂了)

LCA轉RMQ算法是一個在線算法:先用時間去做預處理,然後每讀入一個詢問,就用很短的時間去回答它,即”問一個答一個,回答時間很短“

預備知識:LCA轉爲RMQ後,幾乎是裸的RMQ問題,RMQ問題,這裏推薦ST算法求解,如果不懂ST算法,先學習一下

 

————————————————————————————————————————————————————————————

 

二:LCA的Tarjan算法

Tarjan算法是個離線算法:即先把所有詢問保存下來,但是不回答(也回答不了),重新組織這些詢問,然後再回答,但是回答的順序,不一定是詢問的順序,即”一口氣問完,處理完,再一口氣回答“。如果一定要你按照詢問的順序得出答案,那麼還要稍微處理一下

說說感悟:很多人說Tarjan算法強調遞推的性質,我個人感覺說遞推不夠直接,應該說是強調時間,先後順序。學了Tarjan幾個算法,都有時間戳這個概念,這個算法裏沒強調這個,但是有這個意思。它定義了一個概念,什麼叫處理完的節點,就是這個節點被訪問了且它下面的所有子樹的所有節點都被訪問了,就認爲這個節點是處理完了,由於是前序遍歷這棵樹,所以節點被處理,是有個先後順序的,我們知道Tarjan在處理完一個節點後,就看看這個節點涉及了哪些詢問,看看唄詢問的另一個點是否也是被處理完的,如果另一個點也是被處理完的,那麼這個詢問可以被回答,否則,現在還不能回答,要等下再回答,什麼時候回答,就是等到那個節點也被處理完的時候。

 

說說代碼實現上的問題

如果理解了Tarjan,寫出那個核心的dfs遍歷反而不難,有時候糾結的是怎麼保存詢問的答案,並且按照詢問的順序,還原出答案

首先,我們是先把詢問拆成兩份,例如詢問x和y的lca,拆成x和y的lca , y和x的lca,兩者是完全相同的,等價的

對於一系列詢問

1 2

1 3

2 3

3 4

變爲

1 2

2 1

1 3

3 1

2 3

3 2

3 4

4 3

然後保存,保存方式是用鄰接表(個人感覺這種方法比較好,可以用上位運算,記錄的東西也比較少)。保存在一個表中,表的下標從0開始標號

對已表中的第k項,例如 2 3 , 那麼k^1項和k項的LCA是相同的,所以就可以保存  a[k].lca = a[k^1].lca = ans

 

最後注意一點,Tarjan的僞代碼可以很好幫助理解算法本質,注意裏面一個並查集合並的操作Union(x,y)。這個Union(x,y)有好多寫法,其中最簡單的就是一個語句(個人推薦這種),這個Union的寫法會稍微影響到dfs函數裏面的寫法(不影響算法本質,只是影響寫法)

 

具體看模板

 

LCA轉RMQ的模板

複製代碼
const int N = 40010;
const int M = 25;

int _pow[M];     //事先保存2^x,不必重複計算
int head[N];     //鄰接表表頭
int ver[2*N];    //保存遍歷的節點序列,長度爲2n-1,從下標1開始保存
int R[2*N];      //和遍歷序列對應的節點深度數組,長度爲2n-1,從下標1開始保存
int first[N];    //每個節點在遍歷序列中第一次出現的位置
int dir[N];      //保存每個點到樹根的距離,很多問題中樹邊都有權值,會詢問兩點間的距離,如果樹邊沒權值,相當於權值爲1
int dp[2*N][M];  //這個數組記得開到2*N,因爲遍歷後序列長度爲2*n-1
bool vis[N];     //遍歷時的標記數組
int tot;
struct edge //保存邊,數組大小至少爲2*n
{
    int u,v,w,next;
}e[2*N];

void dfs(int u ,int dep) //遍歷樹,過程中順便做了好多事情
{
    vis[u] = true; ver[++tot] = u; first[u] = tot; R[tot] = dep;
    for(int k=head[u]; k!=-1; k=e[k].next)
        if( !vis[e[k].v] )
        {
            int v = e[k].v , w = e[k].w;
            dir[v] = dir[u] + w;
            dfs(v,dep+1);
            ver[++tot] = u; R[tot] = dep;
        }
}

int RMQ(int x ,int y) //這個詢問僅僅是返回一個位置,即LCA所在序列數組的位置,ver[res]纔是LCA的標號
{
    int K = (int)(log((double)(y-x+1)) / log(2.0));
    int a = dp[x][K] , b = dp[y-_pow[K]+1][K];
    if(R[a] < R[b]) return a;
    else            return b;
}

int LCA(int u ,int v)  //返回點u和點v的LCA
{
    int x = first[u] , y = first[v];
    if(x > y) swap(x,y);
    int res = RMQ(x,y);
    return ver[res];
}


//  lcaxy = LCA(x,y);
//  lcaab = LCA(a,b);
複製代碼

 

Tarjand的僞代碼

複製代碼
void Tarjan(int u)
{
    vis[u] = true;
    Make-Set(u); //以點u自己爲代表元素建立一個集合,此時集合也只有它自己一個元素
    ance[Find(u)] = u; //記錄點u所在的那個集合的祖先是u自己,其實此時還是隻有它自己
    for(u的所有兒子v)
        if(該兒子v沒有被訪問)
        {
            Tarjan(v);
            Union(u,v); //將兒子v所在集合併在點u所在的集合,點u已經是集合的代表元素
            ance[Find(u)] = u; //確保點u所在的集合的祖先是u自己
        }
    colour[u] = true;   //這個點u認爲已經處理完
    for(u的所有兒子v)
        if( colour[v] )  //兒子v也被處理完
            LCA(u,v) = LCA(v,u) = ance[Find(v)];  //兩者的LCA此時可以回答了,就是兒子v所在的集合的祖先
}
複製代碼

 

Tarjan模板

複製代碼
using namespace std;
const int N = 40010;
const int M = 410;

int head[N];            //樹邊鄰接表的表頭
int __head[N];          //保存詢問的鄰接表的表頭
struct edge{            //保存邊
    int u,v,w,next;
}e[2*N];
struct ask{            //保存詢問
    int u,v,lca,next;
}ea[M];
int dir[N];              //保存點到樹根的距離
int fa[N];               //並查集,保存集合的代表元素
int ance[N];             //保存集合的組合,注意對象是集合而不是元素
bool vis[N];             //遍歷時的標記數組

inline void add_edge(int u,int v,int w,int &k) //保存邊
{
    e[k].u = u; e[k].v = v; e[k].w = w;
    e[k].next = head[u]; head[u] = k++;
    u = u^v; v = u^v; u = u^v;
    e[k].u = u; e[k].v = v; e[k].w = w;
    e[k].next = head[u]; head[u] = k++;
}

inline void add_ask(int u ,int v ,int &k) //保存詢問
{
    ea[k].u = u; ea[k].v = v; ea[k].lca = -1;
    ea[k].next = __head[u]; __head[u] = k++;
    u = u^v; v = u^v; u = u^v;
    ea[k].u = u; ea[k].v = v; ea[k].lca = -1;
    ea[k].next = __head[u]; __head[u] = k++;
}

int Find(int x)
{
    return x == fa[x] ? x : fa[x] = Find(fa[x]);
}
void Union(int u ,int v)
{
    fa[v] = fa[u];  //可寫爲  fa[Find(v)] = fa[u];
}

void Tarjan(int u)
{
    vis[u] = true;
    ance[u] = fa[u] = u; //可寫爲 ance[Find(u)] = fa[u] = u;
    for(int k=head[u]; k!=-1; k=e[k].next)
        if( !vis[e[k].v] )
        {
            int v = e[k].v , w = e[k].w;
            dir[v] = dir[u] + w;
            Tarjan(v);
            Union(u,v);
            ance[Find(u)] = u;  //可寫爲ance[u] = u;  //甚至不要這個語句都行
        }
    for(int k=__head[u]; k!=-1; k=ea[k].next)
        if( vis[ea[k].v] )
        {
            int v = ea[k].v;
            ea[k].lca = ea[k^1].lca = ance[Find(v)];
        }
}

int main()
{
    //.............省略..............
    memset(head,-1,sizeof(head));
    memset(__head,-1,sizeof(__head));
    tot = 0;
    for(int i=1; i<n; i++)  //建樹
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        add_edge(u,v,w,tot);
    }
    tot = 0;
    for(int i=0; i<q; i++) //拆開保存詢問
    {
        int u,v;
        scanf("%d%d",&u,&v);
        add_ask(u,v,tot);
    }
    memset(vis,0,sizeof(vis));
    dir[1] = 0;
    Tarjan(1);
    for(int i=0; i<q; i++)
    {
        int s = i * 2 , u = ea[s].u , v = ea[s].v , lca = ea[s].lca;
        //已經按順序取出了詢問和答案,lca = LCA(u,v)
    }
    return 0;
}
發佈了19 篇原創文章 · 獲贊 13 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章