【暖*墟】 #初级数据结构# 倍增求LCA

一. 知识引入

LCA即最近公共祖先,即有根树中x,y的公共祖先中深度最大的一个节点。

求最近公共祖先的方法:暴力法,向上标记法,树上倍增法,Tarjan算法。

【“暴力”法】

先 dfs求出对应点的 dep(深度),深度大的向上跳到与深度小的同一深度,

比较是否相同,不相同的话,两者一起往上跳。

另一个例子

【向上标记法】O(N)

从x点向上走到根节点,标记所有经过的节点。

从y点向上走到根节点的过程中,第一次遇到的已经标记的节点就是lca。

【树上倍增法】O(logN) (在线)

扫描二维码关注公众号,回复: 2889753 查看本文章
设f[i][k]表示点i往上的第2^k个祖先。
首先我们用复杂度O(N*lgN)的dfs预处理出f数组, 递推式:f[i][k]=f[f[i][k-1]][k-1]。
意义为:【i的2^(k-1)辈祖先】的2^(k-1)辈祖先 == i的2^k辈祖先。

1.首先,将两个点跳到同一深度(用二进制拆分思想)。
二进制拆分:依次尝试从x向上走k=2^logn,...,2^1,2^0步,检查到达的结点是否比y深。
每次若检查成功,则令x=f[x,k],继续向上跳,若x=y,则已经找到了lca。

2.x、y同时向上调整并保持深度一致。
依次尝试把x、y同时向上走k=2^logn,...,2^1,2^0步,每次尝试过程中,
若f[x][k]!=f[y][k],则跳过去,令x=f[x,k],y=f[y,k]。否则不跳。

3.因为最后一步是:找到某个位置i,为最后一次f[a][i]!=f[b][i](再无法向上跳)。
所以,最终的答案为f[x][0](f[y][0]一样)。

【】

二. 例题详解

【例题1】洛谷p3379 lca模板题

#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

//例题 P3379【模板】最近公共祖先(LCA)

#define maxn 500500

struct Edge{
    int from,to;
}edges[2*maxn]; //边要乘2,因为是无向图
int firstt[maxn],nextt[2*maxn];  

int read(){  //读入优化
    int re=0; char ch=getchar();
    while(ch<'0' || ch>'9') ch=getchar();
    while(ch>='0' && ch<='9'){ 
        re=re*10+ch-'0'; ch=getchar();
    }
    return re;
}

int n,m,root;
int height[maxn];
float log2n; //用于二进制拆分思想:向上走k=2^logn,...,2^1,2^0步
int f[maxn][20]; //f[i][k]即点i往上的第2^k个祖先,则f[i][0]是父亲
int have[maxn]; //have[i]记录有没有找过

void dfs(int u,int h){ //【预处理】深度+f数组
    int v; height[u]=h; //u代表点的标号,h代表深度
    for(int i=1;i<=log2n;i++){ //不超过logn层
        if(h<=(1<<i)) break; //i是从小到大计算的,(1<<i>=h时可直接退出
        f[u][i]=f[f[u][i-1]][i-1]; //状态转移
    }
    int k=firstt[u];
    while(k!=-1){
        v=edges[k].to;
        if(!have[v]) have[v]=1, f[v][0]=u, dfs(v,h+1);
        //↑↑↑将要找的下一个点的父节点标为当前处理的节点u
        k=nextt[k];
    }
}

int require_LCA(int a,int b){
    int da=height[a],db=height[b];
    if(da!=db){ //【第一步】将a,b两点移到同样的深度
        if(da<db) swap(a,b),swap(da,db); //保证a的深度大于b 
        int d=da-db;
        for(int i=0;i<=log2n;i++) 
            if( (1<<i) & d) a=f[a][i]; //【位运算】
        //考虑到d是一个定值,而(1<<i)在二进制中只有第(i+1)位是1
        //那么d &(1<<i)得到的答案,如果某一位为1,那么表示可以向上移动
        //如果此时不移动,那么i增大了后就无法使height[a]==height[b]了 
    }

    //【第二步】找到某个位置i,在这个位置时最后一次f[a][i]!=f[b][i]。
    //从log2n开始从大到小枚举i,如果超过了a,b的高度,则令i继续减小。
    //如果没有超过a,b的高度,那么就判断移动了后会不会让a==b。
    //若a==b,则i继续减小; 否则,令此时的a=f[a][i],b=f[b][i]。
    if(a==b) return b;
    for(int i=log2n;i>=0;i--) {
        if(height[f[a][i]]<0) continue;
        if(f[a][i]==f[b][i]) continue;
        else a=f[a][i],b=f[b][i];
    }    
    return f[a][0];
}

