生活随笔
收集整理的這篇文章主要介紹了
LCA总结
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
文章目錄 LCA介紹 解決方法概括: 倍增法: Tarjan 歐拉序 樹剖解法:
看了很多關于LCA的文章,這里是一個總結
我們通過這個題洛谷P3379 【模板】最近公共祖先來講LCA
LCA介紹
lca是啥?最近公共祖先 就是:兩個點在這棵樹上距離最近的公共祖先節點 LCA(x,y)=z,z是x與y的最近公共祖先,(換句話說z的深度要盡可能大) 來看一個經典圖 LCA(4,5)=2 LCA(4,3)=1 LCA(2,1)=1
解決方法概括:
常用四種方法 :
用倍增法求解,預處理復雜度是 O(nlogn) ,每次詢問的復雜度是 O(logn), 屬于在線解法。 利用歐拉序轉化為RMQ問題,用ST表求解RMQ問題,預處理復雜度 O(n+nlogn),每次詢問的復雜度為 O(1),也是在線算法。 采用Tarjan算法求解,復雜度 O(α(n)+Q),屬于離線算法。 利用樹鏈剖分求解,復雜度預處理O(n),單次查詢 O(logn) ,屬于在線算法。
倍增法:
倍增:將兩個點調到一個高度之后不斷同時向上調到兩點重合,即為兩點的最近公共祖先 所用到的函數: grand[x][i] ,這個數組表示標號為x節點向上跳2^i步的節點 例如grand[5][0]=2(上圖), 節點5向上跳20 次(1次)到達節點2 grand[5][0]就是x的父節點 grand[x][1]就是x的父親節點的父親節點,就是grand[grand[x][0]][0] 這樣就能得到一個遞推式grand [ x ] [ i ] = grand [ grand [ x ] [ i-1 ] ] [ i-1 ] 先讓x與y處于同一層,然后一起往上跳 跳多少呢? 比如dep[u]>dep[v] u要向上爬h=dep[u]-dep[v],才能和v相同深度 將h進行二進制拆分,比如 h=(15)10 =(1111)2 h=(5)10 =(101)2 從低位開始i=0,如果是這一位是1,就grand[u][i] 任何調動次數都可以用2的指數冪之和來表示O(log n) h = 5 = 22 + 20 h = 15 = 23 + 22 + 21 + 20 dep[]表示節點的深度 一開始跳log2 dep[x]-dep[y] ,log我們可以打表預處理。
lg
[ 0 ] = - 1 ;
for ( int i
= 1 ; i
<= n
; i
++ ) lg
[ i
] = lg
[ i
>> 1 ] + 1 ;
當兩點匯合時,就可以返回了 查詢m組,總的復雜度應該是O(m log n) 輸入:
9 1 1
1 2
1 3
2 4
2 5
4 7
4 8
3 6
6 9
5 8
2
# include <bits/stdc++.h>
# define maxn 500005
using namespace std
;
int n
, m
, root
;
int read ( )
{ int x
= 0 , f
= 1 , ch
= getchar ( ) ; while ( ! isdigit ( ch
) ) { if ( ch
== '-' ) f
= - 1 ; ch
= getchar ( ) ; } ; while ( isdigit ( ch
) ) x
= ( x
<< 3 ) + ( x
<< 1 ) + ch
- '0' , ch
= getchar ( ) ; return x
* f
;
} struct edge
{ int to
, nxt
; edge ( ) { } edge ( int tt
, int nn
) { to
= tt
, nxt
= nn
; }
} e
[ maxn
<< 1 ] ; int head
[ maxn
] , k
= 0 ;
void add ( int u
, int v
)
{ e
[ k
] = edge ( v
, head
[ u
] ) ; head
[ u
] = k
++ ;
} int fa
[ maxn
] [ 25 ] , dep
[ maxn
] , lg
[ maxn
] ;
void dfs ( int now
, int fath
)
{ dep
[ now
] = dep
[ fath
] + 1 ; fa
[ now
] [ 0 ] = fath
; for ( int i
= 1 ; ( 1 << i
) <= dep
[ now
] ; i
++ ) fa
[ now
] [ i
] = fa
[ fa
[ now
] [ i
- 1 ] ] [ i
- 1 ] ; for ( int i
= head
[ now
] ; ~ i
; i
= e
[ i
] . nxt
) if ( e
[ i
] . to
!= fath
) dfs ( e
[ i
] . to
, now
) ;
} int lca ( int x
, int y
)
{ if ( dep
[ x
] < dep
[ y
] ) swap ( x
, y
) ; while ( dep
[ x
] > dep
[ y
] ) x
= fa
[ x
] [ lg
[ dep
[ x
] - dep
[ y
] ] ] ; if ( x
== y
) return x
; for ( int i
= lg
[ dep
[ x
] ] ; i
>= 0 ; i
-- ) if ( fa
[ x
] [ i
] != fa
[ y
] [ i
] ) x
= fa
[ x
] [ i
] , y
= fa
[ y
] [ i
] ; return fa
[ x
] [ 0 ] ;
} int main ( )
{ memset ( head
, - 1 , sizeof head
) ; n
= read ( ) , m
= read ( ) , root
= read ( ) ; register int u
, v
; for ( int i
= 1 ; i
< n
; i
++ ) { u
= read ( ) , v
= read ( ) ; add ( u
, v
) ; add ( v
, u
) ; } dfs ( root
, 0 ) ; lg
[ 0 ] = - 1 ;
for ( int i
= 1 ; i
<= n
; i
++ ) lg
[ i
] = lg
[ i
>> 1 ] + 1 ;
for ( int i
= 1 ; i
<= m
; i
++ ) { u
= read ( ) , v
= read ( ) ; printf ( "%d\n" , lca ( u
, v
) ) ; } return 0 ;
}
Tarjan
倍增是在線算法 Tarjan是離線算法 Tarjan主要用到和并查集差不多的方法 比如查詢7和5 我們從根節點出發,1->2->4->7,發現另一點5還沒被查詢,然后回溯,再遍歷,7->4->8->4->2->5,遍歷到點5,我們發現另外一個點7已經訪問過,就到此結束。在這過程中我們用fa[]來表示父節點,就和并查集一樣,回溯時,7->4 , fa[7]=4 ;8->4 , fa[8]=4; 4->2,fa[4]=2; 5->2,fa[5]=2。 這樣當發現另外一個點已經標記了,那么這個點的祖先一定是兩個點的lca(可以通過路徑壓縮) 在訪問一個點時,我們會將與這點相關的一同詢問,所以tarjan是強制離線 lca用于存結果,每個詢問都存了兩次(因為還要查詢另外一個點是否已經訪問過),最后輸出時 i * 2 每組答案lca[i*2](i->m) 大概時間復雜度為O(n+m)
# include <bits/stdc++.h>
# define maxn 500005
using namespace std
;
int n
, m
, root
, lca
[ maxn
<< 1 ] ;
int read ( )
{ int x
= 0 , f
= 1 , ch
= getchar ( ) ; while ( ! isdigit ( ch
) ) { if ( ch
== '-' ) f
= - 1 ; ch
= getchar ( ) ; } while ( isdigit ( ch
) ) x
= ( x
<< 1 ) + ( x
<< 3 ) + ch
- '0' , ch
= getchar ( ) ; return x
* f
;
} struct edge
{ int to
, nxt
; edge ( ) { } edge ( int tt
, int nn
) { to
= tt
, nxt
= nn
; }
} e
[ maxn
<< 1 ] , qe
[ maxn
<< 1 ] ; int head
[ maxn
] , k
= 0 ;
void add ( int u
, int v
)
{ e
[ k
] = edge ( v
, head
[ u
] ) ; head
[ u
] = k
++ ;
} int qhead
[ maxn
] , qk
= 0 ;
void qadd ( int u
, int v
)
{ qe
[ qk
] = edge ( v
, qhead
[ u
] ) ; qhead
[ u
] = qk
++ ;
} int fa
[ maxn
] ;
int get ( int x
) { return fa
[ x
] == x
? x
: fa
[ x
] = get ( fa
[ x
] ) ; } bool vis
[ maxn
] ;
void tarjan ( int u
)
{ register int v
; vis
[ u
] = 1 ; for ( int i
= head
[ u
] ; ~ i
; i
= e
[ i
] . nxt
) { v
= e
[ i
] . to
; if ( vis
[ v
] ) continue ; tarjan ( v
) ; fa
[ v
] = u
; } for ( int i
= qhead
[ u
] ; ~ i
; i
= qe
[ i
] . nxt
) { v
= qe
[ i
] . to
; if ( vis
[ v
] ) { lca
[ i
] = get ( v
) ; if ( i
& 1 ) lca
[ i
- 1 ] = lca
[ i
] ; else lca
[ i
+ 1 ] = lca
[ i
] ; } }
} int main ( )
{ memset ( head
, - 1 , sizeof head
) ; memset ( qhead
, - 1 , sizeof qhead
) ; n
= read ( ) , m
= read ( ) , root
= read ( ) ; register int u
, v
; for ( int i
= 1 ; i
< n
; i
++ ) { u
= read ( ) , v
= read ( ) ; add ( u
, v
) ; add ( v
, u
) ; fa
[ i
] = i
; } fa
[ n
] = n
; for ( int i
= 1 ; i
<= m
; i
++ ) { u
= read ( ) , v
= read ( ) ; qadd ( u
, v
) ; qadd ( v
, u
) ; } tarjan ( root
) ; for ( int i
= 0 ; i
< m
; i
++ ) printf ( "%d\n" , lca
[ i
<< 1 ] ) ;
}
m較小時用倍增,較大時用Tarjan
歐拉序
歐拉序的定義 樹在dfs過程中的節點訪問順序稱為歐拉序. 那有人會問:dfs序和歐拉序啥區別?
dfs序:是指將一棵樹被dfs時所經過的節點順序(不繞回原點)。 歐拉序:就是從根結點出發,按dfs的順序在繞回原點所經過所有點的順序。
歐拉序與dfs序不同地方在于,歐拉序中每個節點可以出現多次,比如進入一次退出一次,又比如每次回溯時記錄一次。
因此兩個點的LCA,就是在該序列上兩個點第一次出現的區間內深度最小的那個點 比如求D和E的LCA,D和E第一次出現的區間是DDCBE。這里面深度最小的就是點B,所以LCA(D,E)=B 這樣就轉化為區間RMQ問題,所以可以用ST表。 具體怎么做呢? 先求出歐拉序和每個節點的深度,同時用start[]記錄每個節點第一次出現的位置 LCA ( T , u , v ) = RMQ ( B , start ( u ) , start ( v ) ) 然后直接用ST表求RMQ就行 ST表詳解
所用到數組:
a.歐拉序:圖的遍歷(幾種存儲結構寫法不太一樣)
cnt:序列長度(每個元素一進一出共兩次,記得最大初始化為2*MAXN) oula[]:歐拉序列,記錄編號 dfs前記錄一次,dfs后(回溯)再記錄一次 depth[]:每個編號的深度(也可以記錄每個下標的深度,見注釋)
start[]:每個編號第一次出現的序列下標
b.ST表
minl[i][j] 記得第一層初始化為depth[]
pos[][]最值下標,第一層初始化為i
注意這里i是歐拉序列的下標,最終要的是編號(這里經常下標搞混!!)
歐拉序列下標=start【編號】
# pragma GCC optimize ( 3 )
# include <bits/stdc++.h>
using namespace std
;
# define N 500010
vector
< int > G
[ N
] ;
int oula
[ N
<< 1 ] , depth
[ N
] , start
[ N
] ;
int cnt
; int minl
[ N
<< 1 ] [ 19 ] , pos
[ N
<< 1 ] [ 19 ] , len
[ N
<< 1 ] , vis
[ N
] , tmp
;
int n
, m
, s
, x
, y
; inline int read ( )
{ char ch
= '*' ; while ( ! isdigit ( ch
= getchar ( ) ) ) ; int num
= ch
- '0' ; while ( isdigit ( ch
= getchar ( ) ) ) num
= num
* 10 + ch
- '0' ; return num
;
} inline void dfs ( int now
, int fa
, int deep
) { oula
[ ++ cnt
] = now
; if ( depth
[ now
] == 0 ) depth
[ now
] = deep
; if ( start
[ now
] == 0 ) start
[ now
] = cnt
; int z
= G
[ now
] . size ( ) ; for ( int i
= 0 ; i
< z
; i
++ ) { if ( G
[ now
] [ i
] != fa
) { dfs ( G
[ now
] [ i
] , now
, deep
+ 1 ) ; oula
[ ++ cnt
] = now
; } }
} inline void S_table ( ) { for ( int i
= 1 ; i
< 2 * N
; ++ i
) len
[ i
] = ( 1 << ( tmp
+ 1 ) ) == i
? ++ tmp
: tmp
; for ( int i
= 1 ; i
<= cnt
; i
++ ) minl
[ i
] [ 0 ] = depth
[ oula
[ i
] ] , pos
[ i
] [ 0 ] = i
; for ( int j
= 1 ; ( 1 << j
) <= cnt
; j
++ ) { for ( int i
= 1 ; i
+ ( 1 << ( j
- 1 ) ) - 1 <= cnt
; ++ i
) { if ( minl
[ i
] [ j
- 1 ] < minl
[ i
+ ( 1 << ( j
- 1 ) ) ] [ j
- 1 ] ) minl
[ i
] [ j
] = minl
[ i
] [ j
- 1 ] , pos
[ i
] [ j
] = pos
[ i
] [ j
- 1 ] ; else minl
[ i
] [ j
] = minl
[ i
+ ( 1 << ( j
- 1 ) ) ] [ j
- 1 ] , pos
[ i
] [ j
] = pos
[ i
+ ( 1 << ( j
- 1 ) ) ] [ j
- 1 ] ; } }
} inline int rmq ( int l
, int r
) { if ( l
> r
) swap ( l
, r
) ; int k
= len
[ r
- l
+ 1 ] ; int mid
= r
- ( 1 << k
) + 1 ; if ( minl
[ l
] [ k
] <= minl
[ mid
] [ k
] ) return pos
[ l
] [ k
] ; else return pos
[ mid
] [ k
] ;
}
int main ( )
{ n
= read ( ) ; m
= read ( ) ; s
= read ( ) ; for ( int i
= 1 ; i
< n
; ++ i
) { x
= read ( ) ; y
= read ( ) ; G
[ x
] . push_back ( y
) ; G
[ y
] . push_back ( x
) ; } dfs ( s
, - 1 , 1 ) ; S_table ( ) ; for ( int i
= 1 ; i
<= m
; ++ i
) { x
= read ( ) ; y
= read ( ) ; printf ( "%d\n" , oula
[ rmq ( start
[ x
] , start
[ y
] ) ] ) ; } return 0 ;
}
樹剖解法:
樹剖比Tarjan慢,但比倍增快
樹剖詳講
樹剖是把一棵樹按子樹大小分為鏈。樹剖基本操作中有一個是求x到y的路徑的邊權和,或者是所有邊權進行修改。我們可以用樹剖的思路來寫LCA。直接看點x和y是否在一條鏈上,不在則深度較大者跳到鏈頭的父親節點處,也就是跳出這條鏈;在則深度較淺者為LCA。 樹剖一跳就是一條鏈,對于n極大的情況就相當于是倍增的再一優化。
# include <bits/stdc++.h>
# define maxn 500005
using namespace std
;
int n
, m
, root
;
int read ( )
{ int x
= 0 , f
= 1 , ch
= getchar ( ) ; while ( ! isdigit ( ch
) ) { if ( ch
== '-' ) f
= - 1 ; ch
= getchar ( ) ; } while ( isdigit ( ch
) ) x
= ( x
<< 1 ) + ( x
<< 3 ) + ch
- '0' , ch
= getchar ( ) ; return x
* f
;
}
struct edge
{ int to
, nxt
; edge ( ) { } edge ( int tt
, int nn
) { to
= tt
, nxt
= nn
; }
} e
[ maxn
<< 1 ] ; int k
= 0 , head
[ maxn
] ;
void add ( int u
, int v
)
{ e
[ k
] = edge ( v
, head
[ u
] ) ; head
[ u
] = k
++ ;
} int fa
[ maxn
] , dep
[ maxn
] , size
[ maxn
] , son
[ maxn
] ;
void dfs_getson ( int u
)
{ size
[ u
] = 1 ; for ( int i
= head
[ u
] ; ~ i
; i
= e
[ i
] . nxt
) { int v
= 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
; }
} int top
[ maxn
] ;
void dfs_rewrite ( int u
, int tp
)
{ top
[ u
] = tp
; if ( son
[ u
] ) dfs_rewrite ( son
[ u
] , tp
) ; for ( int i
= head
[ u
] ; ~ i
; i
= e
[ i
] . nxt
) { int v
= e
[ i
] . to
; if ( v
!= fa
[ u
] && v
!= son
[ u
] ) dfs_rewrite ( v
, v
) ; }
} void ask ( int x
, int y
)
{ while ( top
[ x
] != top
[ y
] ) { if ( dep
[ top
[ x
] ] > dep
[ top
[ y
] ] ) swap ( x
, y
) ; y
= fa
[ top
[ y
] ] ; } if ( dep
[ x
] > dep
[ y
] ) swap ( x
, y
) ; printf ( "%d\n" , x
) ;
} int main ( )
{ memset ( head
, - 1 , sizeof head
) ; n
= read ( ) , m
= read ( ) , root
= read ( ) ; int u
, v
; for ( int i
= 1 ; i
< n
; i
++ ) { u
= read ( ) , v
= read ( ) ; add ( u
, v
) ; add ( v
, u
) ; } dfs_getson ( root
) ; dfs_rewrite ( root
, root
) ; for ( int i
= 1 ; i
<= m
; i
++ ) { u
= read ( ) , v
= read ( ) ; ask ( u
, v
) ; } return 0 ;
}
總結
以上是生活随笔 為你收集整理的LCA总结 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。