最近公共祖先(LCA)

简介: 最近公共祖先(LCA)

求法


1.祖孙询问(倍增)

因为2^16>4e4,所以开15够了

信息学奥赛一本通(C++版)在线评测系统 (ssoier.cn)

#include<bits/stdc++.h>
using namespace std;
const int N=4e4+10,M=2*N;
int h[N],e[M],ne[M],idx;
int n,m,root;
int depth[N],f[N][16],q[N];//depth存的是每个节点的深度,f存的是以节点i开始跳2的j次方步到达的点
void add(int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void bfs()
{
    memset(depth,0x3f,sizeof depth);//初始化每个深度
    int hh=0,tt=0;
    q[0]=root;//把根节点放进队列中
    depth[0]=0,depth[root]=1;//初始化0为虚拟根,因为待会跳的话会跳出根节点,根节点就定义为第一层
    while(hh<=tt)
    {
        int t=q[hh++];
        for(int i=h[t];~i;i=ne[i])
        {
            int j=e[i];
            if(depth[j]>depth[t]+1)//假如这个节点没被更新过
            {
                depth[j]=depth[t]+1;//更新这个节点的深度
                q[++tt]=j;//把这个点放进队列中
                f[j][0]=t;//从j开始跳2^0=1步是t
                for(int k=1;k<=15;k++)//更新这个点跳的步数走到的点
                    f[j][k]=f[f[j][k-1]][k-1];//用递归的方式更新
            }
        }
    }
}
int lca(int a,int b)
{
    if(depth[a]<depth[b]) swap(a,b);//让a是深度最大的点
    //让a跳到跟b同个深度
    for(int i=15;i>=0;i--)//让a跳到跟b同个深度,从大的步数开始跳
        if(depth[f[a][i]]>=depth[b])
            a=f[a][i];//更新跳到的点
    if(a==b) return b;//假如跳到了b,说明b是最近公共祖先
    //在同一深度下一起跳2^i步来找公共祖先
    for(int i=15;i>=0;i--)
      if(f[a][i]!=f[b][i])
      {
          a=f[a][i];//更新跳到的点
          b=f[b][i];//更新跳到的点
      }
      return f[a][0];//因为是跳到公共祖先的下一层,则输出他们点的上一步就是公共祖先了
}
int main()
{
    cin>>n;
    memset(h,-1,sizeof h);
    while(n--)
    {
        int a,b;
        cin>>a>>b;
        if(b==-1) root=a;
        else add(a,b),add(b,a);
    }
    bfs();//先搜索一下深度和跳多少步到那个位置
    cin>>m;
    while(m--)
    {
        int a,b;
        cin>>a>>b;
        int p=lca(a,b);//求一下公共祖先
        if(p==a) puts("1");
        else if(p==b) puts("2");
        else puts("0");
    }
    return 0;
}

2.距离(tarjan+并查集)

1171. 距离 - AcWing题库

tarjan是离线算法就是先把所有询问存下来,然后在求

#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> pii;
#define x first
#define y second
const int N=2e5+10,M=2*N;
int n,m;
int h[N],e[M],ne[M],w[M],idx;
int ans[N],dist[N];//ans是记录每个点的答案,diat存的是点跟1号点的距离
int st[N];//st存的是三种状态,0表示还没遍历过,1表示争做遍历,2表示已经遍历过
int p[N];//用来并查集
vector<pii> query[N];//第一个存的是询问的点,第二个存的是下标
void add(int a,int b,int c)
{
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u,int fa)//处理每个点跟1的距离
{
    for(int i=h[u];~i;i=ne[i])
    {
        int j=e[i];
        if(j==fa) continue;
        dist[j]=dist[u]+w[i];
        dfs(j,u);
    }
}
int find(int x)//并查集
{
    if(p[x]!=x) p[x]=find(p[x]);
    return p[x];
}
void tarjan(int u)
{
    st[u]=1;//标记这个点正在遍历
    for(int i=h[u];~i;i=ne[i])
    {
        int j=e[i];
        if(!st[j])//假如没有遍历过
        {
            tarjan(j);//则遍历一遍这个点
            p[j]=u;//把这个点记录是跟u一个集合的
        }
    }
    for(auto item:query[u])//处理所有询问
    {
        int y=item.x,id=item.y;//y是询问的点,id是询问的位置
        if(st[y]==2)//假如是遍历过的点
        {
            int anc=find(y);//找一下是由那个点的集合,也就是最近公共祖先
            ans[id]=dist[y]+dist[u]-2*dist[anc];//答案就是y与1距离加上u的距离减去最近公共祖先距离的两倍就是答案
        }
    }
    st[u]=2;//标记这个点已经遍历过
}
int main()
{
   scanf("%d%d",&n,&m);
   memset(h,-1,sizeof h);
   for(int i=1;i<n;i++)
   {
       int a,b,c;
       scanf("%d%d%d",&a,&b,&c);
       add(a,b,c),add(b,a,c);
   }
   for(int i=0;i<m;i++)
   {
       int a,b;
       scanf("%d%d",&a,&b);
       if(a!=b)//假如相等则答案就是0,不用求
       {
           query[a].push_back({b,i});//把a询问的b和下标i放进a询问中
           query[b].push_back({a,i});//把b询问的a和下标i放进b询问中
       }
   }
   dfs(1,-1);//处理每个点跟1号点的距离
   for(int i=1;i<=n;i++) p[i]=i;//并查集初始化
   tarjan(1);//做一遍Trajan算法
   for(int i=0;i<m;i++) printf("%d\n",ans[i]);
    return 0;
}

3.次小生成树(kruskal+倍增)

356. 次小生成树 - AcWing题库

因为2^17>1e5,所以开17就行了

kruskal求最小生成树,倍增求两点之间的最大和次大边

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+10,M=3e5+10,INF=0x3f3f3f3f;
int p[N];//并查集
int h[N],e[M],ne[M],w[M],idx;
int depth[N],d1[N][17],d2[N][17],f[N][17];//depth是深度,d1是某个点走2^j次方步的最大值,d2是次大值,f是某个点走2^k次方步所到达的点
int q[N];
int n,m;
struct Edge
{
    int a,b,w;
    bool used;//用来标记是否是树边
    bool operator <(const Edge &t) const
    {
        return w<t.w;
    }
}edge[M];
void add(int a,int b,int c)
{
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int find(int x)
{
    if(p[x]!=x) p[x]=find(p[x]);
    return p[x];
}
ll kruskal()//求最小生成树
{
    memset(h,-1,sizeof h);//初始化
    for(int i=1;i<=n;i++) p[i]=i;//初始化
    sort(edge,edge+m);//排序
    ll res=0;
    for(int i=0;i<m;i++)
    {
        int a=find(edge[i].a),b=find(edge[i].b),w=edge[i].w;
        if(a!=b)//假如不在一个集合
        {
            p[a]=b;//合并成一个集合
            res+=w;//加上这条最小生成树的边
            edge[i].used=true;//标记一下是树边
            add(edge[i].a,edge[i].b,w),add(edge[i].b,edge[i].a,w);//建树图
        }
    }
    return res;//返回最小生成树的权值和
}
void bfs()//求 深度 最大值 次大值  每个点走几步由那个点更新过来
{
     memset(depth,0x3f,sizeof depth);
     depth[0]=0,depth[1]=1;//0号点是避免跳出去,1号点初始化深度为1
     int hh=0,tt=0;
     q[0]=1;//1号点入队
     while(hh<=tt)
     {
         int t=q[hh++];
         for(int i=h[t];~i;i=ne[i])
         {
             int j=e[i];
             if(depth[j]>depth[t]+1)//假如没有更新过
             {
                 depth[j]=depth[t]+1;//则更新深度
                 q[++tt]=j;//把这个点入队
                 f[j][0]=t;//j这个点走2^0=1步是t
                 d1[j][0]=w[i],d2[j][0]=-INF;//j这个点向上走2^0=1步的最大值是w[i],次大没有标记为负无穷
                 for(int k=1;k<=16;k++)//更新跳到的点
                 {
                     int anc=f[j][k-1];//表示由那个点跳过来的
                     int d[4]={d1[anc][k-1],d2[anc][k-1],d1[j][k-1],d2[j][k-1]};//把j~anc~k的最大次大加到d中
                     f[j][k]=f[anc][k-1];//更新跳的点
                     d1[j][k]=d2[j][k]=-INF;//先初始化为负无穷
                     for(int u=0;u<4;u++)//求一遍最大和次大
                     {
                         int dist=d[u];
                         if(dist>d1[j][k]) d2[j][k]=d1[j][k],d1[j][k]=dist;
                         else if(dist!=d1[j][k]&&dist>d2[j][k]) d2[j][k]=dist;
                     }
                 }
             }
         }
     }
}
int lca(int a,int b,int w)//用来求最大值与次大值能否更新
{
    static int d[2*N];
    int cnt=0;
    if(depth[a]<depth[b]) swap(a,b);//假如a深度低,则交换
    for(int k=16;k>=0;k--)//让a与b同个深度
      if(depth[f[a][k]]>=depth[b])//假如还是大,则继续跳
      {
          //把他们跳过的位置的最大值与次大值都记录下来
          d[cnt++]=d1[a][k];
          d[cnt++]=d2[a][k];
          a=f[a][k];//更新跳的点
      }
    if(a!=b)//假如还没跳到一起,也就是没找到最近公共祖宗
    {
       for(int k=16;k>=0;k--)//让a和b一起跳
          if(f[a][k]!=f[b][k])
          {
              //把他们跳过的位置的最大值与次大值都记录下来
              d[cnt++]=d1[a][k];
              d[cnt++]=d2[a][k];
              d[cnt++]=d1[b][k];
              d[cnt++]=d2[b][k];
             a=f[a][k],b=f[b][k];//更新跳过的位置
          }
          //最后也要把跳到最近公共祖先的位置也记录下来
          d[cnt++]=d1[a][0];
          d[cnt++]=d2[a][0];
          d[cnt++]=d1[b][0];
          d[cnt++]=d2[b][0];
    }
    int dist1=-INF,dist2=-INF;
    for(int i=0;i<cnt;i++)//求一遍最大值和次大值
    {
        int dist=d[i];
        if(dist>dist1) dist2=dist1,dist1=dist;
        else if(dist!=dist1&&dist>dist2) dist2=dist;
    }
    if(w>dist1) return w-dist1;//假如最大值可以用来更新
    if(w>dist2) return w-dist2;//反之次大值可以用来更新
    return INF;//反之为正无穷
}
int main()
{
   scanf("%d%d",&n,&m);
   for(int i=0;i<m;i++)
   {
      int a,b,w;
      scanf("%d%d%d",&a,&b,&w);
      edge[i]={a,b,w};
   }
   ll sum=kruskal();//求一下最小生成树的权值和
   bfs();//求深度 最大值 次大值 更新的点
   ll res=1e18;
   for(int i=0;i<m;i++)
      if(!edge[i].used)//假如是非树边
      {
          int a=edge[i].a,b=edge[i].b,w=edge[i].w;
          res=min(res,sum+lca(a,b,w));//求一遍最大值与次大值能否用来更新
      }
    printf("%lld\n",res);
    return 0;
}


4. 闇の連鎖(倍增+树形差分)

352. 闇の連鎖 - AcWing题库

c表示所有非树边构成的换的树边+1,假如该树边的c大于1 则说明得砍大于1的非树边,则不满足题意方案则+0,假如等于1,说明砍1条非树边就行则方案+1,假如等于0,说明不管怎么砍非树边都满足则+m

然后这题要用到树形差分就是d[x]+=c,d[y]+=c,d[p]-=c,p是x与y的最近公共祖先

 

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+10,M=2e5+10;
int h[N],e[M],ne[M],idx;
int depth[N],f[N][17];//depth是深度,f是记录条几步条到的点
int q[N],d[N];//d存的就是c,就是树形差分
int ans,n,m;;
void add(int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void bfs()
{
    memset(depth,0x3f,sizeof depth);
    depth[0]=0,depth[1]=1;//0号点是虚拟原点,1号点让他为跟
    int hh=0,tt=0;
    q[0]=1;//1号点放进队列中
    while(hh<=tt)
    {
        int t=q[hh++];
        for(int i=h[t];~i;i=ne[i])
        {
            int j=e[i];
            if(depth[j]>depth[t]+1)//假如还没更新过
            {
                depth[j]=depth[t]+1;//则更新
                q[++tt]=j;//把这个点放进队列中
                f[j][0]=t;//标记这个点的上一步是t
                for(int k=1;k<=16;k++)//更新条的其他步到达的点
                    f[j][k]=f[f[j][k-1]][k-1];
            }
        }
    }
}
int lca(int a,int b)
{
    if(depth[a]<depth[b]) swap(a,b);//假如a的深度小于b的则交换
    for(int k=16;k>=0;k--)//让a跳到跟b同个深度
        if(depth[f[a][k]]>=depth[b])
           a=f[a][k];//更新跳到的点
    if(a==b) return b;//假如b是最近公共祖先了,则返回
    for(int k=16;k>=0;k--)//让a跟b一起跳
        if(f[a][k]!=f[b][k])
           a=f[a][k],b=f[b][k];//更新两个跳到的位置
    return f[a][0];//返回他们的上一个就是最近公共祖先节点
}
//该点的值就是子树的差分前缀和,跟其他差分一样都是差分前缀和就是该点的值
int dfs(int u,int fa)//返回值是子树差分值前缀和
{
    int res=d[u];//res记录的是树形前缀和,把这个点的差分也加上
    for(int i=h[u];~i;i=ne[i])
    {
        int j=e[i];
        if(j==fa) continue;//避免重复搜索
        int s=dfs(j,u);//s是j这个子树的差分前缀和
        if(s==0) ans+=m;//假如是0则加上m
        else if(s==1) ans++;//假如是1则加1
        res+=s;//把所有子树的差分都加上
    }
    return res;
}
int main()
{
   memset(h,-1,sizeof h);
   scanf("%d%d",&n,&m);
   for(int i=1;i<n;i++)
   {
       int a,b;
       scanf("%d%d",&a,&b);
       add(a,b),add(b,a);
   }
   bfs();//求一遍深度和跳到的点
   for(int i=1;i<=m;i++)
   {
       int a,b;
       scanf("%d%d",&a,&b);
       int p=lca(a,b);//求a跟b的公共祖先
       d[a]++,d[b]++,d[p]-=2;//因为是差分,则a+1,b+1,p-2
   }
   dfs(1,-1);//最后求一遍所有的方案
   printf("%d\n",ans);
    return 0;
}


相关文章
|
存储
【LeetCode】236. 二叉树的最近公共祖先、 JZ36 二叉搜索树与双向链表
236. 二叉树的最近公共祖先 236. 二叉树的最近公共祖先 题目描述:
68 0
28_二叉树的最近公共祖先
28_二叉树的最近公共祖先
|
7月前
|
算法 vr&ar Windows
Tarjan算法求LCA(最近公共祖先)
Tarjan算法求LCA(最近公共祖先)
45 0
|
C++
二叉树的最近公共祖先(C++实现)
二叉树的最近公共祖先(C++实现)
70 1
|
存储 搜索推荐
【二叉树OJ题(二)】前序遍历&&中序遍历&&后序遍历&&另一颗树的子树&&二叉树遍历&&平衡二叉树(下)
【二叉树OJ题(二)】前序遍历&&中序遍历&&后序遍历&&另一颗树的子树&&二叉树遍历&&平衡二叉树(下)
【二叉树OJ题(二)】前序遍历&&中序遍历&&后序遍历&&另一颗树的子树&&二叉树遍历&&平衡二叉树(上)
【二叉树OJ题(二)】前序遍历&&中序遍历&&后序遍历&&另一颗树的子树&&二叉树遍历&&平衡二叉树(上)
P3379 【模板】最近公共祖先(LCA)
P3379 【模板】最近公共祖先(LCA)
66 0
|
存储 程序员
【Leetcode】KY11 二叉树遍历(牛客网)、144. 二叉树的前序遍历、94. 二叉树的中序遍历、145. 二叉树的后序遍历
【Leetcode】KY11 二叉树遍历(牛客网)、144. 二叉树的前序遍历、94. 二叉树的中序遍历、145. 二叉树的后序遍历
103 0
【Leetcode】KY11 二叉树遍历(牛客网)、144. 二叉树的前序遍历、94. 二叉树的中序遍历、145. 二叉树的后序遍历
二叉树的最近公共祖先
#### [236. 二叉树的最近公共祖先