1. 程式人生 > >最近公共祖先-三(RMQ-ST)

最近公共祖先-三(RMQ-ST)

描述

上上回說到,小Hi和小Ho使用了Tarjan演算法來優化了他們的“最近公共祖先”網站,但是很快這樣一個離線演算法就出現了問題:如果只有一個人提出了詢問,那麼小Hi和小Ho很難決定到底是針對這個詢問就直接進行計算還是等待一定數量的詢問一起計算。畢竟無論是一個詢問還是很多個詢問,使用離線演算法都是隻需要做一次深度優先搜尋就可以了的。

那麼問題就來了,如果每次計算都只針對一個詢問進行的話,那麼這樣的演算法事實上還不如使用最開始的樸素演算法呢!但是如果每次要等上很多人一起的話,因為說不準什麼時候才能夠湊夠人——所以事實上有可能要等上很久很久才能夠進行一次計算,實際上也是很慢的!

“那到底要怎麼辦呢?在等到10分鐘,或者湊夠一定數量的人兩個條件滿足一個時就進行運算?”小Ho想出了一個折衷的辦法。

“哪有這麼麻煩!別忘了和離線演算法相對應的可是有一個叫做線上演算法的東西呢!”小Hi笑道。

小Ho面臨的問題還是和之前一樣:假設現在小Ho現在知道了N對父子關係——父親和兒子的名字,並且這N對父子關係中涉及的所有人都擁有一個共同的祖先(這個祖先出現在這N對父子關係中),他需要對於小Hi的若干次提問——每次提問為兩個人的名字(這兩個人的名字在之前的父子關係中出現過),告訴小Hi這兩個人的所有共同祖先中輩分最低的一個是誰?

提示:最近公共祖先無非就是兩點連通路徑上高度最小的點嘛!

“那你快教我啊!”小Ho耐不住性子。

“不要急,且聽我緩緩道來,還記得很久之前我和你說過的最近公共祖先其實就是這兩個點連通路徑上的那個折點麼(參見hiho一下第十一週樹的直徑)”小Hi問道。

“記得!”

“這個折點也就是這2點所連路徑上深度最小的那個點了!那麼這個問題其實和我們之前所提到的那個求區間最小值的是不是差不多(參見hiho一下第十六週——RMQ-ST演算法),只不過一個是在陣列上的區間,一個是在樹上的區間?”小Hi問道。

“你非要這麼說那我只能說是啦。。但是樹和陣列還是差了挺遠的吧。”小Ho表示汗顏。

小Hi點了點頭,隨即道:“那就這麼弄一下,我從樹的根節點開始進行深度優先搜尋,每次經過某一個點——無論是從它的父親節點進入這個點,還是從它的兒子節點返回這個點,都按順序記錄下來。這樣,是不是就把一棵樹轉換成了一個數組?而找到樹上兩個節點的最近公共祖先,無非就是找到這兩個節點最後一次出現在陣列中的位置所囊括的一段區間中深度最小的那個點?

小Ho顯然是沒有料到小Hi還有這一招,一上來也是感覺明顯就不對嘛,畢竟好好的樹怎麼隨便就弄成陣列了不是,但是靜下心來仔細想想:“從第一個點離開(返回它的父親節點),到從第二個點離開(返回它的父親節點)的這一段路程,的確經過的深度最小的點就是‘最近公共祖先’這一個點!”

看著小Ho露出了驚訝的神情,小Hi滿意的點了點頭,道:“這就是一個很好的將樹轉換成陣列來進行某些特殊演算法的方法!而且你仔細看看就會發現轉換出的陣列的長度其實就是邊數的2倍而已,也是O(n)的級別呢~”

“原來是這樣!那這次我只需要簡單的套用之前寫的演算法,很簡單嘛!”小Ho笑道。

“那是自然,你也不看看之前我們積累了一個月呢,現在你要是還磨磨蹭蹭的,回國怎麼向河蟹先生交代!”

“嘿嘿嘿……”

Close

輸入

每個測試點(輸入檔案)有且僅有一組測試資料。

每組測試資料的第1行為一個整數N,意義如前文所述。

每組測試資料的第2~N+1行,每行分別描述一對父子關係,其中第i+1行為兩個由大小寫字母組成的字串Father_i, Son_i,分別表示父親的名字和兒子的名字。

每組測試資料的第N+2行為一個整數M,表示小Hi總共詢問的次數。

每組測試資料的第N+3~N+M+2行,每行分別描述一個詢問,其中第N+i+2行為兩個由大小寫字母組成的字串Name1_i, Name2_i,分別表示小Hi詢問中的兩個名字。

