生活随笔
收集整理的這篇文章主要介紹了
详解RMQ LCA
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
第一節?RMQ、LCA概述
?
?????? LCA:Lowest?Common?Ancestor,譯為最近公共祖先。其解釋就是說:在有根樹中,找出樹中任意兩個節點最近的公共祖先,或者說找到任意兩個節點離樹根最遠的公共祖先。
?
?????? RMQ:Range?Minimum?Query,譯為區間最小值查詢。其解釋就是說:對于含有N個元素的數列A,在數列中找到兩個指定索引之間的最小值及最小值的位置。
?
第二節?RMQ Algorithm
?
?????? 首先我們來看RQM算法,我將會根據預處理和查詢的速度介紹幾種解決該問題的方法。
???
??? 設有數組A[N],其表示如下:
??? 要求求得區間(2,7)的最小元素,如下圖所示:
解法一:直接遍歷區間
?
?????? 看到這個問題之后,我們最先想到的就是對區間的這些數進行一次遍歷,就可以找到區間的最值,因此查詢的時間為O(M)。但是,當數據量非常大并且查詢很頻繁時,直接遍歷序列的效果就不是那么理想了。因為每查詢一次就得對序列做一次遍歷,對于大數據量這顯然不能滿足要求了。不過對于小數據量,這種算法倒是不錯的選擇!??
???????查詢:O(M)。
算法的代碼如下:
[cpp]?view plaincopy
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[2,7]。為了得到區間[2,7]的最小值,我們需比較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,我們來實現我們的想法,代碼如下:
[cpp]?view plaincopy
快速排序?? 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]的值是從原序列A的i位置開始,連續2j?個元素的最小值的下標,如下所示:
?????? 那么,我們如何計算M[i][j]呢?
?????? 我們采用DP的思想將區間分成兩部分,即M[i][j?-?1]和M[i][2^(j?-?1)]?,F在我們只需比較這兩個子區間就可以得到M[i][j]了。比較規則如下:
于是乎,就可按照此寫出代碼:
[cpp]?view plaincopy
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問題,如需了解線段樹,請到此一游:
?????? 線段樹:http://en.wikipedia.org/wiki/Segment_tree
線段樹的構造口訣:
ok,我們根據口訣,并用上面的例子構造了線段樹,如下:
?????? 那么將線段樹應用到RMQ問題中,首先,維護一個有2^([logN]?+?1?+?1)?元素,名為M的數組,即M[2^([logN]?+?1?+?1)],我先來解釋一些數組M的意義:M[i]表示已劃分節點區間的最小值的位置(下標)。
?
??? 知道了這些,那我們就通過代碼來實現線段樹的構造,并通過節點所代表的值來計算得到數組M。代碼如下:
[cpp]?view plaincopy
void?init_tree(int?node,?int?low,?int?high,?int?*array,?int?*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)。因為我們在前面已經了解了線段樹的幾種操作,所以查詢在這就不贅述了,直接看代碼吧!
[cpp]?view plaincopy
int?query(int?node,?int?low,?int?high,?int?*a,?int?*b,?int?i,?int?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就停止,因為此時p和q的父節點是一樣的,也就是說我們找到了最近公共祖先。
????如果不在同一層,如果p?>?q,也就是說p相對與q,p在樹的更深層。此時,我們仍然通過DP思想來找到q與p的祖先在同一層的節點,即q?=?p_祖先。接下來就可按照在同一層的做法做了。
?
實現就是這么簡單。
首先是預處理得到維護數組P[N][LogN]:
[cpp]?view plaincopy
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];?? ????????}?? ????}?? }??
接下來就是查詢了,如下:
[cpp]?view plaincopy
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分割法德算法,我先就不在這贅述了,以后有時間一定補上。
?
第四節 結束語
?
?????? 想想、寫寫、畫畫.......
?
后續:本文后半部分拖得周期較長,因此寫的比較匆忙。如果本文的內容有任何不妥之處,請指正!
總結
以上是生活随笔為你收集整理的详解RMQ LCA的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。