学习笔记:最近公共祖先
最近公共祖先
最近公共祖先,简称 \(LCA\)
例题: P3379
倍增做法求 \(LCA\)
倍增做法是一种在线做法,时间复杂度为: \(O((n+m)\log n)\)
\(n\) 为点数,\(m\)为询问次数
算法详解
定义
首先我们先 \(dfs\) 预处理两个数组:
\(depth[i]\) :表示 \(i\) 号节点的深度
\(fa[i][j]\) :表示 \(i\) 号节点的 \(2^j\) 级祖先的编号
状态转移方程
然后我们再来思考怎么 \(dfs\) 时求出这两个数组:
由于 \(dfs\) 时我们传了两个参数: \(now\) 和 \(father\)
那么 \(now\) 节点的深度就等于 \(father\) 的深度加一
\(now\) 的 \(2^0\) 级祖先就等于 \(father\)
那么求 \(i>0\) 时 \(fa[now][i]\) 我们可以用 ST 表的思想
我们先让 \(now\) 跳 \(2^{i-1}\) 步,然后再跳 \(2^{i-1}\) 步
那么这时 \(now\) 就等于 \(now\) 的 \(2^i\) 级祖先
所以状态转移方程就是:
查询
这个过程一共分两步:
我们先假设深度目前深度更大的点为 \(x\) ,深度更小的节点为 \(y\)
那么如果不满足这个条件,就先交换两个节点
-
先让 \(x\) 跳到与 \(y\) 深度相同的祖先上:
那么这里就可以用多重背包的二进制优化的思想
假设需要跳 \(5\) 步,那么我们就可以先让 \(x\) 跳到 \(2^2\) 级祖先上,再跳到目前的 \(x\) 的 \(2^0\) 级祖先上
那么 \(x\) 就一共跳了 \(2^2+2^0=5\)步
-
再让 \(x\) 和 \(y\) 同时往上跳,直到跳到最近祖先上:
但是这样不太好处理,那么我们就可以先让 \(x\) 和 \(y\) 跳到 \(LCA\) 的下面那一层的节点,再跳到 \(LCA\)
那么我们就可以从二进制下位数高的枚举到位数低的,如果跳完后两个节点不同,那么就说明跳完后一定不会是 \(LCA\) 或 \(LCA\) 的祖先,那么就让他们往上跳
那么最后返回的结果就是 \(fa[x][0]\)
但是需要注意:有可能执行完第一步后 \(x\) 和 \(y\) 就已经相等了,那么这时就需要直接返回答案为\(x\)
Code
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+1;
vector<int> g[N];
int n,m,root,depth[N],fa[N][20];
void dfs(int now,int father) //预处理dpeth和fa数组
{
depth[now]=depth[father]+1;
fa[now][0]=father;
for (int i=1;i<=19;i++)
fa[now][i]=fa[fa[now][i-1]][i-1];
for (int to:g[now])
if (to!=father)
dfs(to,now);
}
int LCA(int x,int y) //倍增求LCA
{
if (depth[x]<depth[y])
swap(x,y);
for (int i=19;i>=0;i--)
if (depth[fa[x][i]]>=depth[y])
x=fa[x][i];
if (x==y)
return x;
for (int i=19;i>=0;i--)
if (fa[x][i]!=fa[y][i])
x=fa[x][i],y=fa[y][i];
return fa[x][0];
}
int main()
{
scanf("%d%d%d",&n,&m,&root);
for (int i=1,u,v;i<n;i++)
{
scanf("%d%d",&u,&v);
g[u].push_back(v);
g[v].push_back(u);
}
dfs(root,0);
for (int i=1,x,y;i<=m;i++) {
scanf("%d%d",&x,&y);
printf("%d\n",LCA(x,y));
}
return 0;
}
\(Tarjan\) 算法求 \(LCA\)
\(Tarjan\) 算法是一种离线算法,时间复杂度为 \(O(n+m)\)
算法详解
我们先把每个查询得两个节点的对应的节点和查询编号存下来
然后对树进行 \(dfs\) 一遍,在 \(dfs\) 时,将所有点分成三大类:
- 已经遍历过的点,标记为 2
- 正在搜索的点,标记为 1
- 还未搜索到的点,标记为 0
对于正在搜索的点,我们可以把他们全部合并到他们的根节点上
然后我们再找出与当前正在搜的点 \(x\) 有关的询问
如果对应的 \(y\) 已经被遍历过,那么他们的 \(LCA\) 就等于合并 \(y\) 合并到的节点编号 ( 建议自己画图理解一下 )
Code
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+1;
vector<int> g[N];
int fa[N],ans[N],vis[N];
vector< pair<int,int> > Q[N]; //Q[i].first存查询的另外一个点,Q[i].second存查询编号
int find(int x) {
if (fa[x]!=x) fa[x]=find(fa[x]);
return fa[x];
}
void tarjan(int x)
{
vis[x]=1;
for (int y:g[x])
if (!vis[y]) {
tarjan(y);
fa[y]=x;
}
vis[x]=2;
for (auto now:Q[x]) {
int y=now.first,id=now.second;
if (vis[y]==2) ans[id]=find(y);
}
}
int main()
{
int n,m,root;
scanf("%d%d%d",&n,&m,&root);
for (int i=1,u,v,val;i<n;i++)
{
scanf("%d%d",&u,&v);
g[u].push_back(v);
g[v].push_back(u);
}
for (int i=0,x,y;i<m;i++)
{
scanf("%d%d",&x,&y);
Q[x].push_back(make_pair(y,i));
Q[y].push_back(make_pair(x,i));
}
for (int i=1;i<=n;i++) fa[i]=i;
tarjan(root);
for (int i=0;i<m;i++) printf("%d\n",ans[i]);
return 0;
}
重链剖分求 LCA
不会重链剖分的可以看下 \(this\)
时间复杂度 \(O(n+m\log n)\),实际运行时间一般比倍增快很多
Code
#include<bits/stdc++.h>
using namespace std;
const int N=6e5;
vector<int> g[N];
int depth[N],fa[N];
int size[N],son[N],top[N];
void dfs1(int u,int f)
{
fa[u]=f,size[u]=1;
depth[u]=depth[f]+1;
for (int v:g[u])
if (v!=f) {
dfs1(v,u);
size[u]+=size[v];
if (size[v]>size[son[u]]) son[u]=v;
}
}
void dfs2(int u,int topf)
{
top[u]=topf;
if (son[u]) dfs2(son[u],topf);
for (int v:g[u])
if (!top[v])
dfs2(v,v);
}
int LCA(int x,int y)
{
while (top[x]!=top[y]) {
if (depth[top[x]]<depth[top[y]]) swap(x,y);
x=fa[top[x]];
}
return depth[x]<depth[y]?x:y;
}
int main()
{
int n,m,s,x,y;
scanf("%d%d%d",&n,&m,&s);
for (int i=1;i<n;i++) {
scanf("%d%d",&x,&y);
g[x].push_back(y);
g[y].push_back(x);
} dfs1(s,0),dfs2(s,s);
while (m--) {
scanf("%d%d",&x,&y);
printf("%d\n",LCA(x,y));
}
return 0;
}
欧拉序求 LCA
先对树跑遍 dfs,求出欧拉序,每次 dfs 到一个节点就把这个节点加入欧拉序,再遍历子节点,dfs 遍历完整棵子树后回到父节点再把父节点加入欧拉序
由于每条边最多进一次出一次,所以欧拉序的长度为 \(2n-1\)(起点一开始也要加)
再对欧拉序跑遍 ST 表,处理区间中深度最小的节点
询问时查询两点在欧拉序中第一次出现的位置间深度最小的节点即可
时间复杂度 \(O(n\log n+m)\)
Code
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+1,M=1e6;
int n,m,pos[N],depth[N];
int len,id[M],st[M][20];
vector<int> g[N]; int s;
int read() {
int x=0; char ch=0; while (!isdigit(ch) ) ch=getchar();
while (isdigit(ch) ) x=(x<<3)+(x<<1)+(ch&15),ch=getchar();
return x;
}
void dfs(int u,int fa) {
depth[u]=depth[fa]+1,id[++len]=u,pos[u]=len,st[len][0]=u;
for (int v:g[u]) if (v^fa) dfs(v,u),id[++len]=u,st[len][0]=u;
}
void init()
{
for (int j=1;j<20;j++)
for (int i=1;i+(1<<j)-1<=len;i++) {
int x=st[i][j-1],y=st[i+(1<<j-1) ][j-1];
st[i][j]=depth[x]<depth[y]?x:y;
}
}
int LCA(int x,int y)
{
int l=pos[x],r=pos[y];
if (l>r) swap(l,r); int k=log2(r-l+1);
x=st[l][k],y=st[r-(1<<k)+1][k];
return depth[x]<depth[y]?x:y;
}
int main()
{
n=read(),m=read(),s=read();
for (int i=1,x,y;i<n;i++)
{
x=read(),y=read();
g[x].push_back(y);
g[y].push_back(x);
}
dfs(s,0),init();
while (m--) printf("%d\n",LCA(read(),read() ) );
return 0;
}

浙公网安备 33010602011771号