#10130. 「一本通 4.4 例 1」点的距离 (数据结构倍增求LCA)

【题目描述】LOJ题在这

给定一棵 nnn 个点的树,QQQ 个询问,每次询问点 xxx 到点 yyy 两点之间的距离。

【输入格式】

第一行一个正整数 nnn,表示这棵树有 nnn 个节点;

接下来 n−1n-1n−1 行,每行两个整数 x,yx,yx,y 表示 x,yx,yx,y 之间有一条连边;

然后一个整数 QQQ,表示有 QQQ 个询问;

接下来 QQQ 行每行两个整数 x,yx,yx,y 表示询问 xxx 到 yyy 的距离。

【输出格式】

输出 QQQ 行,每行表示每个询问的答案。

【样例输入】

6
1 2
1 3
2 4
2 5
3 6
2
2 6
5 6

【样例输出】

3
4

【数据范围与提示】

对于全部数据,1≤n,m≤105,1≤x,y≤n1\le n,m\le 10^5,1\le x,y\le n1≤n,m≤10​5​​,1≤x,y≤n。

思路:对于这道题,其实也是caioj里面的任意点的距离caioj任意点的距离题在这,但是在解释这两道题之前,我还是要先介绍一个最近公共祖先最原始的模板,caioj最近公共祖先题在这,caioj上面任意点的距离也算是模板,但是没有那么原始,而LOJ和caioj的题目是差不多的,理解了思路都能做出来。

然后我还是直接上最近公共祖先的原始模板题

最近公共祖先代码实现】(有三种解法:数组,结构体和树链剖分)

【第一种数组:这个我写的比较详细,所以就不多说,大家看代码实现吧】

视频大家在http://caioj.cn/

/*
运用了1239的思路
f[i][j]代表的是 i这个节点的第2的j次方个父亲
如:2^0=1,就是父亲 ; 2^1=2,就是爷爷 ;2^2=4,就是爷爷的爷爷 
f[i][0] = fa[i] 表示i的father
f[i][1] = fa[fa[i]] 表示i的爷爷
以此类推,f[i][2]就是爷爷的爷爷, f[i][3].....
 
f[i][j]=
f[f[i][j-1][j-1]
假设j=2,那么j-1就是1 
f[i][j-1]我的爷爷 f[i][j-1]我的爷爷的j-1的爷爷 
f[i][j]=f[i][2]
f[i][j-1]=f[i][1]就是2的一次方=2,就是父亲的父亲就是爷爷,那么这个的2的j-1次方就是2的一次方=2,就是爷爷 
f[f[i][j-1][j-1] f[i][j-1]就是爷爷,那么f[f[i][j-1][j-1]就是爷爷的爷爷 = f[i][2] 
*/
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
struct node
{
    int x,y,next;
}a[210000]; int len,last[110000];
int dep[110000];
int f[110000][25];
void ins(int x,int y)
{
    len++;
    a[len].x=x; a[len].y=y;
    a[len].next=last[x]; last[x]=len;
}
void bt(int x,int fa)
{
    dep[x]=dep[fa]+1;//每当我进来了,我这个点的层数就是我父亲的层数+1 
    f[x][0]=fa;//我的f[x][0]就是记录一下我的父亲 
    for(int i=1;(1<<i)<=dep[x];i++)
        f[x][i]=f[f[x][i-1]][i-1];//用x这个节点的第2的i次方个父亲 
        //就是 x这个节点的第2的i-1次方的父亲的第2的i-1次方个父亲 
    for(int k=last[x];k;k=a[k].next)
    {
        int y=a[k].y;
        if(y==fa) continue;
        bt(y,x); 
    }
}
int LCA(int x,int y)
{
    if(dep[x]<dep[y]) {int t=x;x=y;y=t;}//swap(x,y);
    //先让x在比较深的层数里,再让x往上跳,调到和y一样的层数,每一个层数的点都可以分为一个二进制的数
    //如:5可以分为101 
      
    for(int i=20;i>=0;i--)//从i的20次方到i的0次方来算,0次方就是1
        if(dep[x]-dep[y]>=(1<<i))//dep[x]-dep[y]就是相差的层数 //1<<i就是位运算表示 2^i
            x=f[x][i];
    /*为什么从幂大到幂小的递减呢?
    因为:比如说: 2^0+....2^i-1 < 2^i
    如果一个数比我的值要小,那么他肯定在我的二进制的表达式中有一席之地 
    比如说:5,当遇到2^2这个点的时候就是4,发现比自己小,因为调到2^3=8不行,就到4,发现可以
    就可以一直寻找 
    */
    if(x==y) return x;//同一个家族
    for(int i=20;i>=0;i--) 
        if(dep[x]>=(1<<i) && f[x][i]!=f[y][i])
        /*
        f[x][i]!=f[y][i]:解释 
        假设我们当前有两个点,从大到小往上跳,有可能跳着跳着就跳过头,虽然跳过头的这个点会是
        他们的公共祖先,但不是最近公共祖先,所以不能作为答案,所以只能尽量使他逐渐变小,
        不会超过最近公共祖先,使他控制在公共祖先那里 
        */  
        {
            x=f[x][i];
            y=f[y][i];
        }
        return f[x][0];
}
int main()
{
    int n,m; scanf("%d%d",&n,&m);
    len=0; memset(last,0,sizeof(last));//初始化
    for(int i=1;i<n;i++)
    {
        int x,y; scanf("%d%d",&x,&y);
        ins(x,y); ins(y,x);//建边 
    }
    bt(1,0);//也算是初始化
    while(m--)
    {
        int x,y; scanf("%d%d",&x,&y);
        printf("%d\n",LCA(x,y));
    }
    return 0;
}

