【题目描述】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≤105,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,主要是因为函数多,方面广。