嘿,LCA的题目可是我最喜欢的题目了,今天我就来讲讲如何求LCA?
对于如何LCA,我只会几种,不过比较实用。我重点去讲倍增求LCA。
倍增求LCA
倍增?什么是倍增?
字面上的意思便是成倍增长,其实也就是ST表(不懂的上网搜一搜 )
言归正传,那我们该怎么用倍增求LCA呢?
首先,我们设
f
i
,
j
f_{i,j}
fi,j 表示从
i
i
i 这个点向上跳
2
j
2^j
2j 步所到达的点。
然后我们可以一遍dfs,处理出所有的
f
i
,
0
f_{i,0}
fi,0 即每个点的父亲。
接着我们考虑如何转移:
首先我们先从当前这个点
i
i
i 向上跳
2
j
−
1
2^{j-1}
2j−1,再向上跳
2
j
−
1
2^{j-1}
2j−1
结合图片理解一下:
即:
f
i
,
j
=
f
f
i
,
j
−
1
,
j
−
1
f_{i,j}=f_{f_{i,j-1},j-1}
fi,j=ffi,j−1,j−1
好了,我们继续。
现在我们知道两个点
x
x
x 和
y
y
y ,我们要求他们的LCA,如何求?
首先,我们想要是它们两个都在同一起点,一起向上跳,是不是方便很多?
那么我们假设
y
y
y 的深度比
x
x
x 的深,那么
我们先让
y
y
y 跳到与
x
x
x 同样的深度然后再让它们一起跳
注意特判一点,当它们跳到统一深度时
x
=
y
x=y
x=y 那么直接返回
x
x
x 即可
但是,现在又衍生出来一个问题:怎么跳?
那我们就得回到我们那个倍增数组了
我们看,这个
2
j
2^j
2j ,我们可以运用二进制拆分的思想,即一个数可以被拆解成若干个2的幂相加
那么我们既可以去倒着枚举这个2的指数,至于为什么,你想啊,要是我们正着枚举,我们怎么能判断下一个选的是否符合条件(组成最终的那个数)呢?
那么我们是不是可以上代码了?
//dep[x]表示的是节点x的深度
//maxd指的是最深的深度
//预处理
for (int j=1;j<=log2(maxd);j++){//一定要先枚举j
for (int i=1;i<=n;i++){
f[i][j]=f[f[i][j-1]][j-1];
}
}
//求LCA
int LCA(int x,int y){
if (dep[x]>dep[y]){
int t=x;
x=y;
y=t;
}
if (dep[y]>dep[x]){
for (int i=log2(dep[y]-dep[x]);i>=0;i--){
if (dep[f[y][i]]>dep[x]){
y=f[y][i];
}
}
y=f[y][0];
}
if (x==y) return x;
for (int i=log2(dep[x]);i>=0;i--){
if (f[x][i]!=f[y][i]){
x=f[x][i];
y=f[y][i];
}
}
return f[x][0];//最终还有一步没有跳完,这很重要
}
对于树上倍增,它的例题一般来说比较简单,但一般它的题目不会那么简单。
给道例题:
Description
Input
Output
Sample Input
1
5 7
1 2 2
1 4 1
2 4 2
4 3 2
2 3 1
4 5 1
1 5 2
Sample Output
5
No
解析
首先,我们肯定要求一个最小生成树(别告诉我你不会 ),然后把这棵树存下来。
接着我们枚举每一条不在最小生成树中的边,找到这条边的起点终点在原最小生成树的路径中找到一条最大的边删去并加入这个边,得出一个非严格次小生成树。
然后比较,如果得到的非严格次小生成树=原最小生成树,则输出No,否则输出Yes
我们怎么去找路径呢?
很明显,我们可以用倍增LCA来做。
但是我们怎么去找最大的那条边呢?
我们可以多设一个数组
g
i
,
j
g_{i,j}
gi,j 表示从
i
i
i 这个点向上跳
2
j
2^j
2j 的路径中,最大的边权。
如何预处理?
g
i
,
j
=
m
a
x
(
g
i
,
j
−
1
,
g
f
i
,
j
−
1
,
j
−
1
)
g_{i,j}=max(g_{i,j-1},g_{f_{i,j-1},j-1})
gi,j=max(gi,j−1,gfi,j−1,j−1)
特别的:
g
i
,
0
=
g_{i,0}=
gi,0= 连向点
i
i
i 那条边的边权
很不错吧?
来看看我那2103bytes的代码吧(事实上这道题完全不用这么做,不过我只是为了引入一个倍增的拓展而已)
#include<cmath>
#include<math.h>
#include<cstdio>
#include<algorithm>
#define ll long long
using namespace std;
struct node{
ll x,y,w;
}a[400010];
struct name{
ll to,next,w;
}e[400010];
bool bz[400010];
int n,m,T,cnt,root;
int fa[200010],head[200010];
ll f[100010][30],g[100010][30],dep[100010],ss,md;
void add(int u,int v,ll w){
e[++cnt].to=v;
e[cnt].w=w;
e[cnt].next=head[u];
head[u]=cnt;
return;
}
bool cmp(const node & c,const node & d){
return c.w<d.w;
}
int find(int bb){
if (bb!=fa[bb]) fa[bb]=find(fa[bb]);
return fa[bb];
}
void dfs(int u,int da){
for (int i=head[u];i;i=e[i].next){
int v=e[i].to;
if (v==da) continue;
f[v][0]=u;
g[v][0]=e[i].w;
dep[v]=dep[u]+1;
if (dep[v]>md)
md=dep[v];
dfs(v,u);
}
return;
}
ll LCA(int x,int y){
if (dep[x]>dep[y]){
int t=x;
x=y;
y=t;
}
if (dep[x]<dep[y]){
for (int j=log2(dep[y]-dep[x]);j>=0;j--){
if (dep[f[y][j]]>dep[x]){
ss=max(ss,g[y][j]);
y=f[y][j];
}
}
y=f[y][0];
}
if (x==y)
return ss;
for (int j=log2(dep[x]);j>=0;j--){
if (f[x][j]!=f[y][j]){
ss=max(ss,max(g[x][j],g[y][j]));
x=f[x][j];
y=f[y][j];
}
}
ss=max(ss,max(g[x][0],g[y][0]));
x=f[x][0];
y=f[y][0];
return x;
}
int main(){
scanf("%d",&T);
while (T--){
cnt=0;
scanf("%d%d",&n,&m);
for (int i=1;i<=m;i++)
scanf("%lld%lld%lld",&a[i].x,&a[i].y,&a[i].w);
for (int i=1;i<=n;i++)
fa[i]=i,head[i]=0,dep[i]=0,bz[i]=0;
sort(a+1,a+1+m,cmp);
ll num=0,ans=0;
for (int i=1;i<=m;i++){
int r1=find(a[i].x),
r2=find(a[i].y);
if (r1!=r2){
fa[r2]=r1;
ans+=a[i].w;
num++;
add(r1,r2,a[i].w);
add(r2,r1,a[i].w);
bz[i]=1;
}
if (num==n-1) break;
}
root=1;
bool bj=0;
dep[root]=1;
dfs(root,0);
for (int j=1;j<=log2(md);j++){
for (int i=1;i<=n;i++){
f[i][j]=f[f[i][j-1]][j-1];
g[i][j]=max(g[i][j-1],g[f[i][j-1]][j-1]);
}
}
for (int i=1;i<=m;i++){
if (!bz[i]){
ss=0;
ll lca=LCA(a[i].x,a[i].y);
if (ss==a[i].w){
bj=1;
break;
}
}
}
printf("%lld\n",ans);
if (bj) puts("No");
else puts("Yes");
}
return 0;
}
我就不打注释了,自己感性理解一下。
总结
求LCA还可以用Tarjan(离线)来求,用上并查集,我只打过一次,
当然,还可以用树链剖分来做,虽然我这个蒟蒻不会QAQ。
这周一回来看见我的blog暴增800阅读量,好开心。(题外话)
欢迎大家指出问题!