【第二种结构体:这一种其实和数组的思路是一样的,只是换成了结构体dis,dep,par而已,看上面的函数部分的解释】

#include<cstdio>
#include<cstring>
using namespace std;
struct node
{
    int x,y,next;
}a[210000];int len,last[210000];
struct trnode
{
    int dis,dep;
    int par[20];
}t[210000];
void ins(int x,int y)
{
    len++;
    a[len].x=x;a[len].y=y;
    a[len].next=last[x];last[x]=len;
}
void bt(int x,int fa,int dis)
{
    t[x].dep=t[fa].dep+1; t[x].dis=dis;
    t[x].par[0]=fa;
    for(int i=1;t[x].dep>=(1<<i);i++)
    {
        t[x].par[i]=t[t[x].par[i-1]].par[i-1];
    }
    for(int k=last[x];k;k=a[k].next)
    {
        int y=a[k].y;
        if(y!=fa)
        {
            bt(y,x,dis);
        }
    }
}
int LCA(int x,int y)
{
    if(t[x].dep<t[y].dep) {int t=x;x=y;y=t;} //{x^=y^=x^=y;}
    for(int i=18;i>=0;i--)
    {
        if(t[x].dep-t[y].dep>=(1<<i)) x=t[x].par[i];
    }
    if(x==y) return x;
    for(int i=18;i>=0;i--)
    {
        if(t[x].dep>=(1<<i) && t[x].par[i]!=t[y].par[i])
        {
            x=t[x].par[i];
            y=t[y].par[i];
        }
    }
    return t[x].par[0];
}
int main()
{
    int n,m; scanf("%d%d",&n,&m);
    len=0; memset(last,0,sizeof(last));
    for(int i=1;i<n;i++)
    {
        int x,y; scanf("%d%d",&x,&y);
        ins(x,y); ins(y,x);
    }
    bt(1,0,0);
    for(int i=1;i<=m;i++)
    {
        int x,y; scanf("%d%d",&x,&y);
        printf("%d\n",LCA(x,y));
    }
    return 0;
}

【第三种:树链剖分,反正我当时是考试的时候打出来的,后面发现改变的真的很少】

视频在 caioj.cn找

