最近公共祖先LCA
最近公共祖先LCA
倍增法求LCA
vector<int> e[500010];
int dep[500010],fa[500010][20];
void solve(){
cin>>n>>m>>s;
for (int i=1;i<=n-1;i++){
int u,v;
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
dfs(s,0);
while (m--){
int a,b;
cin>>a>>b;
cout<<lca(a,b)<<endl;
}
}
定义 \(Fa_{i,j}\) 表示 \(i\) 节点向上跳 \(2^j\) 层的祖先节点,可以由递推式得来
\[Fa_{i,j}=Fa_{Fa_{i,j-1},{j-1}}
\]
首先DFS遍历树,每次记录节点的深度以及该节点的父节点,即 \(Fa_{x,0}=p\)
void dfs(int x,int p){
dep[x]=dep[p]+1;//当前节点深度为父节点深度+1
fa[x][0]=p;
for (int i=1;i<=19;i++)//倍增法递推
fa[x][i]=fa[fa[x][i-1]][i-1];
for (auto v:e[x]){//DFS叶子节点
if (v==p) continue;
dfs(v,x);
}
}
利用 \(Fa_{i,j}\) 计算 LCA
首先需要将节点 \(x,y\) 跳到同一层,然后两个节点一起向上跳到最近公共祖先的下一层
int lca(int x,int y){
if (dep[x]<dep[y]) swap(x,y);//保证x的深度较大
for (int i=19;i>=0;i--){
if (dep[fa[x][i]]>=dep[y])//将深节点x跳到y节点同一层上
x=fa[x][i];
}
if (x==y) return y;//如果y是x的祖先节点,那么直接返回
for (int i=19;i>=0;i--){
if (fa[x][i]!=fa[y][i])
x=fa[x][i],y=fa[y][i];//x,y一起向上跳
}
return fa[x][0];//返回当前节点的父节点,即x,y对应的LCA
}
Tarjan算法求LCA
这是一种离线算法,需要对询问进行处理后再逐步求得所有答案
主要采用并查集的维护方式,自底向上对可以求得的询问进行回答
int n,m,s;
vector<int> e[500010];
vector<pair<int,int>> q[500010];
bool vis[500010];
int fa[500010];
int ans[500010];
int find(int x){
if (x==fa[x]) return x;
return fa[x]=find(fa[x]);//路径压缩不影响答案
}
void solve(){
cin>>n>>m>>s;
for (int i=1;i<n;i++){
int u,v;
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
for (int i=1;i<=m;i++){
int a,b;
cin>>a>>b;
if (a==b){
ans[i]=a;
continue;
}
q[a].push_back({b,i});
q[b].push_back({a,i});
}
for (int i=0;i<=n;i++) fa[i]=i;
tarjan(s);
for (int i=1;i<=m;i++) cout<<ans[i]<<endl;
}
递归到底层返回时,查找与这个点有关的询问 [a,b] 如果另外一个节点已经被访问过了,那么就可以计算答案
依靠DFS序得到的公共祖先一定是最近的,因为只有在处理完节点有关的询问后才会回溯到上一层
void tarjan(int x){
vis[x]=1;//标记访问
for (auto v:e[x]){
if (vis[v]) continue;
tarjan(v);//递归
fa[v]=x;
}
for (auto it:q[x]){
if (vis[it.first])//如果另外一个节点也被访问过
ans[it.second]=find(it.first);//计算答案
}
}
树链剖分求LCA
重儿子:父节点所有儿子中子树节点最多的儿子,轻儿子:父节点除开重儿子之外的儿子
重边:连接父节点与他的重儿子的边,轻边:连接父节点与他轻儿子的边
重链:多条连续重边构成的路径
引理:
- 一棵树能够被剖分为若干条重链
- 重链的顶点一定是一个轻儿子
- 树上任意一条连接 \(n\) 个节点路径可以被切分为不超过 \(\log n\) 条重链

图引自OIWIKI
vector<int> e[500010];
int dep[500010],sz[500010];
int fa[500010],son[500010];
int top[500010];
void solve(){
cin>>n>>m>>s;
for (int i=1;i<n;i++){
int u,v;
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
dfs1(s,0);
dfs2(s,s);
for (int i=1;i<=m;i++){
int a,b;
cin>>a>>b;
cout<<lca(a,b)<<endl;
}
}
需要两次DFS处理出不同的信息
void dfs1(int x,int p){
dep[x]=dep[p]+1;
fa[x]=p;
sz[x]=1;
for (auto v:e[x]){
if (v==p) continue;
dfs1(v,x);
sz[x]+=sz[v];
if (sz[son[x]]<sz[v]) son[x]=v;//v的子树大小如果比之前记录的重儿子更大,那么更新重儿子
}
}
首先需要通过第一次 DFS 处理出每个节点的深度,父亲以及他形成的子树的大小
son[i] 数组记录 i 节点的重儿子编号,通过每个儿子的子树大小进行更新
void dfs2(int x,int t){
top[x]=t;
if (!son[x]) return;//如果没有重儿子,说明x节点为叶子节点,直接返回
dfs2(son[x],t);//向下沿着重儿子扩展重链
for (auto v:e[x]){
if (v==fa[x]||v==son[x]) continue;
dfs2(v,v);/沿着轻儿子新开一条重链
}
}
第二次 DFS 开始树链剖分,top[i] 记录 i 节点所在重链的顶点
枚举 \(i\) 节点所有儿子时,如果该儿子是重儿子就继承父节点所在重链的顶点,如果是轻儿子,那么就从这个轻儿子开始重新建立一条重链
int lca(int x,int y){
while(top[x]!=top[y]){
if (dep[top[x]]<dep[top[y]]) swap(x,y);//保证x节点深度最大
x=fa[top[x]];//沿着重链向上跳
}
return dep[x]<dep[y]?x:y;//返回LCA
}
计算 LCA 的原理是让询问的两个节点 \(x,y\) 沿着重链向上跳,当跳到同一条重链上时深度较小的点为 LCA
与倍增法类似,每次都选择深度较大的节点向上跳

浙公网安备 33010602011771号