求最近公共祖先的三种算法(倍增法)(Tarjan)(树链法)

 树链法

求最近公共祖先的树链部分

重儿子: 父节点的所有儿子中结点数目最多的节点
轻儿子:父节点中除重儿子以外的儿子
重边:父节点和重儿子连城的边
轻边:父节点和轻儿子连成的边
重链:由多重边连接而成的路径
1.整棵树会被剖分成若干条重链
2.轻儿子一定是每条重链的顶点
3.任意一条路径被切分成不超过logn条链


数组
fa[u] 存u的夫节点
dep[u] 存u的深度
son[u]  存u的重儿子
sz[u] 存以u为根的子树的节点数
top[u] 存u的所有重链的顶点
 
1.第一遍dfs,搞出fa,dep,son数组 
2.第二遍dfs,搞出top数组
3.让两个游标沿着各自的重链向上跳,跳到同一条重链上时,深度较小的哪个游标所指向的点,就是LCA 

vector<int>e[N];
int fa[N],dep[N],son[N].sz[N];
int top[N];
void  dfs1(inty u,int father){
    fa[u]=father,dep[u]=dep[father]+1,sz[u]=1;
    for(int v:e[v]){
        if(v==father)continue;
        dfs1(v,u);
        sz[u]+=sz[v];//累加和  
        if(sz[son[u]]<sz[v])son[u]=v;//找重儿子 
    }

void dfs2(int v,int t){//找top 
    top[u]=t;//记录链头 
    if(!son[u])return;//叶节点没有重儿子 
    dfs2(son[u],t);//搜重儿子 
    for(int v:e[u]){
        if(v==fa[u]||v==son[u])continue;
        dfs2(v,v);//搜轻儿子 
    } 
}     

int lca(int u,int v){
    while(top[u]!=top[v]){

        if(dep[top[u]]<dep[top[v]])swap(u,v);
        
        u=fa[top[u]];
    }
    return dep[u]<dep[v]?u:v; //最后深度小的为最近公共父节点 

 

 倍增法

两个节点的最近公共祖先(lowest Common Ancestor LCA)
就是这两个点的公共祖先里面,离他们最近的那个

倍增算法是最经典的LCA算法
dep[u]存u点的深度
fa[u][i]存从u点向上跳2层的祖先结点 i=0 1 2 3..
例如,从节点9向上跳2^0层的祖先是3,跳2^1层的祖先是6,跳2^2层的祖先是1,跳2^3层的祖先是0(哨兵)

         1
        / \
       5   4
      /|\
    2  6 7
       /\
      3  8
       \
       9
       
dfs一遍,创建ST表
倍增递推,fa[u][i]=fa[fa[u][i-1]][i-1]
分一半来跳 
    u->fa[u][i-1]->fa[fa[u][i-1]][i-1]
    u->fa[u][i]
    
  0 1 2 3...19
1 0 0 0 0...0
5 1 0 0 0...0
2 5 1 0 0...0
6 5 1 0 0...0
3 6 5 0 0...0
9 3 6 1 0...0


2.利用ST表求LCA
(1),第一阶段,将u,v跳到同一层
设u,v两点深度之差为y,将y进行二进制拆分,可以
将y次游标跳到优化为y的二进制表示所含1的个数
次游标跳跃,一定能跳到同一层

例如:y=1019(0..0111111011)=512+256+..+8+2+1
不越界则跳,共跳9次到达
 
(2) 第二阶段,将u,v一起跳到LCA的下一层
从最大的i开始循环尝试,一直尝试到0,最后游标u,v一定能停在LCA的下一次
例如,v=1019(0..0111110111) 
两游标会跳512+256+..+8+2=1018层,共跳8次到达LCA的下一层
 
const int N=5e5+10;
int n,m,s,a,b;
vector<int>e[N];
void dep[N],fa[N][20];

void dfs(int u,int father){//当前节点,父节点 
    dep[u]=dep[father]+1;//深度为父节点的深度+1 
    fa[u][0]=father;//当前节点往上走2的0次方即为父节点 
    for(int i=1;i<19;i++)
        fa[u][i]=fa[fa[u][i-1]][i-1];//分两半走 
        
    for(int  v:e[v])//往下找子节点dfs 
        if(v!=father)//不是父节点才能dfs 
        dfs(v,u);
    
}

int lcm(int u,int v){
    if(dep[u]<dep[v])swap(u,v);//让u>v这样有助于操作 
    
    for(int i=19;i>=0;i--){
        if(dep[fa[u][i]]<dep[v])//如果u即深度大的点比v的深度大,就让他往上走,直到两者深度相同 
        u=fa[u][i];
    }
    
    if(u==v)return v;//若是相等,直接返回 
    
    for(int i=19;i>=0;i--)
        if(fa[u][i]!=fa[v][i])
        u=fa[u][i];v=fa[v][i];//让两点往上走,始终保持两点父亲不相等,最后停在最近公共父节点的儿子 
        
    return fa[u][0];//返回最近公共父节点 
}
 

重点
dep[u]有u点的深度
fa[u][i]存从u点向上跳2^i层的祖先结点

打表:倍增递推,从大到小枚举
查询,二进制拆分,从大到小枚举
时间复杂度O((n+m)logn )
 

 Tarjan

Tarjan(塔杨)算法是一种离线算法,巧妙利用并查集维护祖先结点
e[u]存树边,e[1]=5;e[5]=1
query[u]存查询,query[3]={4,1},query[4]={3,1}
fa[u]存父节点,fa[5]=1,fa[2]=5
vis[u]打标机,vis[5]=true
ans[i]存查询结果,ans[1]=1,ans[2]=5

1.从根开始深搜遍历,入u时打标记
2.枚举u的儿子v,遍历完v的子树,回u时,把v指向u
3.遍历完u的儿子们,离u时枚举以u为起点的查询,若终点v被搜过,则查找v的根,即u,v的LCA,答案记入ans[]
4.递归遍历完整颗树,得到全部查询答案


 vector<int>e[N];//
 vector<pair<int,int>>query[N]; 
 int fa[N],vis[N],ans[M];
 
 
 
 int find(int u){//没有回来的结点的父节点都是指向自身 
     if(u==fa[u])return u;//两点之间的最近公共父节点的父节点都是自身 
     return fa[u]=find(fa[u]);//当找到指向自身的父节点时,即为两点的最近公共父节点 
 }
 int taryan(int u){
         vis[u]=true;//入u标记u 
         for(auto v:e[u]){//遍历子节点 
             if(!vis[v]){//没被标记过 
                 tarjan(v);//taryan递归 
                 fa[v]=u;//递归出来后更新父节点 
             }
         }
         //查询需要找最近公共父节点的两点 
    for(auto:q:query[u]){//求与该点的其他点的最近公共父节点 
        int v=q.first,i-q.second;//first为另一点,second为两点问题的编号  
        if(vis[v])ans[i]=find(v);//如果另一点vis为true即已标记过,则可求最近公共父节点 
    }
 }
 
 
 
 //main里
 for(int i=1;i>n;i++){//无向边都两边都放进去 
     scanf("%d%d",&a,&b);
     e[a].push_bcak(b);
     e[b].push)bcak(a);
 } 
 
 for(int i=1;i<=m;i++){//一个查询问题各自存两个,问题编号一致 
     scanf("%d%d",&a,&b);
     query(a).push_back({b,i});
     query(b).push_back({a,i});
 }
 
 for(int i=1;i<=N;i++)fa[i]=i;//初始化父节点指向自身 
 

  • 24
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值