#include<cstdio>
#include<cstring>
using namespace std;
int max(int x,int y){return x>y?x:y;}
struct node
{
    int x,y,next;
}a[210000]; int len,last[110000];
void ins(int x,int y)
{
    len++;
    a[len].x=x; a[len].y=y;
    a[len].next=last[x]; last[x]=len;
}
int n,b[110000],fa[110000],dep[110000],son[110000],tot[110000];
int z,ys[110000],yss[110000],top[110000];
struct trnode
{
    int l,r,lc,rc,c;
}tr[210000]; int trlen;
void bt(int l,int r)
{
    trlen++; int now=trlen;
    tr[now].l=l; tr[now].r=r; tr[now].c=0;
    tr[now].lc=tr[now].rc=-1;
    if(l<r)
    {
        int mid=(l+r)/2;
        tr[now].lc=trlen+1; bt(l,mid); 
        tr[now].rc=trlen+1; bt(mid+1,r);
    }
}
void dfs1(int x)
{
    tot[x]=1; son[x]=0;
    for(int k=last[x];k;k=a[k].next)
    {
        int y=a[k].y;
        if(y!=fa[x])
        {
            fa[y]=x;
            dep[y]=dep[x]+1;
            dfs1(y);
            if(tot[son[x]]<tot[y]) son[x]=y;
            tot[x]+=tot[y];
        }
    }
}
void dfs2(int x,int tp)
{
    ys[x]=++z; yss[z]=x; top[x]=tp;
    if(son[x]!=0) dfs2(son[x],tp);
    for(int k=last[x];k;k=a[k].next)
    {
        int y=a[k].y;
        if(y!=son[x] && y!=fa[x])
        dfs2(y,y);
    }
}
int solve(int x,int y)
{
    int tx=top[x]; int ty=top[y]; 
    while(tx!=ty)
    {
        if(dep[tx]>dep[ty])
        {
            int t=x;x=y;y=t;
            t=tx;tx=ty;ty=t;
        }
        y=fa[ty]; ty=top[y];
    }
    if(dep[x]<dep[y]) {int t=x;x=y;y=t;}
    return y;
}
int main()
{
    int m; scanf("%d%d",&n,&m);
    len=0; memset(last,0,sizeof(last));
    for(int i=1;i<n;i++)
    {
        int x,y; scanf("%d%d",&x,&y);
        ins(x,y); ins(y,x);
    }
    dep[1]=1; fa[1]=0;dfs1(1);
    z=0; dfs2(1,1);
    trlen=0; bt(1,z);
    while(m--)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        printf("%d\n",solve(x,y));
    }
    return 0;
}

然后如果你上面都理解了,那我们看一下caioj上面任意点距离的代码,也有两个模板:树链剖分和结构体

【第一种:树链剖分:树链剖分是真的要学会很有用的,而且其实也不难至少,不能和平衡树相提并论】

#include<cstdio>
#include<cstring>
using namespace std;
struct node
{
    int x,y,c,next;
}a[41100];int len,last[41100];
void ins(int x,int y,int c)
{
    len++;
    a[len].x=x;a[len].y=y;a[len].c=c;
    a[len].next=last[x];last[x]=len;
}
int n,m,b[41100],fa[41100],dep[41100],son[41100],tot[41100],deep[41100];//最近祖先到根节点的距离 
int z,ys[41100],yss[41100],top[41100];
int min(int x,int y){return x<y?x:y;}
struct trnode
{
    int l,r,lc,rc,c;
}tr[41100];int trlen;
void bt(int l,int r)
{
    trlen++;int now=trlen;
    tr[now].l=l;tr[now].r=r;tr[now].c=0;
    tr[now].lc=tr[now].rc=-1;
    if(l<r)
    {
        int mid=(l+r)/2;
        tr[now].lc=trlen+1; bt(l,mid);
        tr[now].rc=trlen+1; bt(mid+1,r);
    }
}
void dfs(int x)
{
    son[x]=0; tot[x]=1;
    for(int k=last[x];k;k=a[k].next)
    {
        int y=a[k].y;
        if(y!=fa[x])
        {
            fa[y]=x;
            dep[y]=dep[x]+1;
            deep[y]=min(deep[y],deep[x]+a[k].c);
            dfs(y);
            if(tot[son[x]]<tot[y]) son[x]=y;
            tot[x]+=tot[y];
        }
    }
}
void dfs2(int x,int tp)
{
    ys[x]=++z; yss[z]=x; top[x]=tp;
    if(son[x]!=0) dfs2(son[x],tp);
    for(int k=last[x];k;k=a[k].next)
    {
        int y=a[k].y;
        if(y!=fa[x] && y!=son[x])
        dfs2(y,y);
    }
}
int solve(int x,int y)
{
    int tx=top[x]; int ty=top[y]; int ans=0;
    ans=deep[x]+deep[y];
    while(tx!=ty)
    {
        if(dep[tx]>dep[ty]) 
        {
            int t=x;x=y;y=t;
            t=tx;tx=ty;ty=t;
        }
        y=fa[ty]; ty=top[y];
    }
    if(dep[x]<dep[y]) {int t=x;x=y;y=t;}
    ans=ans-deep[y]*2;
    return ans;
}
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<n;i++)
    {
        int x,y,c;
        scanf("%d%d%d",&x,&y,&c);
        ins(x,y,c); ins(y,x,c);
    }
    memset(deep,63,sizeof(deep)); deep[1]=0;
    dep[1]=1; fa[1]=0; dfs(1);
    z=0; dfs2(1,1); 
    trlen=0; bt(1,z);
    for(int i=1;i<=m;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        printf("%d\n",solve(x,y));
    }
    return 0;
}

