零零散散學算法之詳解RMQ & LCA

深入理解RMQ & LCA

 

正文

 

第一節 RMQLCA概述

 

       LCA:Lowest Common Ancestor,譯爲最近公共祖先。其解釋就是說:在有根樹中,找出樹中任意兩個節點最近的公共祖先,或者說找到任意兩個節點離樹根最遠的公共祖先。

 

       RMQRange Minimum Query,譯爲區間最小值查詢。其解釋就是說:對於含有N個元素的數列A,在數列中找到兩個指定索引之間的最小值及最小值的位置。

 

第二節 RMQ Algorithm

 

       首先我們來看RQM算法,我將會根據預處理和查詢的速度介紹幾種解決該問題的方法。

   

    設有數組A[N],其表示如下:

    要求求得區間(2,7)的最小元素,如下圖所示:

解法一:直接遍歷區間

 

       看到這個問題之後,我們最先想到的就是對區間的這些數進行一次遍歷,就可以找到區間的最值,因此查詢的時間爲O(M)。但是,當數據量非常大並且查詢很頻繁時,直接遍歷序列的效果就不是那麼理想了。因爲每查詢一次就得對序列做一次遍歷,對於大數據量這顯然不能滿足要求了。不過對於小數據量,這種算法倒是不錯的選擇!  

       查詢:O(M)

算法的代碼如下:

int MaxNum = 0; for(i = 0; i < range; i++) {    /**查找最大值**/    if(array[i] > MaxNum)    {       MaxNum = array[i];    } }

解法二:切割法

 

       解法一中查詢的速度爲O(M),如果每次查詢都這樣的話,那真就成了龜速了。於是我們對解法一做了預處理,這就是該節要講的:切割法。

    首先,我們將序列分成sqrt(N)個部分,用數組M[sqrt(N) ]來表示每個部分中最小的值的下標,即這個最小數的位置。對於數組M,我們只需對原序列進行一次遍歷就可以得到M。如下圖所示:

       接下來我們來求RMQ[27]。爲了得到區間[27]的最小值,我們需比較A[2]A[M[1]]A[6],以及A[7],並得到他們中最小值的下標。

 

    分析:其實,這種方法較第一種方法而言並沒有實質的改進,甚至還不如方法一。至於爲什麼這樣做,我的解釋是:我們是基於查詢快慢的角度上來比較的,說白了,就是我們追求的是查詢速度,所以說只要查的快了,先做一些預處理也是值得的(解法四正是基於這種思想)現在我們根據上面的例子來看看法二,當做完預處理之後,得到了數組M,此時我們要求區間的最值,那麼我們只需將在區間內,包含數組M的值以及包含兩個邊界的值作比較就行,這樣的話,查詢的次數:O(M) <= 查詢次數 < O(M) + K,其中K < sqrt(N)。

 

解法三:排序

 

       解法二已經提到我們的目的是查得快,那麼我們可對選擇區間的這M個數據進行排序,然後就可以直接得到最小值。但是如果做排序的話,會有很大的缺陷。我們來看看。

 

       分析:我們選擇快速排序,O(M * LogM),但是快速排序會改變序列中數的相對位置,因此用快排的話,爲了保證原數據的順序不變,我們還得用O(M)的空間來維護原序列,因此這樣的消耗是很大的。附註:複雜度爲O(M * M)的排序算法在這就不囉嗦了!你懂得!

    查詢:O(1)

OK,我們來實現我們的想法,代碼如下:

快速排序 int partition(int *array, int low, int high) { 	int	key = array[high]; 	int	i = low; 	int	j = high;  	while(i < j) 	{ 		while(array[i] <= key && i < j) 		{ 			i++; 		} 		array[j] = array[i];  		while(array[j] >= key && i < j) 		{ 			j--; 		} 		array[i] = array[j]; 	} 	array[i] = key; 	 	return i; }  void quicksort(int *array, int low, int high) { 	int	index; 	int	i = low; 	int	j = high;  	if(i < j) 	{ 		index = partition(array, low, high); 		 		quicksort(array, low, index - 1); 		quicksort(array, index + 1, high); 	} }

排完序之後就可以直接得到最值了!

 

解法四:Sparse Table(ST) algorithm

 

       ST算法是一種比較高效的在線處理RMQ問題的算法,所謂在線算法,是指每輸入一個查詢就會馬上處理這個查詢。ST算法首先會對序列做預處理,完成之後就可以對查詢做回答了。

 

       分析:

               預處理:O(N * LogN)。

               查詢:O(1),這樣的查詢正是我們想要的

 

好了,我來詳細講述一下ST算法:

 

       預處理:首先用維護一個數組M[N][LogN]M[i][j]的值是從原序列Ai位置開始,連續2j 個元素的最小值的下標,如下所示:

 

       那麼,我們如何計算M[i][j]呢?

       我們採用DP的思想將區間分成兩部分,即M[i][j - 1]M[i][2^(j - 1)]。現在我們只需比較這兩個子區間就可以得到M[i][j]了。比較規則如下:

 於是乎,就可按照此寫出代碼:
void Proprocessing(int M[N][logN], int *A, int N) { 	int	i, j; 	 	for(j = 1; (1 << j) < N; j++) 	{ 		for(i = 0; (i + (1 << j) - 1) < N; i++) 		{ 			if(A[ M[i][j - 1] ] < A[ M[i + (1 << (j - 1))][i - 1]]) 			{ 				M[i][j] = M[i][j - 1]; 			} 			else 			{ 				M[i][j] = A[ M[i + (1 << (j - 1))][i - 1]]; 			} 		} 	} }

