与图论的邂逅05:最近公共祖先LCA
生活随笔
收集整理的這篇文章主要介紹了
与图论的邂逅05:最近公共祖先LCA
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
什么是LCA?
祖先鏈
對于一棵樹T,若它的根節點是r,對于任意一個樹上的節點x,從r走到x的路徑是唯一的(顯然),那么這條路徑上的點都是并且只有這些點是x的祖先。這些點組成的鏈(或者說路徑)就是x的祖先鏈。
LCA
根據名字來說,最近公共祖先就是兩個點最近的相同祖先。實際上也可以理解為:兩個點的祖先鏈深度最大的那個交點。極端的情況下,LCA可以就是兩個點之一,或者就是根節點root。
順便貼下eg:
?
樹中節點8和7的LCA為3,節點4和7的LCA為1,節點5和2的LCA為2。
*可以寫成LCA(u,v)=w的形式
LCA的求法
問題:給出一棵有n(n≤500000)個節點的樹,并給出m(m≤500000)個詢問,每個詢問給出兩個點u,v,請你求出每個u,v的LCA。
1.向上標記法
首先根據定義來。LCA(u,v)是兩個點的祖先鏈的第一個交點(從下往上第一個)。那么我們可以先從u開始往根節點走,那么走到的點都是u的祖先,走出的路徑就是u的祖先鏈。那么我們在走的時候把祖先鏈上的點都標記一下,再從v開始往根節點走,走到的第一個被標記過的點就是LCA了。最壞的情況下時間復雜度為O(n),總共就是O(n*m)。顯然不能滿足題目的需要~
2.樹上倍增法
一個一個地跳顯然太慢,不如加加速?怎么加速呢?根據一貫套路,慢了就倍增~為什么這里不倍增呢?那就倍增一下咯~
樹上倍增的思路不同于標記的思路,因為倍增的時候是無法標記的。反正跳得快,就干脆讓u,v都跳一跳,跳到同一個點時就到了LCA了。不過不是一上來就一起跳的,樹上倍增的預處理可是很浩大的。
首先預處理出倍增之后跳到的點是誰。設f(i,j)為節點i往上跳2^j步到達的節點,那么
f(i,j)=f(f(i,j-1),j-1)?
初始化f(i,0)=fa(i),即i的父親。(剛才那里真的是零......)
設d(i)為i的深度,順便把這貨也處理出來(這么簡單就不講了)。
弄了這么多之后還不能直接兩個點倍增。首先需要將深度大的那個點(假設是v)往上倍增到和u深度相同為止。基于二進制轉化的思想,任何樹都可以用二進制表示出來,所以絕對是可以倍增到同一深度的。
接著,兩個點同時倍增。經過若干次倍增之后,若fa(u)==fa(v),那么fa(u)就是LCA了。
放上預處理的代碼:
xxxxxxxxxx inline void bfs(){ ? ?q.push(s); ? ?d[s] = 1; ? ?while(!q.empty()){ ? ? ? ?int u = q.front(); q.pop(); ? ? ? ?for(int i = head[u]; ~i; i = e[i].next){ ? ? ? ? ? ?int v = e[i].to; ? ? ? ? ? ?if(d[v]) continue; ? ? ? ? ? ?d[v] = d[u] + 1,f[v][0] = u; ? ? ? ? ? ?for(int j = 1; j <= t; j++) f[v][j] = f[f[v][j - 1]][j - 1]; ? ? ? ? ? ?q.push(v); ? ? ? } ? } } ? //在main函數中 t = (int)(log(n) / log(2)) + 1; bfs();以及倍增的代碼:
xxxxxxxxxx inline int lca(int &u, int &v){ ? ?if(d[u] > d[v]) swap(u, v); ? ?for(int i = t; i >= 0; i--) if(d[f[v][i]] >= d[u]){ ? ? ? ?v = f[v][i]; ? } ? ?if(u == v) return u; ? ?for(int i = t; i >= 0; i--) if(f[u][i] != f[v][i]){ ? ? ? ?u = f[u][i]; ? ? ? ?v = f[v][i]; ? } ? ?return f[u][0]; }*一般數組會開成這樣:
xxxxxxxxxx int f[maxn][20];這樣一般就夠用了。
預處理的復雜度為O(nlogn),每個詢問的復雜度為O(logn),一共就是O((n+m)logn),可以跑過題目的數據。
3.Tarjan
不同于倍增,Tarjan是一種離線算法,并且很優秀很優秀(就是難寫)。考慮到剛接觸LCA的OIer可能難以理解Tarjan,這里我用幾種不同的方式來講。
本人的方式:你首先在心里構造出一棵樹來......標記祖先鏈的方式其實可以多個詢問一起進行。首先依照Tarjan的流程,遍歷這棵樹。假設現在遍歷到了點u,并且開始回溯,那么你可以想象在回溯的過程中實際上是從節點u往上拉出了一條祖先鏈。當拉鏈子拉到了一處分叉,并且分叉的另一邊還沒去過時,就停止拉鏈,往下走。假設這個過程中走到了節點v并開始回溯,并且詢問里有問u和v的LCA,那么在從v回溯的過程中相當于從v開始也拉上去了一條祖先鏈,會一直拉到之前的分叉處,此時u和v的祖先鏈便第一次相交,交于分叉處,這個分叉處便是LCA(u,v)。也就是說,當我們遍歷到一個點v,發現詢問里有LCA(u,v)這個詢問,并且從u已經拉出了祖先鏈,那么LCA(u,v)就是此時u的祖先鏈的頂端。其實我們不該只局限于這一個詢問,從宏觀上看,在遍歷的過程中我們其實從每個回溯過的點都拉出了一條祖先鏈來。如果你覺得拉的鏈子太多,我們可以剪掉一些:只留下詢問里涉及的點的祖先鏈。那么這些祖先鏈的交點就是遍歷過程中走到的那些分叉口,所以一次遍歷就可以求出所有詢問的LCA。
還是附個流程圖吧......
?
我們先在要求:LCA(9,10),LCA(9,5),LCA(8,7),LCA(9,8)。
然后我們從左到右遍歷下去,遍歷到了9號節點,便往回拉祖先鏈:
?
(箭頭的邊是祖先鏈)現在拉到了4號節點,發現有分叉,往下走到10號節點,發現詢問里有LCA(9,10)這個詢問,并且從9已經拉出了祖先鏈,那么LCA(9,10)就是此時9的祖先鏈的頂端:4號節點。(由于關于10的詢問都處理完了,10的祖先鏈就不畫了)此時繼續往上拉鏈:
?
此時發現又有一個分叉,便往分叉走,走到5的位置,并發現詢問里有LCA(9,5),那么LCA(9,5)就是此時9的祖先鏈的頂端:2號節點。(5的祖先鏈也不畫了)繼續往上拉鏈。
?
發現有分叉,往下走到8的位置,發現詢問里有LCA(9,8),那么LCA(9,8)就是此時9的祖先鏈的頂端:1號節點。接著從8往上拉祖先鏈:
?
發現有分叉,往下走到7的位置,發現詢問里有LCA(8,7),那么LCA(8,7)就是此時8的祖先鏈的頂端:3號節點。
這樣應該就可以理解了......整個遍歷的復雜度為O(n),加上回答詢問的復雜度總共也才O(n+m)。像上面題目里的數據范圍可以隨便跑。
下面是教練的方式:拉祖先鏈的過程可以想象成灌水......當你往下灌到底部時,水就會慢慢往上漲,當漲到一個分岔口時就會往另一邊流。其實也差不多啦~
附上存詢問的代碼(用鄰接表來存,方便查詢):
xxxxxxxxxx struct edge{ ? ?int to, next, lca; ? ?edge(){} ? ?edge(register const int &_to, register const int &_next){ ? ? ? ?to = _to,next = _next; ? } }qe[maxm << 1]; int qhead[maxn], qk; ? inline void qadd(register const int &u, register const int &v){ ? ?qe[qk] = edge(v, qhead[u]); ? ?qhead[u] = qk++; } ? //main函數中 ? ?memset(qhead, -1, sizeof qhead); ? ?for(register int i = 1; i <= m; i++){ ? ? ? ?scanf("%d%d", &u, &v); ? ? ? ?qadd(u, v),qadd(v, u); ? }然后是Tarjan函數:
xxxxxxxxxx inline int find(register const int &x){ ? ?if(fa[x] == x) return x; ? ?return fa[x] = find(fa[x]); }//用并查集的方式查找祖先鏈的頂端 ? void LCA_tarjan(int u, int pre){ ? ?vis[u] = true; ? ?for(register int i = head[u]; ~i; i = e[i].next){ ? ? ? ?int v = e[i].to; ? ? ? ?if(v != pre){ ? ? ? ? ? ?LCA_tarjan(v, u); ? ? ? ? ? ?fa[v] = u;//拉祖先鏈 ? ? ? } ? } ? ? ? ?for(register int i = qhead[u]; ~i; i = qe[i].next){ ? ? ? ?v = qe[i].to; ? ? ? ?//如果v已經拉出了祖先鏈,就回答詢問 ? ? ? ?if(vis[v]) qe[i].lca = qe[i ^ 1].lca = find(v); ? } } ? //main函數中 ? ?for(register int i = 1; i <= n; i++) fa[i] = i; ? ?LCA_tarjan(s, 0);//s為根以及回答詢問:
xxxxxxxxxx ? ?for(int i = 0; i < qk; i += 2) printf("%d\n", qe[i].lca);考慮到并查集的存在,Tarjan的復雜度其實為:O(n+mlogn),只不過實際遠遠達不到這個程度而已。
只有倍增和Tarjan兩種算法可以跑LCA?
4.樹鏈剖分
*聲明:如果你不會樹鏈剖分你可以不看這一塊。
樹上倍增嫌慢?Tarjan嫌內存太大操作太麻煩?樹鏈剖分求LCA,你值得擁有!類似于樹上倍增的思想,只不過加快了往上跳的速度而已。樹鏈剖分的方法是,一條鏈子一條鏈子地往上跳!當跳到兩個點所在的鏈子為同一條時,淺的那個點就是LCA。
xxxxxxxxxx ? struct graph{ ? ?struct edge{ ? ? ? ?int to, next; ? ? ? ?edge(){} ? ? ? ?edge(const int &_to, const int &_next){ ? ? ? ? ? ?to = _to; ? ? ? ? ? ?next = _next; ? ? ? } ? }e[maxm << 1]; ? ?int head[maxn], k; ? ?inline void init(){ ? ? ? ?memset(head, -1, sizeof head); ? ? ? ?k = 0; ? } ? ?inline void add(const int &u, const int &v){ ? ? ? ?e[k] = edge(v, head[u]); ? ? ? ?head[u] = k++; ? } }g; ? int fa[maxn], son[maxn], size[maxn], dep[maxn]; int dfn[maxn], id[maxn], top[maxn], cnt[maxn], tot; int n, m, s; ? inline void swap(int &x, int &y){int t = x; x = y; y = t;} ? inline void dfs_getson(int u){ ? ?size[u] = 1; ? ?for(int i = g.head[u]; ~i; i = g.e[i].next){ ? ? ? ?int v = g.e[i].to; ? ? ? ?if(v == fa[u]) continue; ? ? ? ?dep[v] = dep[u] + 1; ? ? ? ?fa[v] = u; ? ? ? ?dfs_getson(v); ? ? ? ?size[u] += size[v]; ? ? ? ?if(size[v] > size[son[u]]) son[u] = v; ? } } ? inline void dfs_rewrite(int u, int tp){ ? ?top[u] = tp; ? ?dfn[u] = ++tot; ? ?id[tot] = u; ? ?if(son[u]) dfs_rewrite(son[u], tp); ? ?for(int i = g.head[u]; ~i; i = g.e[i].next){ ? ? ? ?int v = g.e[i].to; ? ? ? ?if(v != son[u] && v != fa[u]) dfs_rewrite(v, v); ? } ? ?cnt[u] = tot; } ? inline int lca(int u, int v){ ? ?while(top[u] != top[v]){ ? ? ? ?if(dep[top[u]] > dep[top[v]]) swap(u, v); ? ? ? ?v = fa[top[v]]; ? } ? ?if(dep[u] > dep[v]) swap(u, v); ? ?return u; } ? int main(){ ? ?g.init(); ? ?scanf("%d%d%d", &n, &m, &s); ? ?for(int i = 1; i < n; i++){ ? ? ? ?int u, v; ? ? ? ?scanf("%d%d", &u, &v); ? ? ? ?g.add(u, v); ? ? ? ?g.add(v, u); ? } ? ?dfs_getson(s); ? ?dfs_rewrite(s, s); ? ? ? ?for(int i = 1; i <= n; i++){ ? ? ? ?int u, v; ? ? ? ?scanf("%d%d", &u, &v); ? ? ? ?printf("%d\n", lca(u, v)); ? } ? ? ? ?return 0; }#滑稽
經過親測,放出三種算法的成績:
樹上倍增——用時2287ms,內存50.66MB
Tarjan——用時1272ms,內存29.76MB
樹鏈剖分——用時1538ms,內存25.49MB
綜合一下時間和內存的開銷,Tarjan和樹剖完爆樹上倍增......不過樹剖的過程中可以很容易得實現更復雜的操作:區間修改、子樹修改之類的,個人認為樹剖比Tarjan更優。
加上常數優化的樹剖:用時1102ms,內存25.55MB
轉載于:https://www.cnblogs.com/akura/p/10808772.html
總結
以上是生活随笔為你收集整理的与图论的邂逅05:最近公共祖先LCA的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 梦到做房子好不好
- 下一篇: 做梦梦到拆房子好不好