LCA算法总结
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
LCA算法总结
LCA问题(Least Common Ancestors,最近公共祖先问题),是指给定⼀棵有根树T,给出若⼲个查询LCA(u, v)(通常查询数量较⼤),每次求树T 中两个顶点u和v的最近公共祖先,即找⼀个节点,同时是u和v的祖先,并且深度尽可能⼤(尽可能远离树根)。
LCA问题有很多解法:线段树、Tarjan算法、跳表、RMQ与LCA互相转化等。
⼀ LCA问题
LCA问题的⼀般形式:给定⼀棵有根树,给出若⼲个查询,每个查询要求指定节点u和v的最近公共祖先。
LCA问题有两类解决思路:
在线算法,每次读⼊⼀个查询,处理这个查询,给出答案。
离线算法,⼀次性读⼊所有查询,统⼀进⾏处理,给出所有答案。
⼀个LCA的例⼦如下。
⽐如节点1和6的LCA为0。
⼆、Tarjan算法
Tarjan算法是离线算法,基于后序DFS(深度优先搜索)和并查集。
算法从根节点root开始搜索,每次递归搜索所有的⼦树,然后处理跟当前根节点相关的所有查询。
算法⽤集合表⽰⼀类节点,这些节点跟集合外的点的LCA都⼀样,并把这个LCA设为这个集合的祖先。
当搜索到节点x时,创建⼀个由x本⾝组成的集合,这个集合的祖先为x⾃⼰。
然后递归搜索x的所有⼉⼦节点。
当⼀个⼦节点搜索完毕时,把⼦节点的集合与x节点的集合合并,并把合并后的集合的祖先设为x。
因为这棵⼦树内的查询已经处理完,x的其他⼦树节点跟这棵⼦树节点的LCA都是⼀样的,都为当前根节点x。
所有⼦树处理完毕之后,处理当前根节点x相关的查询。
遍历x的所有查询,如果查询的另⼀个节点v已经访问过了,那么x和v的LCA即为v所在集合的祖先。
其中关于集合的操作都是使⽤并查集⾼效完成。
算法的复杂度为,O(n)搜索所有节点,搜索每个节点时会遍历这个节点相关的所有查询。
如果总的查询个数为m,则总的复杂度为O(n+m)。
⽐如上⾯的例⼦中,前⾯处理的节点的顺序为4->7->5->1->0->…。
当访问完4之后,集合{4}跟集合{1}合并,得到{1,4},并且集合祖先为1。
然后访问7。
如果(7,4)是⼀个查询,由于4已访问过,于是LCA(7,4)为4所在集合{1,4}的祖先,即1。
7访问完之后,把{7}跟{5}合并,得到{5,7},祖先为5。
然后访问5。
如果(5,7)是⼀个查询,由于7已访问过,于是LCA(5,7)为7所在集合{5,7}的祖先,即5。
如果(5,4)也是⼀个查询,由于4已访问过,则LCA(5,4)为4所在集合{1,4}的祖先,即1。
5访问完毕之后,把{5,7}跟{1,4}合并,得到{1,4,5,7},并且祖先为1。
然后访问1。
如果有(1,4)查询,则LCA(1,4)为4所在集合{1,4}的祖先,为1。
1访问完之后,把{1,4,5,7}跟{0}合并,得到{0,1,4,5,7},祖先为0。
然后剩下的2后⾯的节点处理类似。
【算法实现】
接下来提供⼀个完整算法实现。
使⽤邻接表⽅法存储⼀棵有根树。
并通过记录节点⼊度的⽅法找出有根树的根,⽅便后续处理。
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数组query存储所有的查询。
跟x相关的所有查询(x,y)都会放在query[x]的数组中,⽅便查找。
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];
}
再就是Tarjan算法的核⼼代码。
在调⽤Tarjan之前已经初始化并查集给每个节点创建了⼀个集合,并且把集合的祖先赋值为⾃⼰了,因⽽这⾥不⽤给根节点x单独创建。
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);
/*前⾯例⼦相关的⼀个输⼊输出如下:
8
0 1 0 2 0 3 1 4 1 5 5 7 3 6
7
1 4 4 5 4 7 5 7 0 5 4 3 1 6
7和4的最近公共祖先为:1
5和4的最近公共祖先为:1
5和7的最近公共祖先为:5
1和4的最近公共祖先为:1
6和1的最近公共祖先为:0
3和4的最近公共祖先为:0
0和5的最近公共祖先为:0
*/
}
下⾯是完整模板:
1/*
2 Problem:
3 OJ:
4 User: S.B.S.
5 Time:
6 Memory:
7 Length:
8*/
9 #include<iostream>
10 #include<cstdio>
11 #include<cstring>
12 #include<cmath>
13 #include<algorithm>
14 #include<queue>
15 #include<cstdlib>
16 #include<iomanip>
17 #include<cassert>
18 #include<climits>
19 #include<functional>
20 #include<bitset>
21 #include<vector>
22 #include<list>
23#define F(i,j,k) for(int i=j;i<=k;++i)
24#define M(a,b) memset(a,b,sizeof(a))
25#define FF(i,j,k) for(int i=j;i>=k;i--)
26#define maxn 10001
27#define inf 0x3f3f3f3f
28#define maxm 4001
29#define mod 998244353
30//#define LOCAL
31using namespace std;
32int read(){
33int x=0,f=1;char ch=getchar();
34while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
35while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
36return x*f;
37 }
38int n,m;
39int root;
40struct EDGE
41 {
42int from;
43int to;
44int value;
45int next;
46 }e[maxn];
47int head[maxn],tot,in[maxn];
48 inline void addedge(int u,int v)
49 {
50 tot++;
51 e[tot].from=u;
52 e[tot].to=v;
53 e[tot].next=head[u];
54 head[u]=tot;
55 }
56 vector<int> qq[maxn];
57 inline void input()
58 {
59 cin>>n>>m;M(head,-1);
60 F(i,1,n-1){int u,v;cin>>u>>v;addedge(u,v);in[v]++;}
61 F(i,0,n-1)if(in[i]==0){root=i;break;}
62 F(i,1,m){int u,v;cin>>u>>v;qq[u].push_back(v);qq[v].push_back(u);} 63return;
64 }
65int fa[maxn],rank[maxn];
66 inline void init()
67 {
68 F(i,0,n-1) fa[i]=i,rank[i]=0;
69 }
70 inline int find(int u)
71 {
72if(u!=fa[u]) fa[u]=find(fa[u]);
73return fa[u];
74 }
75 inline void Union(int x,int y)
76 {
77 x=find(x);y=find(y);
78if(x==y) return;
79if(rank[x]>rank[y]) fa[y]=x;
80else fa[x]=y,rank[y]+=rank[x]==rank[y];
81 }
82int dfn[maxn];
83bool vis[maxn];
84 inline void tarjan(int u)
85 {
86for(int i=head[u];i!=-1;i=e[i].next)
87 {
88 cout<<e[i].to<<endl;
89 tarjan(e[i].to);
90 Union(u,e[i].to);
91 dfn[find(u)]=u;
92 }
93 vis[u]=true;
94for(int i=0;i<qq[u].size();i++)
95if(vis[qq[u][i]]) cout<<u<<" and "<<qq[u][i]<<" 's LCA is : "<<dfn[find(qq[u][i])]<<endl;
96 }
97int main()
98 {
99 std::ios::sync_with_stdio(false);//cout<<setiosflags(ios::fixed)<<setprecision(1)<<y;
100 #ifdef LOCAL
101 freopen("data.in","r",stdin);
102 freopen("data.out","w",stdout);
103#endif
104 input();init();
105 F(i,0,n) dfn[i]=i;
106 M(vis,false);
107 cout<<endl<<root<<endl;
108 F(i,1,tot) cout<<e[i].from<<""<<e[i].to<<""<<e[i].next<<endl;cout<<endl;
109 tarjan(root);
110return0;
111 }
LCA1
三、RMQ算法
每当“进⼊”或回溯到某个结点时,将这个结点的深度存⼊数组E最后⼀位。
同时记录结点i在数组中第⼀次出现的位置(事实上就是进⼊结点i 时记录的位置),记做R[i]。
如果结点E[i]的深度记做D[i],易见,这时求LCA(T,u,v),就等价于求E[RMQ(D,R[u],R [v])],。