對於100%的資料,滿足N<=10^5,M<=10^5, 且資料中所有涉及的人物中不存在兩個名字相同的人(即姓名唯一的確定了一個人),所有詢問中出現過的名字均在之前所描述的N對父子關係中出現過,且每個輸入檔案中第一個出現的名字所確定的人是其他所有人的公共祖先

輸出

對於每組測試資料,對於每個小Hi的詢問,按照在輸入中出現的順序,各輸出一行,表示查詢的結果:他們的所有共同祖先中輩分最低的一個人的名字。

Sample Input

4
Adam Sam
Sam Joey
Sam Micheal
Adam Kevin
3
Sam Sam
Adam Sam
Micheal Kevin

Sample Output

Sam
Adam
Adam

題解

對於這個題,它的核心部分其實就是通過找到每一個區間的深度最小值,然後把它的編號(第幾次出現)寫入dp陣列。

當在LCA函式裡面呼叫query函式的時候,因為之前已經通過RMQ-ST求得了 所有已知的區間的深度最小值所對應的編號(第幾次出現),所以query函式就可以找到dp數組裡面存的深度最小值對應的編號(第幾次出現,不再一一具體指出)。

既然已知該區間深度最小值對應的編號,然後就可以通過之前深搜時寫下的編號陣列f找到對應的點,該點通過map已經存下了字串和整型數對應的關係,進而也就找到了解。

陣列f裡面存的是第幾次出現了哪個點。

這就是這段程式碼的核心思想,至於RMQ-ST演算法的思想,我的另外一篇部落格裡面有,讀者可以自己去找一下,也可以參觀其它的部落格。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<map>
#include<string>
#include <cmath>
using namespace std;
const int N = 200005;
struct edge
{
    int to,nxt,d;
    edge(int t = 0,int n = 0,int d = 0):to(t),nxt(n),d(d){}
}E[N*2];
int n;
int head[N*2],tot,deg[N];
int cnt,vis[N],f[N*2],rk[N*2],pos[N*2],dis[N],dp[N*2][35];
//f儲存節點編號,rk儲存節點深度,pos記錄結點第一次出現的位置
map<string,int> mp;
string mm[N];
void init()
{
    memset(head,-1,sizeof(head));
    for(int i = 0;i < N;i++)
        vis[i] = dis[i] = 0;
    tot = cnt = 0;
    mp.clear();
}
void add_edge(int s,int t,int d)
{
    E[tot] = edge(t,head[s],d);
    head[s] = tot++;
}
void dfs(int u,int depth)
{
    vis[u] = 1;
    f[++cnt] = u;
    pos[u] = cnt;
    rk[cnt] = depth;
    for(int i = head[u];~i;i = E[i].nxt)//訪問所有子結點
    {
        int v = E[i].to;
        if(!vis[v])
        {
            //dis[v] = dis[u] + w;
            dfs(v,depth+1);
            f[++cnt] = u;
            rk[cnt] = depth;
        }
    }
}
void RMQ(int n)
{
    for(int i = 1;i <= n;i++)
        dp[i][0] = i;//初始化dp陣列,讓數組裡面區間長度為1的最小值設定為它本身 
    for(int j = 1;(1<<j) <= n;j++)
    {
        for(int i = 1;i+(1<<j)-1 <= n;i++)
        {
            int a = dp[i][j-1],b = dp[i + (1<<(j-1))][j-1];
            dp[i][j] = rk[a] < rk[b] ? a : b;
        }
    }
}
int query(int l,int r)
{
    int k = (int)(log(r - l + 1.0) / log(2.0));
    int a = dp[l][k],b = dp[r-(1<<k)+1][k];
    return rk[a] < rk[b] ? a : b;
}
int LCA(int u,int v)
{
    int x = pos[u],y = pos[v];
    if(x > y) swap(x,y);
    int t = query(x,y);//找到深度最小的結點編號
    return f[t];
}
int main()
{
    // ios::sync_with_stdio(false);
    // cin.tie(0);

    int n,m;
    init();
    cin >> n;
    string u,v;
    int num = 0;
    for(int i = 1;i <= n;i++)
    {
        cin >> u >> v;
        if(mp[u] == 0)
            mp[u] = ++num,mm[num] = u;
        if(mp[v] == 0)
            mp[v] = ++num,mm[num] = v;
        //cout << mp[u] << " " << mp[v] << "\n";
        add_edge(mp[u],mp[v],0);
        add_edge(mp[v],mp[u],0);
        deg[mp[v]]++;
    }
    dfs(1,1);
    RMQ(cnt);
    cin >> m;
    while(m--)
    {
        cin >> u >> v;
        int lca = LCA(mp[u],mp[v]);
        cout << mm[lca] << "\n";
    }
    getchar();
    getchar();
    return 0;
}