【第二种:结构体:这一个代码我写了一些注释,联系公共祖先的数组解释就是函数那一块,会很好理解的】

#include<cstdio>
#include<cstring>
using namespace std;
struct node
{
    int x,y,d,next;
}a[21000]; int len,last[21000];
struct trnode
{
    int dis,dep;
    int par[20]; //par[i]表示点x的2^i的父亲是谁 
}t[21000];
void ins(int x,int y,int d)
{
    len++;
    a[len].x=x; a[len].y=y; a[len].d=d;
    a[len].next=last[x]; last[x]=len;
}
void bt(int x,int fa,int dis)
{
    t[x].dep=t[fa].dep+1; t[x].dis=dis;
    t[x].par[0]=fa; //t[x]的2^0个父亲 
    for(int i=1;t[x].dep>=(1<<i);i++) //构建好par数组 
    {
        t[x].par[i]=t[t[x].par[i-1]].par[i-1];
    }
    for(int k=last[x];k;k=a[k].next)
    {
        int y=a[k].y;
        if(y!=fa)
        {
            bt(y,x,dis+a[k].d);
        }
    }
}
int LCA(int x,int y)
{
    if(t[x].dep<t[y].dep) {int t=x;x=y;y=t;}//使x始终深于y,方便操作 
    for(int i=18;i>=0;i--)//2的2^0 到 2^maxd 能组成 1 到 N 的所有数字 
    {
        if(t[x].dep-t[y].dep>=(1<<i))
        {
            x=t[x].par[i];//使x和y同一深度 
        }
    }
    if(x==y) return x;//如果一个恰好是另一个的祖先 
    for(int i=18;i>=0;i--)
    {
        if(t[x].dep>=(1<<i) && t[x].par[i]!=t[y].par[i])
        //目的是让x跟y尽可能接近公共祖先,为什么不让 t[x].par[i]==t[y].par[i]? 
        {
            x=t[x].par[i];
            y=t[y].par[i];//一起往上跳,保持两者深度始终相等 
        }
    }
    return t[x].par[0];//往上跳一步 
}
int main()
{
    int n,m; scanf("%d%d",&n,&m);
    len=0; memset(last,0,sizeof(last));
    for(int i=1;i<n;i++)
    {
        int x,y,d; scanf("%d%d%d",&x,&y,&d);
        ins(x,y,d); ins(y,x,d);
    }
    bt(1,0,0); 
    for(int i=1;i<=m;i++)
    {
        int x,y; scanf("%d%d",&x,&y);
        int k=LCA(x,y);
        int ans=t[x].dis+t[y].dis-2*t[k].dis;
        printf("%d\n",ans);
    }
    return 0;
}

这两个是caioj上面的题目,最后我们看一下改变之后,LOJ题目的代码

【LOJ任意点代码实现】

