概況
CA(Lowest Common Ancestors),即最近公共祖先,是指在有根樹中,找出某兩個結點u和v最近的公共祖先。
基本介紹
則有:
實現
暴力/Tarjan/DFS+ST/倍增
-
如果當前結點t 大於結點u、v,說明u、v都在t 的左側,所以它們的共同祖先必定在t 的左子樹中,故從t 的左子樹中繼續查找;
-
如果當前結點t 小於結點u、v,說明u、v都在t 的右側,所以它們的共同祖先必定在t 的右子樹中,故從t 的右子樹中繼續查找;
-
如果當前結點t 滿足 u <t < v,說明u和v分居在t 的兩側,故當前結點t 即為最近公共祖先;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
int
query(Node t, Node u, Node v) {
int
left = u.value;
int
right = v.value;
//二叉查找樹內,如果左結點大於右結點,不對,交換
if
(left > right) {
int
temp = left;
left = right;
right = temp;
}
while
(
true
) {
//如果t小於u、v,往t的右子樹中查找
if
(t.value < left)
t = t.right;
//如果t大於u、v,往t的左子樹中查找
else
if
(t.value > right)
t = t.left;
else
return
t.value;
}
}
|

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
int
tot, seq[N << 1], pos[N << 1], dep[N << 1];
// dfs過程,預處理深度dep、dfs序數組seq
void
dfs(
int
now,
int
fa,
int
d) {
pos[now] = ++tot, seq[tot] = now, dep[tot] = d;
for
(
int
i = head[now]; i; i = e[i].next) {
int
v = e[i].to;
if
(v == fa)
continue
;
dfs(v, now, d + 1);
seq[++tot] = now, dep[tot] = d;
}
}
int
anc[N << 1][20];
// anc[i][j]表示i節點向上跳2^j層對應的節點
void
init(
int
len) {
for
(
int
i = 1; i <= len; i++)
anc[i][0] = i;
for
(
int
k = 1; (1 << k) <= len; k++)
for
(
int
i = 1; i + (1 << k) - 1 <= len; i++)
if
(dep[anc[i][k - 1]] < dep[anc[i + (1 << (k - 1))][k - 1]])
anc[i][k] = anc[i][k - 1];
else
anc[i][k] = anc[i + (1 << (k - 1))][k - 1];
}
int
rmq(
int
l,
int
r) {
int
k =
log
(r - l + 1) /
log
(2);
return
dep[anc[l][k]] < dep[anc[r + 1 - (1 << k)][k]] ? anc[l][k] : anc[r + 1 - (1 << k)][k];
}
int
calc(
int
x,
int
y) {
x = pos[x], y = pos[y];
if
(x > y) swap(x, y);
return
seq[rmq(x, y)];
}
int
lca(
int
a,
int
b) {
dfs(root, 0, 1);
// root為樹根節點的編號
init(0);
return
calc(a, b);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
void
dfs(
int
u) {
for
(
int
i=head[u]; i!=-1; i=edge[i].next) {
int
to=edge[i].to;
if
(to==p[u][0])
continue
;
d[to]=d[u]+1;
dist[to]=dist[u]+edge[i].w;
p[to][0]=u;
//p[i][0]存i的父節點
dfs(to);
}
}
void
init()
//i的2^j祖先就是i的(2^(j-1))祖先的2^(j-1)祖先
{
for
(
int
j=1; (1<<j)<=n; j++)
for
(
int
i=1; i<=n; i++)
p[i][j]=p[p[i][j-1]][j-1];
}
int
lca(
int
a,
int
b) {
if
(d[a]>d[b])swap(a,b);
//b在下面
int
f=d[b]-d[a];
//f是高度差
for
(
int
i=0; (1<<i)<=f; i++)
//(1<<i)&f找到f化為2進制后1的位置,移動到相應的位置
if
((1<<i)&f)b=p[b][i];
//比如f=5,二進制就是101,所以首先移動2^0祖先,然后再移動2^2祖先
if
(a!=b) {
for
(
int
i=(
int
)log2(N); i>=0; i--)
if
(p[a][i]!=p[b][i])
//從最大祖先開始,判斷a,b祖先,是否相同
a=p[a][i], b=p[b][i];
//如不相同,a b同時向上移動2^j
a=p[a][0];
//這時a的father就是LCA
}
return
a;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
|
const
int
mx = 10000;
//最大頂點數
int
n, root;
//實際頂點個數,樹根節點
int
indeg[mx];
//頂點入度,用來判斷樹根
vector<
int
> tree[mx];
//樹的鄰接表(不一定是二叉樹)
void
inputTree()
//輸入樹
{
scanf
(
"%d"
, &n);
//樹的頂點數
for
(
int
i = 0; i < n; i++)
//初始化樹,頂點編號從0開始
tree[i].clear(), indeg[i] = 0;
for
(
int
i = 1; i < n; i++)
//輸入n-1條樹邊
{
int
x, y;
scanf
(
"%d%d"
, &x, &y);
//x->y有一條邊
tree[x].push_back(y);
indeg[y]++;
//加入鄰接表,y入度加一
}
for
(
int
i = 0; i < n; i++)
//尋找樹根,入度為0的頂點
if
(indeg[i] == 0)
{
root = i;
break
;
}
}
vector<
int
> query[mx];
//所有查詢的內容
void
inputQuires()
//輸入查詢
{
for
(
int
i = 0; i < n; i++)
//清空上次查詢
query[i].clear();
int
m;
scanf
(
"%d"
, &m);
//查詢個數
while
(m--)
{
int
u, v;
scanf
(
"%d%d"
, &u, &v);
//查詢u和v的LCA
query[u].push_back(v);
query[v].push_back(u);
}
}
int
father[mx], rnk[mx];
//節點的父親、秩
void
makeSet()
//初始化並查集
{
for
(
int
i = 0; i < n; i++) father[i] = i, rnk[i] = 0;
}
int
findSet(
int
x)
//查找
{
if
(x != father[x]) father[x] = findSet(father[x]);
return
father[x];
}
void
unionSet(
int
x,
int
y)
//合並
{
x = findSet(x), y = findSet(y);
if
(x == y)
return
;
if
(rnk[x] > rnk[y]) father[y] = x;
else
father[x] = y, rnk[y] += rnk[x] == rnk[y];
}
int
ancestor[mx];
//已訪問節點集合的祖先
bool
vs[mx];
//訪問標志
void
Tarjan(
int
x)
//Tarjan算法求解LCA
{
for
(
int
i = 0; i < tree[x].size(); i++)
{
Tarjan(tree[x][i]);
//訪問子樹
unionSet(x, tree[x][i]);
//將子樹節點與根節點x的集合合並
ancestor[findSet(x)] = x;
//合並后的集合的祖先為x
}
vs[x] = 1;
//標記為已訪問
for
(
int
i = 0; i < query[x].size(); i++)
//與根節點x有關的查詢
if
(vs[query[x][i]])
//如果查詢的另一個節點已訪問,則輸出結果
printf
(
"%d和%d的最近公共祖先為:%d\n"
, x,
query[x][i], ancestor[findSet(query[x][i])]);
}
int
main()
{
inputTree();
//輸入樹
inputQuires();
//輸入查詢
makeSet();
for
(
int
i = 0; i < n; i++)
ancestor[i] = i;
memset
(vs, 0,
sizeof
(vs));
//初始化為未訪問
Tarjan(root);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
const
int
N=500004;
int
head[N*2],next[N*2],to[N*2];
// 樹的鄰接表
int
deep[N],fa[N];
// deep表示節點深度,fa表示節點的父親
int
size[N],son[N],top[N];
// size表示節點所在的子樹的節點總數
// son表示節點的重孩子
// top表示節點所在的重鏈的頂部節點
inline
void
add(
int
u,
int
v,
int
tnt)
// 鄰接表加邊
{
nt[tnt]=ft[u];
ft[u]=tnt;
ed[tnt]=v;
}
void
DFS(
int
u,
int
Fa)
// 第一遍dfs,處理出deep,size,fa,son
{
size[u]=1;
for
(
int
i=head[u];i;i=next[i])
{
if
(to[i]==Fa)
continue
;
deep[to[i]]=d[u]+1;
fa[to[i]]=u;
DFS(to[i],u);
size[u]+=size[to[i]];
if
(size[to[i]]>size[son[u]])
son[u]=to[i];
}
}
void
Dfs(
int
u)
// 第二遍dfs,將所有相鄰的重邊連成重鏈
{
if
(u==son[fa[u]])
top[u]=top[fa[u]];
else
top[u]=u;
for
(
int
i=head[u];i;i=next[i])
if
(to[i]!=fa[u])
Dfs(to[i]);
}
int
LCA(
int
u,
int
v)
// 處理LCA
{
while
(top[u]!=top[v])
// 如果u,v不在同一條重鏈上
{
if
(deep[top[u]]>deep[top[v]])
// 將深度大的節點上調
u=fa[top[u]];
else
v=fa[top[v]];
}
return
deep[u]>deep[v]?v:u;
// 返回深度小的節點(即為LCA(u,v))
}
|
下面詳細介紹一下Tarjan算法的基本思路:
1.任選一個點為根節點,從根節點開始。
2.遍歷該點u所有子節點v,並標記這些子節點v已被訪問過。
3.若是v還有子節點,返回2,否則下一步。
4.合並v到u上。
5.尋找與當前點u有詢問關系的點v。
6.若是v已經被訪問過了,則可以確認u和v的最近公共祖先為v被合並到的父親節點a。
遍歷的話需要用到dfs來遍歷(我相信來看的人都懂吧...),至於合並,最優化的方式就是利用並查集來合並兩個節點。
下面上偽代碼:
1 Tarjan(u)//marge和find為並查集合並函數和查找函數 2 { 3 for each(u,v) //訪問所有u子節點v 4 { 5 Tarjan(v); //繼續往下遍歷 6 marge(u,v); //合並v到u上 7 標記v被訪問過; 8 } 9 for each(u,e) //訪問所有和u有詢問關系的e 10 { 11 如果e被訪問過; 12 u,e的最近公共祖先為find(e); 13 } 14 }
個人感覺這樣還是有很多人不太理解,所以我打算模擬一遍給大家看。
建議拿着紙和筆跟着我的描述一起模擬!!
假設我們有一組數據 9個節點 8條邊 聯通情況如下:
1--2,1--3,2--4,2--5,3--6,5--7,5--8,7--9 即下圖所示的樹
設我們要查找最近公共祖先的點為9--8,4--6,7--5,5--3;
設f[]數組為並查集的父親節點數組,初始化f[i]=i,vis[]數組為是否訪問過的數組,初始為0;
下面開始模擬過程:
取1為根節點,往下搜索發現有兩個兒子2和3;
先搜2,發現2有兩個兒子4和5,先搜索4,發現4沒有子節點,則尋找與其有關系的點;
發現6與4有關系,但是vis[6]=0,即6還沒被搜過,所以不操作;
發現沒有和4有詢問關系的點了,返回此前一次搜索,更新vis[4]=1;
表示4已經被搜完,更新f[4]=2,繼續搜5,發現5有兩個兒子7和8;
先搜7,發現7有一個子節點9,搜索9,發現沒有子節點,尋找與其有關系的點;
發現8和9有關系,但是vis[8]=0,即8沒被搜到過,所以不操作;
發現沒有和9有詢問關系的點了,返回此前一次搜索,更新vis[9]=1;
表示9已經被搜完,更新f[9]=7,發現7沒有沒被搜過的子節點了,尋找與其有關系的點;
發現5和7有關系,但是vis[5]=0,所以不操作;
發現沒有和7有關系的點了,返回此前一次搜索,更新vis[7]=1;
表示7已經被搜完,更新f[7]=5,繼續搜8,發現8沒有子節點,則尋找與其有關系的點;
發現9與8有關系,此時vis[9]=1,則他們的最近公共祖先為find(9)=5;
(find(9)的順序為f[9]=7-->f[7]=5-->f[5]=5 return 5;)
發現沒有與8有關系的點了,返回此前一次搜索,更新vis[8]=1;
表示8已經被搜完,更新f[8]=5,發現5沒有沒搜過的子節點了,尋找與其有關系的點;
發現7和5有關系,此時vis[7]=1,所以他們的最近公共祖先為find(7)=5;
(find(7)的順序為f[7]=5-->f[5]=5 return 5;)
又發現5和3有關系,但是vis[3]=0,所以不操作,此時5的子節點全部搜完了;
返回此前一次搜索,更新vis[5]=1,表示5已經被搜完,更新f[5]=2;
發現2沒有未被搜完的子節點,尋找與其有關系的點;
又發現沒有和2有關系的點,則此前一次搜索,更新vis[2]=1;
表示2已經被搜完,更新f[2]=1,繼續搜3,發現3有一個子節點6;
搜索6,發現6沒有子節點,則尋找與6有關系的點,發現4和6有關系;
此時vis[4]=1,所以它們的最近公共祖先為find(4)=1;
(find(4)的順序為f[4]=2-->f[2]=2-->f[1]=1 return 1;)
發現沒有與6有關系的點了,返回此前一次搜索,更新vis[6]=1,表示6已經被搜完了;
更新f[6]=3,發現3沒有沒被搜過的子節點了,則尋找與3有關系的點;
發現5和3有關系,此時vis[5]=1,則它們的最近公共祖先為find(5)=1;
(find(5)的順序為f[5]=2-->f[2]=1-->f[1]=1 return 1;)
發現沒有和3有關系的點了,返回此前一次搜索,更新vis[3]=1;
更新f[3]=1,發現1沒有被搜過的子節點也沒有有關系的點,此時可以退出整個dfs了。