int main(){
    n=read(); m=read(); root=read();
    memset(firstt,-1,sizeof(firstt));
    memset(nextt,-1,sizeof(nextt));

    int s,t,dsd=2*(n-1); //dsd用于编号
    for(int i=1;i<=dsd;i+=2) {
        s=read();t=read(); 
        edges[i].from=s; edges[i].to=t; //无向图双向建边
        edges[i+1].from=t; edges[i+1].to=s;
        nextt[i]=firstt[s]; firstt[s]=i; //链式前向星
        nextt[i+1]=firstt[t]; firstt[t]=i+1;
    }

    log2n=log(n)/log(2)+1; //求log2n,对无理数加上1或0.5可以减小误差
    memset(have,0,sizeof(have));
    memset(height,0,sizeof(height));
    memset(f,-1,sizeof(f));
    have[root]=1; dfs(root,1); //have[]记得初始化
                    
    for(int i=1;i<=n;i++)
        for(int j=0;j<=log2n;j++) 
            if(height[i] <=(1<<j) ) break;

    for(int i=0;i<m;i++){
        s=read();t=read();
        int y=require_LCA(s,t);
        printf("%d\n",y);
    }
    return 0;
}

【例题2】hdoj 2586 任意两点间距离

#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

/*【倍增求lca】hdoj 2586 
给你一个N个点的树和Q次查询,每次查询问你任意两点间距离。*/

//<u,v>距离=dist[u]+dist[v]-2*dist[LCA(u,v)]。其中dist存储节点到根的距离。

const int maxn=100005;
int n,q,x,y,z,now;
int nextt[maxn*2],firstt[maxn*2],go[maxn*2]; //边的连向关系
int dep[maxn],dist[maxn],edge[maxn];
int f[maxn][21]; //f[i][k]即点i往上的第2^k个祖先

int adds(int u,int v,int z){ //前向星建边,z为价值
    nextt[++now]=firstt[u]; firstt[u]=now; 
    edge[now]=z; go[now]=v;
    nextt[++now]=firstt[v]; firstt[v]=now; 
    edge[now]=z; go[now]=u;
}

void pre_dfs(int u,int fa){ //预处理
    for(int i=0;i<=19;i++) f[u][i+1]=f[f[u][i]][i];
    for(int e=firstt[u];e;e=nextt[e]){
        int v=go[e]; //找到下一条相连的边
        if(v==fa) continue;
        dep[v]=dep[u]+1; //深度
        dist[v]=dist[u]+edge[e]; //距离
        f[v][0]=u; pre_dfs(v,u); //记录father,递归
    }
}

int lca(int x,int y){ //找lca的主程序
    
    if(dep[x]<dep[y]) swap(x,y); //保证dep[x]>dep[y]
    
    for(int i=20;i>=0;i--){ //注意:这里的20和上面的19都是log2n的近似取值
        if(dep[f[x][i]]>=dep[y]) x=f[x][i]; 
        //↑↑↑i的2^k辈祖先的结点仍比y深,令x=f[x,i],继续向上跳
        if(x==y) return x; //若x=y,则已经找到了lca
    }

    for(int i=20;i>=0;i--) //↓↓↓未找到lca时的倍增跳法
        if(f[x][i]!=f[y][i]){ x=f[x][i]; y=f[y][i]; } 

    return f[x][0]; //最终的答案为f[x][0]
}

int main(){
    int T; scanf("%d",&T);
    while(T--){
        scanf("%d%d",&n,&q);
        for(int i=1;i<=n;i++) firstt[i]=nextt[i]=dep[i]=0;
        now=0; dep[1]=1; //↓↓↓读入带权树
        for(int i=1;i<n;i++){ scanf("%d%d%d",&x,&y,&z); adds(x,y,z); }
        pre_dfs(1,0); //从根节点fa=0开始dfs预处理
        while(q--){
            scanf("%d%d",&x,&y);
            printf("%d\n",dist[x]+dist[y]-2*dist[lca(x,y)]);
        }
    }
    return 0;
}

                                               ——时间划过风的轨迹,那个少年,还在等你。

猜你喜欢

转载自blog.csdn.net/flora715/article/details/81873974
今日推荐