#include<cstdio>
#include<cstring>
using namespace std;
struct node
{
    int x,y,next;
}a[210000]; int len,last[110000];
struct trnode
{
    int dis,dep;
    int par[20]; //par[i]表示点x的2^i的父亲是谁 
}t[110000];
void ins(int x,int y)
{
    len++;
    a[len].x=x; a[len].y=y;
    a[len].next=last[x]; last[x]=len;
}
void bt(int x,int fa,int dis)
{
    t[x].dep=t[fa].dep+1; t[x].dis=dis;
    t[x].par[0]=fa; //t[x]的2^0个父亲 
    for(int i=1;t[x].dep>=(1<<i);i++) //构建好par数组 
    {
        t[x].par[i]=t[t[x].par[i-1]].par[i-1];
    }
    for(int k=last[x];k;k=a[k].next)
    {
        int y=a[k].y;
        if(y!=fa)
        {
            bt(y,x,dis+1);
        }
    }
}
int LCA(int x,int y)
{
    if(t[x].dep<t[y].dep) {int t=x;x=y;y=t;}//使x始终深于y,方便操作 
    for(int i=18;i>=0;i--)//2的2^0 到 2^maxd 能组成 1 到 N 的所有数字 
    {
        if(t[x].dep-t[y].dep>=(1<<i))
        {
            x=t[x].par[i];//使x和y同一深度 
        }
    }
    if(x==y) return x;//如果一个恰好是另一个的祖先 
    for(int i=18;i>=0;i--)
    {
        if(t[x].dep>=(1<<i) && t[x].par[i]!=t[y].par[i])
        //目的是让x跟y尽可能接近公共祖先,为什么不让 t[x].par[i]==t[y].par[i]? 
        {
            x=t[x].par[i];
            y=t[y].par[i];//一起往上跳,保持两者深度始终相等 
        }
    }
    return t[x].par[0];//往上跳一步 
}
int main()
{
    int n; scanf("%d",&n);
    len=0; memset(last,0,sizeof(last));
    for(int i=1;i<n;i++)
    {
        int x,y,d; scanf("%d%d",&x,&y);
        ins(x,y); ins(y,x);
    }
    bt(1,0,0);
    int m; scanf("%d",&m); 
    for(int i=1;i<=m;i++)
    {
        int x,y; scanf("%d%d",&x,&y);
        int k=LCA(x,y);
        int ans=t[x].dis+t[y].dis-2*t[k].dis;
        printf("%d\n",ans);
    }
    return 0;
}

还有一个就是书上面的数组的代码,我也上传上来,但是他设置的比较复杂,不太推荐

【书上代码实现】

#include<iostream>  
#include<algorithm>  
#include<cstdio>  
#include<cstdlib>  
using namespace std;  
const int ONE=100005;  
int n,Q;  
int x,y;  
int next[ONE*2],first[ONE*2],go[ONE*2],tot;  
int Dep[ONE];  
int f[ONE][21];  
int Add(int u,int v)               //连边
{  
    next[++tot]=first[u]; first[u]=tot; go[tot]=v;  
    next[++tot]=first[v]; first[v]=tot; go[tot]=u;  
}  
void Deal_first(int u,int father)     //预处理 
{  
    Dep[u]=Dep[father]+1;  
    for(int i=0;i<=19;i++)  
    {  
        f[u][i+1]=f[f[u][i]][i];  
    }  
    for(int e=first[u];e;e=next[e])  
    {  
        int v=go[e];  
        if(v==father) continue;  
        f[v][0]=u;  
        Deal_first(v,u);  
    }  
}  
int LCA(int x,int y)              //找lca
{  
    if(Dep[x]<Dep[y]) swap(x,y);  
    for(int i=20;i>=0;i--)  
    {  
        if(Dep[f[x][i]]>=Dep[y]) x=f[x][i];  
        if(x==y) return x;  
    }  
    for(int i=20;i>=0;i--)  
    {  
        if(f[x][i]!=f[y][i])  
        {  
            x=f[x][i];  
            y=f[y][i];  
        }  
    }  
    return f[x][0];  
}  
int dist(int x,int y)               //求距离
{  
    return Dep[x]+Dep[y]-2*Dep[LCA(x,y)];  
}  
int main()  
{  
    cin>>n;  
    for(int i=1;i<n;i++)  
    {  
        scanf("%d%d",&x,&y);  
        Add(x,y);  
    }  
    Deal_first(1,0);  
    cin>>Q;  
    while(Q--)  
    {  
        scanf("%d%d",&x,&y);  
        printf("%d\n",dist(x,y));  
    }  
}  

 最后总的来说这道题还是蛮经典的,大家一定要搞懂,而且一定要记住这是LCA,求公共祖先的代码,然后还有就是,几种代码都要深入理解,特别是最近公共祖先,因为模板题不仅仅是做出来,更主要的一定是深刻理解,将算法的发明人的精髓理解了,这个算法才算完成。 难度系数大概为6.5,主要是因为函数多,方面广。

猜你喜欢

转载自blog.csdn.net/qq_42367531/article/details/82049536
今日推荐