RMQ and LCA

                                        Range Minimum Query and Lowest Common Ancestor

    【原文見  http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=lowestCommonAncestor

                                                                                                                作者:      By danielp
                                                                                                                    Topcoder  Member
                                                                                                                翻譯:      農夫三拳@seu
Introduction
 Notations
 Range Minimum Query (RMQ)
     Trivial algorithms for RMQ
     A <O(N), O(sqrt(N))> solution
     Sparse Table (ST) algorithm
     Segment Trees
 Lowest Common Ancestor (LCA)
     A <O(N), O(sqrt(N))> solution
     Another easy solution in <O(N logN, O(logN)>
     Reduction from LCA to RMQ
 From RMQ to LCA
 An <O(N), O(1)> algorithm for the restricted RMQ
 Conclusion 

Introduction

      在一棵樹中查找一對結點的最近公共祖先(LCA)的問題在20世紀末期已經被仔細的研究過了,並且它現在已經成爲算法中圖論的基本算法了。這個問題之所以有趣並不是因爲處理它的算法很有技巧,而是因爲它在字符串處理和生物學計算中的廣泛應用,例如,當LCA和後綴樹或者其他樹形結構在一起使用時。Harel and Tarjan是首先深入研究這個問題的人,他們得出:在對輸入樹LCA進行線性處理後,查詢可以在常數時間內得到答案。他們的工作已經得到了廣泛的延伸,這篇教程將展示一些有趣的方法,而它們還也可以用在其他的問題上。


      讓我們考慮一個不太抽象的LCA的例子:生命樹。地球上當前的居住者是由其他物種進化而來已經是一個不爭的事實。這種進化結構可以表示成一棵樹,其中節點表示物種,而它的孩子結點表示從該物種直接進化得到的物種。現在通過在樹中查找一些結點的LCA把具有相似特徵的物種劃分成組,我們可以找出兩個物種共同的祖先,並且我們可以知道它們所擁有的相似特徵是來自於那個祖先。

      Range Minimum Query(RMQ)被用在數組中用來查找兩個指定索引中具有最小值的元素的位置。我們後面將會看到LCA問題可以歸約成一個帶限制的RMQ問題,其中相鄰的數組元素相差1。

      儘管如此, RMQ並不是僅僅和LCA一起用的。當他們在和後綴數組(一個新的數據結構,它支持和後綴樹同等效率的字符串查詢,但是使用更少的內存且編碼很簡單)一起使用時,在字符串處理中扮演着相當重要的角色。

      在這篇教程中,我們將首先討論RMQ。我們將給出解決這個問題的多種方法--有一些速度比較慢但是容易編碼,而其他的則更快。在第二部分我們將討論LCA和RMQ之間的關係。首先我們先回顧一下不使用RMQ來解決LCA的兩個簡單方法;然後我們將指出RMQ和LCA問題其實是等價的;並且,最後,我們將看到RMQ問題怎樣規約成它的限制版本,並且對於這個特殊情況給出一個最快的算法。


Notations
      假設一個算法預處理時間爲 f(n),查詢時間爲g(n)。這個算法複雜度的標記爲<f(n), g(n)>。我們將用RMQA(i, j)來表示數組中索引i和j之間最小值的位置。 uv的離樹T根結點最遠的公共祖先用LCAT(u, v)表示。

Range Minimum Query(RMQ)
      給定數組A[0, N-1]找出給定的兩個索引間的最小值的位置。



Trivial algorithms for RMQ

       對每一對索引(i, j),將RMQA(i, j)存儲在M[0, N-1][0, N-1]表中。普通的計算將得到一個<O(N3), O(1)> 複雜度的算法。儘管如此,通過使用一個簡單的動態規劃方法,我們可以將複雜度降低到<O(N2), O(1)>。預處理的函數和下面差不多:

void process1(int M[MAXN][MAXN], int A[MAXN], int N)
{
      
int
 i, j;
    
for (i =0; i < N; i++
)
        M[i][i] 
=
 i;
    
for (i = 0; i < N; i++
)
        
for (j = i + 1; j < N; j++
)
            
if (A[M[i][j - 1]] <
 A[j])
                M[i][j] 
= M[i][j - 1
];
            
else

                M[i][j] 
= j;
}
這個普通的算法相當的慢並且使用 O(N2)的空間,對於大數據它是無法工作的。


An <O(N), O(sqrt(N))> solution

  一個比較有趣的點子是把向量分割成sqrt(N)大小的段。我們將在M[0,sqrt(N)-1]爲每一個段保存最小值的位置。
M可以很容易的在O(N)時間內預處理。下面是一個例子:


       現在讓我們看看怎樣計算RMQA(i, j)。想法是遍歷所有在區間中的sqrt(N)段的最小值,並且和區間相交的前半和後半部分。爲了計算上圖中的RMQA(2,7),我們應該比較A[2]A[M[1]]A[6] 和A[7],並且獲得最小值的位置。可以很容易的看出這個算法每一次查詢不會超過3 * sqrt(N)次操作。

      這個方法最大的有點是能夠快速的編碼(對於TopCoder類型的比賽),並且你可以把它改成問題的動態版本(你可以在查詢中間改變元素)。

Sparse Table (ST) algorithm    

     一個更好的方法預處理RMQ 是對2k 的長度的子數組進行動態規劃。我們將使用數組M[0, N-1][0, logN]進行保存,其中M[i][j]是以i 開始,長度爲 2j的子數組的最小值的索引。下面是一個例子



爲了計算M[i][j]我們必須找到前半段區間和後半段區間的最小值。很明顯小的片段有這2j - 1 長度,因此遞歸如下:


預處理的函數如下:
void process2(int M[MAXN][LOGMAXN], int A[MAXN], int N)
  
{
      
int
 i, j;
   
  
//initialize M for the intervals with length 1

      for (i = 0; i < N; i++)
          M[i][
0=
 i;
  
//compute values from smaller to bigger intervals

      for (j = 11 << j <= N; j++)
          
for (i = 0; i + (1 << j) - 1 < N; i++
)
              
if (A[M[i][j - 1]] < A[M[i + (1 << (j - 1))][j - 1
]])
                  M[i][j] 
= M[i][j - 1
];
              
else

                  M[i][j] 
= M[i + (1 << (j - 1))][j - 1];
  }
  

一旦我們預處理了這些值,讓我們看看怎樣使用它們去計算RMQA(i, j)。思路是選擇兩個能夠完全覆蓋區間[i..j]的塊並且找到它們之間的最小值。設k = [log(j - i + 1)].。爲了計算RMQA(i, j) 我們可以使用下面的公式



So, the overall complexity of the algorithm is <O(N logN), O(1)>

Segment trees

     爲了解決RMQ問題我們也可以使用線段樹。線段樹是一個類似堆的數據結構,可以在基於區間數組上用對數時間進行更新和查詢操作。我們用下面遞歸方式來定義線段樹的[i, j]區間:

  • 第一個結點將保存區間[i, j]區間的信息
  • 如果i<j 左右的孩子結點將保存區間[i, (i+j)/2][(i+j)/2+1, j] 的信息

      注意具有N個區間元素的線段樹的高度爲[logN] + 1。下面是區間[0,9]的線段樹:




線段樹和堆具有相同的結構,因此我們定義x是一個非葉結點,那麼左孩子結點爲2*x,而右孩子結點爲2*x+1

 使用線段樹解決RMQ問題,我們應該使用數組 M[1, 2 * 2[logN] + 1],這裏M[i]保存結點i區間最小值的位置。初始時M的所有元素爲-1。樹應當用下面的函數進行初始化(be是當前區間的範圍):

  
void initialize(int node, int b, int e, int M[MAXIND], int A[MAXN], int N)
{
if (b ==
 e)
M[node] 
=
 b;
    
else

     
{
//compute the values in the left and right subtrees

        initialize(2 * node, b, (b + e) / 2, M, A, N);
        initialize(
2 * node + 1, (b + e) / 2 + 1
, e, M, A, N);
//
search for the minimum value in the first and
//second half of the interval

        if (A[M[2 * node]] <= A[M[2 * node + 1]])
            M[node] 
= M[2 *
 node];
        
else

            M[node] 
= M[2 * node + 1];
    }

}

      上面的函數映射出了這棵樹建造的方式。當計算一些區間的最小值位置時,我們應當首先查看子結點的值。調用函數的時候使用 node = 1b = 0e  = N-1

      現在我們可以開始進行查詢了。如果我們想要查找區間[i, j]中的最小值的位置時,我們可以使用下一個簡單的函數:

  int query(int node, int b, int e, int M[MAXIND], int A[MAXN], int i, int j)
{
int
 p1, p2;
 
 
//
if the current interval doesn't intersect
//the query interval return -1

    if (i > e || j < b)
        
return -1
;
 
//
if the current interval is included in
//the query interval return M[node]

    if (b >= i && e <= j)
        
return
 M[node];
 
//
compute the minimum position in the
//left and right part of the interval

    p1 = query(2 * node, b, (b + e) / 2, M, A, i, j);
    p2 
= query(2 * node + 1, (b + e) / 2 + 1
, e, M, A, i, j);
 
//
return the position where the overall
//minimum is

    if (p1 == -1)
        
return M[node] =
 p2;
    
if (p2 == -1
)
        
return M[node] =
 p1;
    
if (A[p1] <=
 A[p2])
        
return M[node] =
 p1;
    
return M[node] =
 p2;
 
}

     你應該使用node = 1b = 0e = N - 1來調用這個函數,因爲分配給第一個結點的區間是[0, N-1]

     可以很容易的看出任何查詢都可以在O(log N)內完成。注意當我們碰到完整的in/out區間時我們停止了,因此數中的路徑最多分裂一次。用線段樹我們獲得了<O(N), O(logN)>的算法。線段樹非常強大,不僅僅是因爲它能夠用在RMQ上,

還因爲它是一個非常靈活的數據結構,它能夠解決動態版本的RMQ問題和大量的區間搜索問題。

Lowest Common Ancestor (LCA)

      給定一棵樹T和兩個節點uv,找出uv的離根節點最遠的公共祖先。下面是一個例子(這篇教程中所有的例子中樹的根結點均爲1):




An <O(N), O(sqrt(N))> solution     

      將輸入分成同等大小的部分來解決RMQ問題是一個很有趣的方法。這個方法對LCA問題同樣適用。大致思想是將樹分成sqrt(H)個部分,其中H是樹的高度。因此第一個段將包含0sqrt(H)-1層,第二個段則包含sqrt(H)2*sqrt(H)-1層,依次下去。下面給出了樣例中的樹是如何被分割的:



      現在,對於每一個結點,我們應該知道每一個段的在上一層中的祖先。我們將預處理這些值,並將他們存儲在P[1, MAXN]中。下面是對於樣例中的樹的P數組內容(爲了簡化,對於在第一個段中的所有結點i,P[i]=1):



      注意對於每一個段中的上面一部分,P[i]=T[i]。我們可以使用深度優先搜索對P進行預處理(T[i]是樹中i結點的父親結點,nr[sqrt(H)]L[i]是結點i所處的層的編號):

  
void dfs(int node, int T[MAXN], int N, int P[MAXN], int L[MAXN], int nr)  {
int
 k;
 
//
if node is situated in the first
//
section then P[node] = 1
//
if node is situated at the beginning
//
of some section then P[node] = T[node]
//
if none of those two cases occurs, then
//P[node] = P[T[node]]

if (L[node] < nr)
         P[node] 
= 1
;
else

        
if(!(L[node] % nr))
              P[node] 
=
 T[node];
        
else

              P[node] 
= P[T[node]];
 
for
 each son k of node
      dfs(k, T, N, P, L, nr);
}

 現在,我們可以很容易的進行查詢了。爲了找到LCA(x,y),我們首先找出它所在的段,然後再用普通的方法計算它。下面是代碼:

 int LCA(int T[MAXN], int P[MAXN], int L[MAXN], int x, int y)
{
//
as long as the node in the next section of
//
x and y is not one common ancestor
//
we get the node situated on the smaller
//lever closer

    while (P[x] != P[y])
          
if (L[x] >
 L[y])
=
 P[x];
          
else

                y 
= P[y];
          
//now they are in the same section, so we trivially compute the LCA

while (x != y)
          
if (L[x] >
 L[y])
             x 
=
 T[x];
          
else

             y 
= T[y];
      
return
 x;
}

      這個函數最多執行2 * sqrt(H)次操作。通過使用這個方法,我們得到了<O(N), O(sqrt(H))>的算法,這裏H指的是樹的高度。在最壞的情況下H=N,因此總的複雜度爲<O(N), O(sqrt(N))>。這個算法的主要好處是易於編碼(Division1中的程序員應該在15分鐘內完成這段代碼)。

Another easy solution in <O(N logN, O(logN)>

    如果我們對這個需要一個更快的解決方法,我們可以使用動態規劃。首先我們構建一張表P[1,N][1,logN],這裏P[i][j]指的是結點i的第2j個祖先。爲了計算這個值,我們可以使用下面的遞歸:
 



預處理的函數如下:
 void process3(int N, int T[MAXN], int P[MAXN][LOGMAXN])
{
    
int
 i, j;
 
//we initialize every element in P with -1

    for (i = 0; i < N; i++)
        
for (j = 01 << j < N; j++
)
            P[i][j] 
= -1
;
 
//the first ancestor of every node i is T[i]

    for (i = 0; i < N; i++)
        P[i][
0=
 T[i];
 
//bottom up dynamic programing

for (j = 11 << j < N; j++)
       
for (i = 0; i < N; i++
)
           
if (P[i][j - 1!= -1
)
P[i][j] 
= P[P[i][j - 1]][j - 1
];
}

這個過程將花費O(N logN) 的時間和空間。現在讓我們看看如何查詢。用L[i]來表示節點i在樹中所處的層數。可以看到,如果pq在樹中的同一層中,我們可以使用一個類二分查找的方法進行搜索。因此,對於2j次方(界於log[L[p]0之間,降序),如果P[p][j] != P[q][j] ,那麼可以知道LCA(p, q)必然在更高的層中,因此我們繼續搜索LCA(p = P[p][j], q = P[q][j])。最後,pq都有了相同的祖先,因此返回T[p]。讓我們看看如果L[p] != L[q]的情況。 不妨假設L[p] < L[q]。我們可以使用類似的二分搜索方法來查找與q在同一層次的p的祖先,然後我們在用下面所描述的方法計算LCA。整個函數如下:


int query(int N, int P[MAXN][LOGMAXN], int T[MAXN],
int L[MAXN], int p, int
 q)
{
    
int
 tmp, log, i;
 
//if p is situated on a higher level than q then we swap them

if (L[p] < L[q])
tmp 
= p, p = q, q =
 tmp;
//we compute the value of [log(L[p)]

    for (log = 11 << log <= L[p]; log++);
    log
--
;
 
//
we find the ancestor of node p situated on the same level
//with q using the values in P

    for (i = log; i >= 0; i--)
        
if (L[p] - (1 << i) >=
 L[q])
            p 
=
 P[p][i];
 
    
if (p ==
 q)
        
return
 p;
 
//we compute LCA(p, q) using the values in P

for (i = log; i >= 0; i--)
        
if (P[p][i] != -1 && P[p][i] !=
 P[q][i])
            p 
= P[p][i], q =
 P[q][i];
 
    
return
 T[p];
}

  

      現在,我們可以看到這個函數最多需要執行2*log(H)次的操作,這裏的H是樹的高度。在最壞情況下H=N,因此總的時間複雜度爲<O(NlogN),O(logN)>。這個方案非常易編碼,並且它比前一個要快。

Reduction from LCA to RMQ

     現在,讓我們看看怎樣用RMQ來計算LCA查詢。事實上,我們可以在線性時間裏將LCA問題規約到RMQ問題,因此每一個解決RMQ的問題都可以解決LCA問題。讓我們通過例子來說明怎麼規約的:




                                                                    點擊放大圖片
      注意LCAT(u, v)是在對T進行dfs過程當中在訪問uv之間離根結點最近的點。因此我們可以考慮樹的歐拉環遊過程uv之間所有的結點,並找到它們之間處於最低層的結點。爲了達到這個目的,我們可以建立三個數組:

E[1, 2*N-1]  - 對T進行歐拉環遊過程中所有訪問到的結點;E[i]是在環遊過程中第i個訪問的結點
L[1,2*N-1] -  歐拉環遊中訪問到的結點所處的層數;L[i]E[i]所在的層數
H[1, N] - H[i] E中結點i第一次出現的下標(任何出現i的地方都行,當然選第一個不會錯)

    假定H[u]<H[v](否則你要交換uv)。可以很容易的看到uv第一次出現的結點是E[H[u]..H[v]]。現

在,我們需要找到這些結點中的最低層。爲了達到這個目的,我們可以使用RMQ。因此 LCAT(u, v) = E[RMQL(H[u], H[v])] (記住RMQ返回的是索引),下面是E,L,H數組:


點擊放大圖片


注意L中連續的元素相差爲1。

From RMQ to LCA
      我們已經看到了LCA問題可以在線性時間規約到RMQ問題。現在讓我們來看看怎樣把RMQ問題規約到LCA。這個意味着我們實際上可以把一般的RMQ問題規約到帶約束的RMQ問題(這裏相鄰的元素相差1)。爲了達到這個目的,我們需要使用笛卡爾樹。
      對於數組A[0,N-1]的笛卡爾樹C(A)是一個二叉樹,根節點是A的最小元素,假設iA數組中最小元素的位置。當i>0時,這個笛卡爾樹的左子結點是A[0,i-1]構成的笛卡爾樹,其他情況沒有左子結點。右結點類似的用A[i+1,N-1]定義。注意對於具有相同元素的數組A,笛卡爾樹並不唯一。在這篇教程中,將會使用第一次出現的最小值,因此笛卡爾樹看作唯一。可以很容易的看到RMQA(i, j) = LCAC(i, j)
下面是一個例子:





現在我們需要做的僅僅是用線性時間計算C(A)。這個可以使用棧來實現。初始棧爲空。然後我們在棧中插入A的元素。在第i步,A[i]將會緊挨着棧中比A[i]小或者相等的元素插入,並且所有較大的元素將會被移除。在插入結束之前棧中A[i]位置前的元素將成爲i的左兒子,A[i]將會成爲它之後一個較小元素的右兒子。在每一步中,棧中的第一個元素總是笛卡爾樹的根。
如果使用棧來保存元素的索引而不是值,我們可以很輕鬆的建立樹。下面是上述例子中每一步棧的狀態:

Step Stack Modifications made in the tree
0 0 0 is the only node in the tree.
1 0 1 1 is added at the end of the stack. Now, 1 is the right son of 0.
2 0 2 2 is added next to 0, and 1 is removed (A[2] < A[1]). Now, 2 is the right son of 0 and the left son of 2 is 1.
3 3 A[3] is the smallest element in the vector so far, so all elements in the stack will be removed and 3 will become the root of the tree. The left child of 3 is 0.
4 3 4 4 is added next to 3, and the right son of 3 is 4.
5 3 4 5 5 is added next to 4, and the right son of 4 is 5.
6 3 4 5 6 6 is added next to 5, and the right son of 5 is 6.
7 3 4 5 6 7 7 is added next to 6, and the right son of 6 is 7.
8 3 8 8 is added next to 3, and all greater elements are removed. 8 is now the right child of 3 and the left child of 8 is 4.
9 3 8 9 9 is added next to 8, and the right son of 8 is 9.

     注意A中的每個元素最多被增加一次和最多被移除一次。因此上述算法的時間複雜度爲O(N)。下面是樹的處理函數:

void computeTree(int A[MAXN], int N, int T[MAXN])
 
{
    
int st[MAXN], i, k, top = -1
;
 
//
we start with an empty stack
//at step i we insert A[i] in the stack

    for (i = 0; i < N; i++)
    
{
//
compute the position of the first element that is
//equal or smaller than A[i]

        k = top;
        
while (k >= 0 && A[st[k]] >
 A[i])
            k
--
;
//we modify the tree as explained above

       if (k != -1)
            T[i] 
=
 st[k];
       
if (k <
 top)
            T[st[k 
+ 1]] =
 i;
//
we insert A[i] in the stack and remove
//any bigger elements

        st[++k] = i;
        top 
=
 k;
    }

//the first element in the stack is the root of
//the tree, so it has no father

    T[st[0]] = -1;
}

 
An<O(N), O(1)> algorithm for the restricted RMQ

      現在我們知道了一般的RMQ問題可以使用LCA歸約成約束版本。這裏,數組中相鄰的元素差值爲1.我們可以使用一個更快的<O(N), O(1)> 的算法。下面我們將在數組A[0,N-1]上解決RMQ問題,這裏|A[i]-A[i+1]|=1,i=[1,N-1]。我們將把A轉換爲一個二元的有着N-1個元素的數組,其中A[i]=A[i]-A[i+1]。很顯然A中的元素只有可能是+1或者-1。注意原來的A[i]的值現在是A[1],A[2],...,A[i]的和加上原來的A[0]。儘管如此,下面我們根本不需要原來的值。


      爲了解決這個問題的約束版本,我們需要將A分成l = [(log N) / 2]的大小塊.讓A'[i]A中第i塊的最小值,B[i]A中最小塊值的位置。A和B的長度均爲N/l。現在我們利用第一節中討論的ST算法預處理A'數組。這個將花費O(N/l * log(N/l))=O(N)的時間和空間。經過預處理之後,我們可以在O(1)時間內在很多塊上進行查詢。具體的查詢過程和上面說過的一樣。注意每個塊的長度爲l=[(logN)/2],這個非常的小。同樣,要注意A是一個二元數組。二元數組的總的元素的大小l滿足2l=sqrt(N)。因此,對於每一個二元數組中的塊l,我們需要在表P中查詢每一對索引的RMQ。這個可以在O(sqrt(N)*l2)=O(N) 的時間和空間內解決。爲了索引表P,可以預處理A中的每一個塊並且將其存儲在數組T[1,N/l]中。塊的類型可以成爲一個二進制數如果把-1替換成0,把+1替換成1。

    現在,對於詢問 RMQA(i, j) 我們有兩種情況:

  • ij在同一個塊中,因此我們使用在PT中計算的值 
  • ij在不同的塊中,因此我們計算三個值:從ii所在塊的末尾的PT中的最小值,所有ij中塊中的通過與處理得到的最小值以及從j所在塊ij在同一個塊中,因此我們使用在PT中計算的值jPT的最小值;最後我們我們只要計算三個值中最小值的位置即可。
    Conclusion
          RMQ和LCA是密切相關的問題,因爲他們互相之間都可以規約。有許多算法可以用來解決它們,並且他們適應於一類問題。690

    下面是一些用來練習線段樹,LCA和RMQ的題目:

    SRM 310 -> 
    Floating Median
    http://www.topcoder.com/tc?module=LinkTracking&link=http://acm.pku.edu.cn/JudgeOnline/problem?id=1986&refer=
    http://www.topcoder.com/tc?module=LinkTracking&link=http://acm.pku.edu.cn/JudgeOnline/problem?id=2374&refer=
    http://www.topcoder.com/tc?module=LinkTracking&link=http://acmicpc-live-archive.uva.es/nuevoportal/data/problem.php?p=2045&refer=
    http://www.topcoder.com/tc?module=LinkTracking&link=http://acm.pku.edu.cn/JudgeOnline/problem?id=2763&refer=
    http://www.topcoder.com/tc?module=LinkTracking&link=http://www.spoj.pl/problems/QTREE2/&refer=
    http://www.topcoder.com/tc?module=LinkTracking&link=http://acm.uva.es/p/v109/10938.html&refer=
    http://www.topcoder.com/tc?module=LinkTracking&link=http://acm.sgu.ru/problem.php?contest=0%26problem=155&refer=


    References
    - "
    Theoretical and Practical Improvements on the RMQ-Problem, with Applications to LCA and LCE" [PDF] by Johannes Fischer and Volker Heunn
    - "
    The LCA Problem Revisited" [PPT] by Michael A.Bender and Martin Farach-Colton - a very good presentation, ideal for quick learning of some LCA and RMQ aproaches
    - "
    Faster algorithms for finding lowest common ancestors in directed acyclic graphs" [PDF] by Artur Czumaj, Miroslav Kowaluk and Andrzej Lingas 

  • 發佈了44 篇原創文章 · 獲贊 50 · 訪問量 27萬+
    發表評論
    所有評論
    還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
    相關文章