一. 知识引入
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;
}
——时间划过风的轨迹,那个少年,还在等你。