解法五:線段樹

 
       我們也可用線段樹來解決RMQ問題,如需瞭解線段樹,請到此一遊:
 
線段樹的構造口訣:

 ok,我們根據口訣,並用上面的例子構造了線段樹,如下:

        那麼將線段樹應用到RMQ問題中,首先,維護一個有2^([logN] + 1 + 1) 元素,名爲M的數組,即M[2^([logN] + 1 + 1)],我先來解釋一些數組M的意義:M[i]表示已劃分節點區間的最小值的位置(下標)。
 
    知道了這些,那我們就通過代碼來實現線段樹的構造,並通過節點所代表的值來計算得到數組M。代碼如下:
void init_tree(int node, int low, int high, int *array, int *M) { /***node:表示線段樹中的某個節點 ****low	:表示低索引 ****high:表示高索引 ****array:表示原數組 ****M:	表示維護下標的數組 ***/         if(low == high)	//爲葉子節點         {                 M[node] = low;         }         else         {                 init_tree(2 * node, low, (low + high)/2, array, M);                 init_tree(2 * node + 1, (low + high)/2 + 1, high, array, M);                  if(array[ M[2 * node] ] <= array[ M[2 * node + 1] ])	//拿到較小值的下標                 {                         M[node] = M[2 * node];                 }                 else                 {                         M[node] = M[2 * node + 1];                 }         } }
通過代碼,可得到構造線段樹的複雜度爲O(N)。
 
       線段樹構造成功,接下來就是查詢了。我們知道,線段樹查詢所需的時間爲O(LogN)。因爲我們在前面已經瞭解了線段樹的幾種操作,所以查詢在這就不贅述了,直接看代碼吧!
int query(int node, int low, int high, int *a, int *b, int i, int j) { /***node:表示線段樹中的某個節點 ****low	:表示低索引 ****high:表示高索引 ****array:表示原數組 ****M:	表示維護下標的數組 ****i, j:表示要查詢的區間 ***/ 	int	s, t; 	if(i > high || j < low) 		return -1;  	if(low >= i && high <= j) 		return b[node];	//返回最小值的下標  	s = query(2 * node, low, (low + high)/2, a, b, i, j); 	t = query(2 * node + 1, (low + high)/2 + 1, high, a, b, i, j);  	if(s == -1) 		return b[node] = t; 	if(t == -1) 		return b[node] = s;  	if(a[s] <= a[t]) 		return b[node] = s; 	else 		return b[node] = t; }


第三節 LCA Algorithm

 
       LCA算法的概念我們已經知道了,那我們就來看看它的實現過程吧!
 
       對於一棵樹,在這我用二叉樹,如下圖所示。我們要找節點8和節點9的最近公共祖先,即節點2。
 
       附註:有些朋友說這個問題可以當做兩條鏈表是否相交的問題來解決,我們只需分別得到兩個節點到根節點的路徑,而這兩條路徑就是兩條鏈表,問題就迎刃而解了。顯然這是可行的。
 

 

戰前準備:

       數組T[i]:表示樹中某個節點i的父節點;

       數組L[i]:表示樹中的某個節點i。

       維護數組:P[N][LogN]:其中,P[i][j]表示樹中i節點的第j個祖先。

 

實現的過程如下:

 

利用二分檢索判斷節點p和節點q是否在樹的同一層:

       如果在同一層,那麼我們通過DP思想,不斷地求LCA(p = P[p][j]q = P[q][j]),一旦 p = q就停止,因爲此時pq的父節點是一樣的,也就是說我們找到了最近公共祖先。

    如果不在同一層,如果p > q,也就是說p相對與qp在樹的更深層。此時,我們仍然通過DP思想來找到qp的祖先在同一層的節點,即q = p_祖先。接下來就可按照在同一層的做法做了。

 

實現就是這麼簡單。

首先是預處理得到維護數組P[N][LogN]:

void preprocessing(int *t, int n, int p[][max]) { 	int	i, j;  	for(i = 0; i < n; i++) 		p[i][0] = t[i];  	for(j = 1; (1 << 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];         }     } }

接下來就是查詢了,如下:

int query(int *t, int *l, int s, int t, int n, int p[][max]) { 	int	tmp, lg, i; 	if(l[s] < l[t]) 	{ 		tmp = s;s = t;t = tmp; 	}  	for(lg = 1; (1 << lg) <= l[s]; lg++);  	for(i = lg; i >= 0; i--) 	{ 		if((l[s] - (1 << i)) >= l[t]) 			s = p[s][i]; 	}  	if(s == t) 		return s;  	for(i = lg; i >= 0; i--) 	{ 		if(p[s][i] != -1 && p[s][i] != p[t][i]) 		{ 			s = p[s][i]; 			t = p[t][i]; 		} 	} 	return t[s]; }

       上面說的LCA的這種算法應該是最容易想到的,預處理過程O(NLogN),查詢O(LogN)。還有一種類似於RMQ分割法德算法,我先就不在這贅述了,以後有時間一定補上。

 

第四節 結束語

 

       想想、寫寫、畫畫.......

 

後續:本文後半部分拖得週期較長,因此寫的比較匆忙。如果本文的內容有任何不妥之處,請指正!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章