本文参考https://oi-wiki.org/graph/lca/
定义树上u,v两点的LCA(最近公共祖先)是从根节点dfs到上述两节点路径上距离上述两点最近的公共点。
LCA有如下性质:
1、u是v的祖先,当且仅当LCA(u,v)=u
2、d(u,v)=h(u)+h(v)-2h(LCA(U,V))。其中d为树上两点距离,h为某点到树根的距离。
LCA求解方法如下:
1、朴素算法:每次寻找深度较大节点控制其上跳,最终实现重合。这里需要预处理整棵树,获得深度数组。
代码如下:
1 #include <iostream>
2 using namespace std;
3 int h[100];//深度存储
4 int child[100];
5 int sib[100];
6 int parent[100];
7 void insert(int p,int self){
8 int p_c = child[p];
9 child[p]=self;
10 sib[self]=p_c;
11 parent[self]=p;
12 }//左孩子右兄弟存储
13 int l=1;
14 void dfs(int u){
15 h[u]=l;
16 for (int i=child[u];i;i=sib[i]){
17 l++;
18 dfs(i);
19 l--;
20 }
21 }//dfs遍历获取深度
22 int find(int u,int v){
23 int l_1 = h[u];
24 int l_2 = h[v];
25 while(u!=v){
26 if (l_1>l_2){
27 l_1--;
28 u=parent[u];
29 }
30 else if(l_1<l_2){
31 l_2--;
32 v=parent[v];
33 }
34 else{
35 u=parent[u];
36 v=parent[v];
37 }
38 }//当位于相同高度时就一起跳
39 return u;
40 }
41 int main(){
42 insert(4,2);
43 insert(4,1);
44 insert(1,3);
45 insert(1,5);
46 dfs(4);//4是根节点
47 cout<<find(3,2);
48 return 0;
49 }
2、倍增算法:对上述朴素算法的优化,把跳跃过程二进制化来减少跳跃次数。
示例代码如下:
1 #include <iostream>
2 #include <algorithm>
3 using namespace std;
4 int fa[100][100];
5 int child[100];
6 int sib[100];
7 int h[100];
8 void insert(int p,int self){
9 int p_c = child[p];
10 child[p]=self;
11 sib[self]=p_c;
12 }//左孩子右兄弟存储
13 void dfs(int u,int l){
14 h[u]=l;//存储深度
15 for (int i=1;i<=20;i++)
16 fa[u][i]=fa[fa[u][i-1]][i-1];
17 for (int i=child[u];i;i=sib[i]){
18 fa[i][0]=u;
19 dfs(i,l+1);
20 }
21 }
22 int find (int u,int v){
23 int l_1 = h[u];
24 int l_2 = h[v];
25 if (l_1<l_2)
26 swap(u,v);//保证u的深度大于等于v
27 for (int i=20;i>=0;i--){
28 if (h[fa[u][i]]>=h[v])//如果可以缩小深度就跳跃。同时为了简便我们选择从大到小跳
29 u=fa[u][i];
30 if (u==v)
31 return u;
32 }
33 for (int i=20;i>=0;i--){
34 if (fa[u][i]!=fa[v][i]){//相同深度后一起倍增跳
35 u=fa[u][i];
36 v=fa[v][i];
37 }
38 }
39 return fa[u][0];
40 }
41 int main(){
42 insert(4,2);
43 insert(4,1);
44 insert(1,3);
45 insert(1,5);
46 dfs(4,1);//4是根节点
47 cout<<find(1,5);
48 return 0;
49 }
3、Tarjan算法(求解LCA):这个算法作为离线算法,其根源还是利用dfs进行遍历,不同的是其利用并查集实现了信息的存储。
其大致思路如下:
此处以二叉形式为例,圆圈为根节点,三角为根节点下属子树。这里运用数学归纳法实现思路推导:假设tarjan(n)可以解决以n为根的树的LCA求解问题,于是我们分别对左右子树使用tarjan算法,并将完成tarjan算法遍历的子树通过并查集与根节点连接,如下图所示:
这里红色表示已经对子树使用tarjan算法,并且将子树与根通过并查集连接到了一起。这时,我们再对右子树重复上述过程。得到了左右子树都解决LCA问题的一个树。这个树可以实现两查找节点都在左子树或者都在右子树的LCA查找。现在考虑查找节点分别位于左右节点的情况。因为通过并查集将左右子树连接在一起,所以最终只需寻找其祖宗节点即可。
为了防止无法寻找到真正的LCA,这里采用visit数组来记录节点的访问情况。如果两节点其中一个未被访问,就证明我们还未获取另一节点的相对位置信息,暂时不需要对其使用tarjan算法。只有当两节点都被访问后我们才可以根据二者的相对位置信息来使用tarjan算法。
示例代码如下:
1 #include <iostream>
2 #include <cstring>
3 using namespace std;
4 typedef struct ask{
5 int u,v;
6 int ans;
7 }ask;
8 ask a[100];//询问
9 int parent[100];
10 int child[100];
11 int sib[100];
12 int visit[100];
13 void insert(int p,int s){
14 int p_c = child[p];
15 child[p]=s;
16 sib[s]=p_c;
17 }
18 void init(){
19 for (int i=0;i<100;i++)
20 parent[i]=i;
21 }
22 int find_root(int u){
23 if (parent[u]==u)
24 return u;
25 else
26 return find_root(parent[u]);
27 }
28 void Union(int u,int v){
29 int p_u = find_root(u);
30 int p_v = find_root(v);
31 parent[p_v]=p_u;//顺序不能反,保证是后面的以前面的为祖先节点
32 }
33 void tarjan(int u){
34 visit[u]=1;
35 for (int i=child[u];i;i=sib[i]){
36 tarjan(i);
37 Union(u,i);
38 }
39 int v_1 = a[0].u;
40 int v_2 = a[0].v;
41 if (visit[v_1]&&visit[v_2])
42 a[0].ans=find_root(v_1);
43 }
44 int main(){
45 init();
46 insert(1,2);
47 insert(1,3);
48 insert(2,4);
49 insert(2,5);
50 insert(3,6);
51 a[0].u=4;
52 a[0].v=3;
53 tarjan(1);
54 cout<<a[0].ans;
55 return 0;
56 }