zoukankan      html  css  js  c++  java
  • 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
    ", 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
     31 using namespace std;
     32 int read(){
     33     int x=0,f=1;char ch=getchar();
     34     while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
     35     while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
     36     return x*f;
     37 }
     38 int n,m;
     39 int root;
     40 struct EDGE
     41 {
     42     int from;
     43     int to;
     44     int value;
     45     int next;
     46 }e[maxn];
     47 int 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);}
     63     return;
     64 }
     65 int 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 {
     72     if(u!=fa[u]) fa[u]=find(fa[u]);
     73     return fa[u];
     74 }
     75 inline void Union(int x,int y)
     76 {
     77     x=find(x);y=find(y);
     78     if(x==y) return;
     79     if(rank[x]>rank[y]) fa[y]=x;
     80     else fa[x]=y,rank[y]+=rank[x]==rank[y];
     81 }
     82 int dfn[maxn];
     83 bool vis[maxn];
     84 inline void tarjan(int u)
     85 {
     86     for(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;
     94     for(int i=0;i<qq[u].size();i++)
     95         if(vis[qq[u][i]]) cout<<u<<" and "<<qq[u][i]<<" 's LCA is : "<<dfn[find(qq[u][i])]<<endl;
     96 }
     97 int 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);
    110     return 0;
    111 }
    LCA1

    三、RMQ算法

    每当“进入”或回溯到某个结点时,将这个结点的深度存入数组E最后一位。同时记录结点i在数组中第一次出现的位置(事实上就是进入 结点i时记录的位置),记做R[i]。如果结点E[i]的深度记做D[i],易见,这时求LCA(T,u,v),就等价于求E[RMQ(D,R[u],R [v])],

  • 相关阅读:
    Redis键是如何过期删除的
    使用EventBus + Redis发布订阅模式提升业务执行性能(下)
    使用EventBus + Redis发布订阅模式提升业务执行性能
    使用C#的计时器加观察者模式完成报警推送需求
    Go 语言入门教程:变量
    Go 语言入门教程:安装
    Docker 入门:Dockerfile
    Docker 入门:容器
    Docker 入门:什么是 Docker ?
    Windows 系统如何安装 Docker
  • 原文地址:https://www.cnblogs.com/SBSOI/p/5978699.html
Copyright © 2011-2022 走看看