與圖論的邂逅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
#include <stdio.h>
#include <string.h>
#define maxn 500010
#define maxm 500010
?
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

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/387746.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/387746.shtml
英文地址,請注明出處:http://en.pswp.cn/news/387746.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

MAC地址進行驗證的方法

需要對對應的MAC地址進行驗證的方法&#xff0c;以為很簡單就能過&#xff0c;鼓搗了半天以后才發現&#xff0c;我的機器是window7&#xff0c;查詢出來是亂碼&#xff0c;居然不給支持。沒辦法在網上繼續找資料。終于找到了&#xff0c;貼上來&#xff0c;以備不時之需。 東西…

JAVA 分布式環境 Redis互斥鎖

開始的時候項目沒有添加互斥鎖&#xff0c;用的依然是老的思路&#xff0c;在并發量增加的情況下&#xff0c;遇到了很多的問題&#xff0c;包括數據庫重復讀等&#xff0c;想了下考慮增加 互斥鎖來排序對單個資源的操作。 Target(ElementType.METHOD) Retention(RetentionPoli…

相機添加多張圖片css布局

<section class"feedback-upload"><aside class"photos"><div></div><div class"camera"></div></aside><aside class"tips"><div><span>選填0~4</span></div&…

移動端滑動操作學習

(function(window,document){var Slide function(box,judge,fun){if (!(this instanceof Slide)) return new Slide(box,judge,fun);var startx,starty;box.addEventListener("touchstart", function(e) {e.preventDefault(); // 阻止瀏覽器默認事件startx parseIn…

深入學習Oracle分區表及分區索引

關于分區表和分區索引(About Partitioned Tables and Indexes)對于10gR2而言&#xff0c;基本上可以分成幾類&#xff1a; ?    Range(范圍)分區 ?    Hash(哈希)分區 ?    List(列表)分區 ?    以及組合分區&#xff1a;Range-Hash,R…

跟隨我在oracle學習php(21)

變量間的傳值方式 總體說明&#xff1a; 1&#xff0c;這里討論的傳值方式是指&#xff1a;一個變量對另一個變量 2&#xff0c;它不僅僅適用于賦值語句&#xff0c;也適用于其他有同樣含義的語句&#xff0c;比如&#xff1a;函數的實參到形參 3&#xff0c;傳值方式只有2種&a…

分區索引常用命令

一般使用LOCAL索引較為方便&#xff0c;而且維護代價較低&#xff0c;并且LOCAL索引是在分區的基礎上去創建索引&#xff0c;類似于在一個子表內部去創建索引&#xff0c;這樣開銷主要是區分分區上&#xff0c;很規范的管理起來&#xff0c;在OLAP系統中應用很廣泛&#xff1b;…

面向對象簡述

1&#xff0c;封裝&#xff1a;將對象的屬性集成在 class person:def __init__(self,name,idnum):self.namenameself.idnumidnum 2&#xff0c;繼承&#xff1a;子類自動擁有父類的的封裝&#xff0c;除了非私有之外 class person: def __init__(self,name,idnum): self.namena…

== 和 is 的區別

1. 比較的是值 a2 b2 print(a b) # True lis1 [1,2,3] lis2 [1,2,3] print(lis1 lis2) # True 2.is 是比較的是內存地址 a name print(id(a)) # 內存地址 字符串 a name b name print(a is b) # True 數字 n 10 n110 print(n is n1) # True 小數據池 數字 -5~256 字…

oracle數據量大時候分區索引思路

有一個分區表&#xff0c;按list分區&#xff0c;只有一個本地唯一索引&#xff0c;沒有外鍵和觸發器 當單個分區數量在2000萬以內時&#xff0c;insert效率還可以&#xff0c;每秒2.3-2.5萬條 但數據量越大&#xff0c;速度越慢&#xff0c; 目前單個分區數量達到3億&#xff…

【轉】WPF自定義控件與樣式(3)-TextBox RichTextBox PasswordBox樣式、水印、Label標簽、功能擴展...

一&#xff0e;前言.預覽 申明&#xff1a;WPF自定義控件與樣式是一個系列文章&#xff0c;前后是有些關聯的&#xff0c;但大多是按照由簡到繁的順序逐步發布的等。 本文主要是對文本輸入控件進行樣式開發&#xff0c;及相關擴展功能開發&#xff0c;主要內容包括&#xff1a;…

JVM調優 dump文件怎么生成和分析

1、獲取JVM的dump文件的兩種方式   1. JVM啟動時增加兩個參數: #出現 OOME 時生成堆 dump: -XX:HeapDumpOnOutOfMemoryError #生成堆文件地址&#xff1a; -XX:HeapDumpPath/home/liuke/jvmlogs/ 2. 發現程序異常前通過執行指令&#xff0c;直接生成當前JVM的dmp文件&#x…

關于 Oracle 分區索引的失效和重建

--創建測試表 SQL> create table t as select object_id,object_name from dba_objects;表已創建。SQL> select min(object_id),max(object_id) from t;MIN(OBJECT_ID) MAX(OBJECT_ID)-------------- --------------2 76083SQL> create table t_part(object…

【網絡安全/CTF】unseping 江蘇工匠杯

該題考察序列化反序列化及Linux命令執行相關知識。 題目 <?php highlight_file(__FILE__);class ease{private $method;private $args;function __construct($method, $args) {$this->method $method;$this->args $args;}function __destruct(){if (in_array($thi…

yum配置中driver-class-name: com.mysql.jdbc.Driver報錯

錯誤&#xff1a; 原因&#xff1a; 解決方法&#xff1a;把方框中的<scope>runtime</scope>刪掉 轉載于:https://www.cnblogs.com/zly123/p/10834958.html

gitlab中的CI

https://blog.csdn.net/chengzi_comm/article/details/78778284 轉載于:https://www.cnblogs.com/effortsing/p/10142720.html

增加表空間大小的四種方法

增加表空間大小的四種方法Meathod1&#xff1a;給表空間增加數據文件ALTER TABLESPACE app_data ADD DATAFILED:\ORACLE\PRODUCT\10.2.0\ORADATA\EDWTEST\APP03.DBF SIZE 50M;Meathod2&#xff1a;新增數據文件&#xff0c;并且允許數據文件自動增長ALTER TABLESPACE app_data …

Red Hat 8.0中設置光盤為軟件源

為什么80%的碼農都做不了架構師&#xff1f;>>> 以管理員身份登錄 su 編輯設置軟件源的repo文件 gedit /etc/yum.repos.d/redhat.repo 粘貼如下文本至空白處&#xff1a; [InstallMedia] nameRed Hat Enterprise Linux 8.0.0 mediaidNone metadata_expire-1 gpgche…

C++11并發編程:多線程std::thread

一&#xff1a;概述 C11引入了thread類&#xff0c;大大降低了多線程使用的復雜度&#xff0c;原先使用多線程只能用系統的API&#xff0c;無法解決跨平臺問題&#xff0c;一套代碼平臺移植&#xff0c;對應多線程代碼也必須要修改。現在在C11中只需使用語言層面的thread可以解…

圖像特征提取——韋伯局部描述符(WLD)

一、原理及概述 韋伯局部描述符&#xff08;WLD&#xff09;是一種魯棒性好、簡單高效的局部特征描述符。WLD由兩個部分組成&#xff1a;差分激勵和梯度方向。 其具體算法是對于給定的一幅圖像&#xff0c;通過對每個像素進行這兩個分量的計算來提取其差分激勵圖像和梯度方向圖…