• 算法系列——最近公共祖先LCA(暴力,倍增算法,Tarjan)


    LCA最近公共祖先

    LCA:树上两个结点的最近公共祖先

    适用于求树上某条路径相关的一些问题

    方法一:暴力求解

    原理:对于已知每个结点的父亲结点的树,要求xyLCA,首先让深度更深的结点,此处

         不妨设为y,向上跳到与x同样的深度,然后两个结点再同时往上跳,直至重合,这重合的结点就是xyLCA

    缺点:效率低下,一般不使用

    代码

    int GetLCA(int x,int y){
    
      if(dep[x]>dep[y]) //令y为更深
    
      swap(x,y);
    
      while(dep[x]<dep[y])
    
        y=fa[y];//让更深的y先往上跳,直到与x相同的高度
    
      while(x!=y) //两者同时上跳,直到重合
    
        x=fa[x],y=fa[y];
    
    return x;
    
    }

    方法二:倍增算法(对暴力的优化)

    原理:暴力法,单纯地往上跳的方法缺点在于一个一个的跳,时间复杂度过高。那么对于这个跳的过程,可以进行倍增优化,就是每一次跳不止一个结点,这样就可将复杂度降到对数级。

    复杂度O(nlogn+qlogn)n个结点,q次询问

    具体实现细节(两部分):

    预处理时处理标明结点的深度

    1预处理。求出每个结点的1248......倍父亲结点,具体可以用一个比较简单的DP

    复杂度O(nlogn)

    const int N=500010;
    
    int fa[N][20];//存2的各个次方倍的父亲结点,fa[i][k]:结点i的2^k父亲结点,为0时表示不存在
    
    int maxdepth,n; //树的最大深度,树的结点数
    
    int depth[N];//各结点在树上的深度
    
     
    
    void init(){ //预处理函数
    
      for(int k=0;1<<k<=maxdepth;k++) //初始化根节点1的父亲结点全为空
    
        fa[1][k]=0;
    
      for(int k=1;1<<k<=maxdepth;k++)//倍增跳的深度2^k
    
        for(int i=2;i<=n;i++) //对每个结点都预处理
    
          fa[i][k]=fa[fa[i][k-1]][k-1]; //当前结点的2^k父亲就是当前结点的2^(k-1)的2^(k-1)父亲结点
    
    //在计算2^k父亲结点之前所有的2^(k-1)父亲结点都已算出
    
    }

     

    2:查询部分(查询xyLCA):首先让更深的y跳到与x同样的深度,此处跳的长度是两者深度差以内的最大的2的某次幂。然后xy在同样的深度一起往上跳,跳的深度也是2的尽可能大的某次幂

    复杂度O(logn)

    eg:xy深度差de=11,先跳8,再跳2,再跳1,二者便在同一深度,类似于快速幂(1011)。

    int find(int x,int y){
    
      if(depth[x]>depth[y]) swap(x,y); //令y为更深
    
      while(depth[y]>depth[x]) //当不是同一深度时
    
        y=fa[y][int(log(depth[y]-depth[x])/log(2))]; //让y尽可能的往上跳
    
    //跳到2^a父亲结点中去,a=以2为底深度差的对数(的整数部分)
    
     
    
    i  f(x==y) return x;//此时恰巧重合
    
      for(int k=log(depth[x])/log(2);k>=0;k--) //从最大跨度开始跳
    
        if(fa[x][k]!=fa[y][k])         //fa[x][k]==fa[y][k],是公共祖先但不一定是最近公共祖先
    
          x=fa[x][k],y=fa[y][k]; //二者同时往上尽可能的跳
    
    //这个for循环不会到达最近公共祖先,而是到达最近公共祖先的孩子结点
    
    return fa[x][0];//输出上面最后的孩子结点的父亲结点
    
    }

    模板题:

    洛谷P3379最近公共祖先(LCA)

    题目描述:

    给定一棵有根多叉树,请求指定两个点直接最近公共祖先。

    输入格式:

    第一行包含三个正整数N,M,S(N,M<=500000),分别表示树的结点个数、询问的个数和树根结点的序号。

    接下来N-1行每行包含两个正整数x,y,表示x结点和y结点之间有一条直线连接的边(数据保证可以构成树)

    接下里M行每行包含两个正整数a,b,表示询问a结点和b结点的最近公共祖先

    输出格式:

    输出包含M行,每行包含一个正整数,依次为每一个的询问结果

    输入样例:             输出样例:

    5 5 4                   4

    3 1                     4

    2 4                     1

    5 1                     4

    1 4                     4

    2 4

    3 2

    3 5

    1 2

    4 5

    模板代码:

    #include<iostream>
    
    #include<algorithm>
    
    #include<vector>
    
    #include<cmath>
    
    using namespace std;
    
    const int N=500005;
    
    const int dep=20;
    
    int fa[N][dep];
    
    int depth[N]={0};
    
    vector<int>e[N];
    
    int n,m,s;
    
    int maxdepth=0;
    
    void dfs(int x,int y){ //dfs遍历无向图,确定各个结点的深度(depth[]),树的深度(maxdepth),父亲结点(fa[i][0])
    
      for(int i=0;i<e[x].size();i++){
    
        if(e[x][i]==y) continue;
    
        depth[e[x][i]]=depth[x]+1;
    
        maxdepth=max(maxdepth,depth[e[x][i]]);
    
        fa[e[x][i]][0]=x;//一倍父亲结点
    
        dfs(e[x][i],x);
    
    }
    
    }
    
    void init(int x){//初始化倍增结点(fa[x][i])
    
       for(int i=0;i<dep;i++) fa[x][i]=0;//为0表示不存在2^k倍父亲结点
    
      for(int i=1;(1<<i)<=maxdepth;i++){
    
        for(int j=1;j<=n;j++){
    
        if(j==x) continue;
    
        fa[j][i]=fa[fa[j][i-1]][i-1];
    
        }
    
      }
    
    }
    
    int find(int x,int y){//查找结点x,y的最近公共祖先
    
      if(depth[x]>depth[y]) swap(x,y);
    
       while(depth[y]>depth[x])
    
        y=fa[y][int(log(depth[y]-depth[x])/log(2))];
    
        if(x==y) return x;
    
        for(int i=(log(depth[y])/log(2));i>=0;i--)
    
        if(fa[x][i]!=fa[y][i])
      
        x=fa[x][i],y=fa[y][i];
    
    return fa[x][0];
    
    }
    
    int main(){
    
      cin>>n>>m>>s;
    
      int u,v;
    
      for(int i=1;i<n;i++){
      
        cin>>u>>v;
    
        e[u].push_back(v);//构造无向图
    
        e[v].push_back(u);
    
      }
    
      depth[s]=0;//初始根节点深度为0
    
      dfs(s,-1);
    
      init(s);
    
      while(m--){
    
        cin>>u>>v;
    
        cout<<find(u,v)<<endl;
    
      }
    
      return 0;
    
    }

    注:不过这个代码有两个点过不去,即便使用链式前向星(听说比vetor快)但仍旧有两个点过不去(超时),下面的Tarjan倒是可以(⊙﹏⊙)

    链式前向星:

    #include<iostream>
    
    #include<algorithm>
    
    #include<vector>
    
    #include<cmath>
    
    using namespace std;
    
    const int N=500005;
    
    const int dep=20;
    
    int fa[N][dep];
    
    int depth[N]={0};
    
    vector<int>e[N];
    
    int n,m,s;
    
    int maxdepth=0;
    
    void dfs(int x,int y){ //dfs遍历无向图,确定各个结点的深度(depth[]),树的深度(maxdepth),父亲结点(fa[i][0])
    
      for(int i=0;i<e[x].size();i++){
    
        if(e[x][i]==y) continue;
    
        depth[e[x][i]]=depth[x]+1;
    
        maxdepth=max(maxdepth,depth[e[x][i]]);
    
        fa[e[x][i]][0]=x;//一倍父亲结点
    
        dfs(e[x][i],x);
    
      }
    
    }
    
    void init(int x){//初始化倍增结点(fa[x][i])
    
      for(int i=0;i<dep;i++) fa[x][i]=0;//为0表示不存在2^k倍父亲结点
    
      for(int i=1;(1<<i)<=maxdepth;i++){
    
        for(int j=1;j<=n;j++){
    
          if(j==x) continue;
    
          fa[j][i]=fa[fa[j][i-1]][i-1];
    
        }
      
      }
    
    }
    
    int find(int x,int y){//查找结点x,y的最近公共祖先
    
      if(depth[x]>depth[y]) swap(x,y);
    
      while(depth[y]>depth[x])
    
        y=fa[y][int(log(depth[y]-depth[x])/log(2))];
    
      if(x==y) return x;
    
      for(int i=(log(depth[y])/log(2));i>=0;i--)
    
        if(fa[x][i]!=fa[y][i])
    
        x=fa[x][i],y=fa[y][i];
    
      return fa[x][0];
    
    }
    
    int main(){
    
      cin>>n>>m>>s;
    
      int u,v;
    
      for(int i=1;i<n;i++){
    
        cin>>u>>v;
    
        e[u].push_back(v);//构造无向图
    
        e[v].push_back(u);
    
      }
    
      depth[s]=0;//初始根节点深度为0
    
      dfs(s,-1);
    
      init(s);
    
      while(m--){
    
        cin>>u>>v;
    
        cout<<find(u,v)<<endl;
      
      }
    
      return 0;
    
    }

    洛谷P1967货车运输

    题目描述:

    A国有n座城市,编号从1n,城市之间有m条双向道路,每一条道路对车辆都有重量限制,简称限重。现在又q辆货车在此运输货物,司机们想知道,每辆车在不超过车辆限重的情况下。最多可以运多重的货物。

    输入格式:

    第一行有两个用一个空格隔开的整数n,m(n<=10000,m<=50000),表示A国有n座城市和m条道路。接下来m行,每行三个整数x,y,z,每两个整数之间用一个空格隔开,表示在x城市和y城市之间存在一条限重为z(z<=100000)的道路。

    注意:x!=y,两座城市之间可能有多条道路。

    接下一行有一个整数q(q<=30000),表示有q辆货车需要运输。

    接下来q行,每行两个整数,之间用一个空格隔开,表示一辆货车需要从x城市到y城市,保证x!=y

    输出格式:

    共有q行,,每行一个整数,表示对于每一辆货车,它的最大载重是多少。

    如果货车不能到达目的地,输出-1

    题解:(最小值的最大化问题)

    求出两点之间路径(若干条路径)上最小边权的最大值。

    给定两点之间可能会有多个路径,由于要求的是路径上的边权的最小值,这个最小值要最大,因此可以先用最大生成树算法生成一个边权尽可能大的连通图,这样路径就是唯一的,要求的答案就是这个路径上的最小边权,此处可以使用倍增LCA算法中的思想来实现,两结点之间的简单路径就是先到他们的LCA,再到另一个结点。

    AC代码:

    #include<iostream>
    
    #include<queue>
    
    #include<vector>
    
    #include<cmath>
    
    #include<algorithm>
    
    using namespace std;
    
    const int N=10005;
    
    int n,m,q;
    
    int depth[N];//结点的深度
    
    int fa[N][16];//倍增父亲结点
    
    int min_edge[N][16]={0}; //与倍增父亲结点之间的最小值
    
    struct node{
    
      int x,y,z;
    
    }b;//表示输入的边
    
    struct edg{
    
      int x,z;
    
    }d;//表示放入图中的边
    
    vector<struct edg>e[N];//无向图的边的存储
    
    vector<int>se[N];//se[i]存储表示以i为根节点的树的结点编号的集合 ,包括根节点本身
    
    int c[N];//并查集的标记数组
    
    int find_bing(int x){
    
      if(c[x]==x) return x;
    
      return c[x]=find_bing(c[x]);
    
    }
    
    void merge(int x,int y){
    
      int fx=find_bing(x),fy=find_bing(y);
    
      c[fx]=fy;
    
    }
    
     
    
    void dfs(int x,int y){
    
      for(int i=0;i<e[x].size();i++){
    
        d=e[x][i];
    
        if(d.x==y) continue;
    
        depth[d.x]=depth[x]+1;
    
        fa[d.x][0]=x;
    
        min_edge[d.x][0]=d.z;
    
        dfs(d.x,x);
    
      }
    
    }
    
    void init(int x){
    
      for(int i=0;i<16;i++) fa[x][i]=0;
    
      for(int i=1;i<16;i++){
    
        for(int j=0;j<se[x].size();j++){
    
          if(se[x][j]==x) continue;
    
          int y=se[x][j],fy=fa[y][i-1];
    
          min_edge[y][i]=min(min_edge[y][i-1],min_edge[fy][i-1]);
    
          fa[y][i]=fa[fy][i-1];
    
        }
    
      }
    
    }
    
    int find(int x,int y){
    
      int minn=100000000;
    
      if(depth[x]>depth[y]) swap(x,y);
    
      while(depth[y]>depth[x]){
      
        minn=min(minn,min_edge[y][int(log(depth[y]-depth[x])/log(2))]);//这步之前与下行代码位置错了,醉了
    
        y=fa[y][int(log(depth[y]-depth[x])/log(2))];
      
      }
      
      if(x==y) return minn;
    
      for(int i=log(depth[y])/log(2);i>=0;i--){
    
        if(fa[x][i]!=fa[y][i]){
    
          minn=min(minn,min_edge[x][i]);
    
          minn=min(minn,min_edge[y][i]);
    
          x=fa[x][i],y=fa[y][i];//这里与上面两行的顺序之前又错了,,,,,,
    
        }
    
      }
    
      minn=min(minn,min_edge[x][0]);
      
      minn=min(minn,min_edge[y][0]);
    
      return minn;
    
    }
    
    bool operator<(struct node a,struct node b){
    
      return a.z<b.z;
    
    }//队首元素大
    
     
    
    int main(){
    
      cin>>n>>m;
    
      priority_queue<struct node,vector<struct node> ,less<struct node> >que;
    
      while(m--){
    
        cin>>b.x>>b.y>>b.z;
    
        que.push(b);
    
      }
    
      int s=0;
    
      for(int i=1;i<=n;i++) c[i]=i;
    
      while(s<n-1&&!que.empty()){//当找到n-1条边,或者所有的边都遍历完
    
        b=que.top();
    
        que.pop();
    
        if(find_bing(b.x)==find_bing(b.y)) continue;
    
        merge(b.x,b.y);
    
        d.x=b.y,d.z=b.z;
    
        e[b.x].push_back(d);
    
        d.x=b.x;
    
        e[b.y].push_back(d);
    
        s++;
    
      }
    
      for(int i=1;i<=n;i++){
    
        se[find_bing(i)].push_back(i);
    
      }
    
      for(int i=1;i<=n;i++){
    
        if(c[i]==i) {
    
        depth[i]=0;
    
        dfs(i,-1);
    
        init(i);
    
        }
    
      }
    
      int u,v;
    
      cin>>q;
    
      while(q--){
    
        cin>>u>>v;
    
        int w;
    
        if(find_bing(u)!=find_bing(v)) w=-1;
    
        else w=find(u,v);
    
        cout<<w<<endl;
    
      }
    
      return 0;
      
    }
    
     

    注:在求倍增节点的过程中,可以顺便求出类似最小最大权值,权值和等信息。

    方法三:Tarjan算法求LCA

    复杂度:On+q)比倍增算法快

    原理:离线(要先知道所有的询问)(也就是直接一遍求出所有需要查询结点的LCA)处理询问,将询问用链表存在相关的结点上,dfs遍历整棵树,当到x结点发现y结点已经到过了时,此时y结点上方还未回溯的结点就是(x结点和y结点)的LCA

     

    洛谷P3379最近公共祖先(LCA)

    题目描述

    如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先。

    输入格式

    第一行包含三个正整数 N,M,SN,M,S,分别表示树的结点个数、询问的个数和树根结点的序号。

    接下来 N-1N1 行每行包含两个正整数 x, yx,y,表示 xx 结点和 yy 结点之间有一条直接连接的边(数据保证可以构成树)。

    接下来 MM 行每行包含两个正整数 a, ba,b,表示询问 aa 结点和 bb 结点的最近公共祖先。

    输出格式

    输出包含 MM 行,每行包含一个正整数,依次为每一个询问的结果。

    #include<bits/stdc++.h>
    
    using namespace std;
    
    const int N=500005;
    
    int LCAUFS[N];
    
    int Is_Pass[N]={0};
    
    int ans[N];
    
    int LCANum=0,LCATo[2*N],LCANext[2*N],LCAHead[N]={0};//注意这里与下一行中好几个数组开的是2*N空间,是因为要存两遍
    
    int QueryNum=0,QueryTo[2*N],QueryNext[2*N],QueryHead[N]={0},Pos[2*N];
    
    /*int UFSFind(int x){
    
    if(x==LCAUFS[x]) return x;
    
    return LCAUFS[x]=UFSFind(LCAUFS[x]);
    
    }*/
    
    int UFSFind(int x){//非递归路径压缩并查集比递归压缩要快 ,当然一般情况递归就够用了
    
      int re=x,p;
    
      while(LCAUFS[re]!=re) re=LCAUFS[re];
    
      while(LCAUFS[x]!=x) p=x,x=LCAUFS[x],LCAUFS[p]=re;
    
      return re;
    
    }
    
    void AddLCAEdge(int u,int v){
    
      LCATo[++LCANum]=v,LCANext[LCANum]=LCAHead[u],LCAHead[u]=LCANum;
    
    }
    
    void AddQueryEdge(int u,int v,int num){
    
      QueryTo[++QueryNum]=v,QueryNext[QueryNum]=QueryHead[u],QueryHead[u]=QueryNum,Pos[QueryNum]=num;//Pos存储的是第i组数据是在输入的第num组数据
    
    }
    
    void Tarjan(int x,int y){
    
      LCAUFS[x]=x;//使用并查集表示x所在回溯树的根节点,也就是结点上方还未回溯的结点(也就是最近LCA)
    
      Is_Pass[x]=1;//标记x结点已经到过了,但不一定进行了回溯
    
      for(int e=LCAHead[x];e;e=LCANext[e]){//递归遍历其子结点
    
        if(LCATo[e]==y) continue;
    
        Tarjan(LCATo[e],x);
    
        LCAUFS[LCATo[e]]=x;
    
    //表示此时已经是到了LCATo[e]结点的回溯时刻,需要利用并查集的知识
    
    //来标记该结点回溯树的根节点,此时表示刚回溯到此结点时,其所在回溯树
    
    //的根结点是其父亲结点
    
      }
    
    //需要知道回溯的过程一定是先回溯子结点之后才能回溯祖先结点
    
      for(int e=QueryHead[x];e;e=QueryNext[e]){//表示e结点已经经过了并且现在是正在回溯的过程中
    
        if(Is_Pass[QueryTo[e]]){//只要与其对应的结点已经经过就可以
    
          ans[Pos[e]]=UFSFind(QueryTo[e]);
        
        }
    
      }
    
      return ;
      
    }
    
    int main(){
    
      int n,m,s;
    
      cin>>n>>m>>s;
    
      for(int i=1,u,v;i<n;i++){
    
        scanf("%d%d",&u,&v);
    
        //使用链式前向星存储树的边
    
        AddLCAEdge(u,v);
    
        AddLCAEdge(v,u);
    
      }
    
      for(int i=1,u,v;i<=m;i++){
    
        scanf("%d%d",&u,&v);
    
        //使用链式前向星存储每个结点与哪几个结点进行求LCA
    
        AddQueryEdge(u,v,i);
    
        AddQueryEdge(v,u,i);
    
      }
      
      Tarjan(s,-1);
    
      for(int i=1;i<=m;i++)
    
      printf("%d
    ",ans[i]);
    
      return 0;
    
    }

    注:前面不少地方使用了链式前向星。所谓的链式前向星就是vector<int>e[N]方法存储图的边的另一种方法,而且比vector方法快。可以理解使用数组(使用结构体的话可能会更容易理解)完成的邻接表(指针完成的邻接表比较复杂,而且带有指针,比较容易出错)。每次操作就是在邻接表的最前面插入一个元素,与指针类型的邻接表其实在本质上没有什么大区别。

    void AddLCAEdge(int u,int v){
    
      LCATo[++LCANum]=v,LCANext[LCANum]=LCAHead[u],LCAHead[u]=LCANum;
    
    }

    虽然Tarjan算法比倍增算法快,但是在一些场合还是会使用倍增算法,因为倍增算法不仅可以求出祖先结点的信息,还可以获得权值和,最小权值,最大权值等信息


    作者:孙建钊
    出处:http://www.cnblogs.com/sunjianzhao/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

  • 相关阅读:
    算法训练 表达式计算
    基础练习 十六进制转十进制
    基础练习 十六进制转十进制
    基础练习 十六进制转十进制
    New ways to verify that Multipath TCP works through your network
    TCP的拥塞控制 (Tahoe Reno NewReno SACK)
    Multipath TCP Port for Android 4.1.2
    How to enable ping response in windows 7?
    NS3
    Multipath TCP Port for Android
  • 原文地址:https://www.cnblogs.com/sunjianzhao/p/12769284.html
Copyright © 2020-2023  润新知