倍增法求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=dep[p]+1;//当前节点深度为父节点深度+1
fa[0]=p;
for (int i=1;i<=19;i++)//倍增法递推
fa[i]=fa[fa[i-1]][i-1];
for (auto v:e){//DFS叶子节点
if (v==p) continue;
dfs(v,x);
}
}
利用 $Fa_{i,j}$ 计算 LCA
首先需要将节点 $x,y$ 跳到同一层,然后两个节点一起向上跳到最近公共祖先的下一层
int lca(int x,int y){
if (dep<dep[y]) swap(x,y);//保证x的深度较大
for (int i=19;i>=0;i--){
if (dep[fa[i]]>=dep[y])//将深节点x跳到y节点同一层上
x=fa[i];
}
if (x==y) return y;//如果y是x的祖先节点,那么直接返回
for (int i=19;i>=0;i--){
if (fa[i]!=fa[y][i])
x=fa[i],y=fa[y][i];//x,y一起向上跳
}
return fa[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) return x;
return fa=find(fa);//路径压缩不影响答案
}
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=1;//标记访问
for (auto v:e){
if (vis[v]) continue;
tarjan(v);//递归
fa[v]=x;
}
for (auto it:q){
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=dep[p]+1;
fa=p;
sz=1;
for (auto v:e){
if (v==p) continue;
dfs1(v,x);
sz+=sz[v];
if (sz[son]<sz[v]) son=v;//v的子树大小如果比之前记录的重儿子更大,那么更新重儿子
}
}
首先需要通过第一次 DFS 处理出每个节点的深度,父亲以及他形成的子树的大小
son[i]
数组记录 i
节点的重儿子编号,通过每个儿子的子树大小进行更新
void dfs2(int x,int t){
top=t;
if (!son) return;//如果没有重儿子,说明x节点为叶子节点,直接返回
dfs2(son,t);//向下沿着重儿子扩展重链
for (auto v:e){
if (v==fa||v==son) continue;
dfs2(v,v);/沿着轻儿子新开一条重链
}
}
第二次 DFS 开始树链剖分,top[i]
记录 i
节点所在重链的顶点
枚举 $i$ 节点所有儿子时,如果该儿子是重儿子就继承父节点所在重链的顶点,如果是轻儿子,那么就从这个轻儿子开始重新建立一条重链
int lca(int x,int y){
while(top!=top[y]){
if (dep[top]<dep[top[y]]) swap(x,y);//保证x节点深度最大
x=fa[top];//沿着重链向上跳
}
return dep<dep[y]?x:y;//返回LCA
}
计算 LCA 的原理是让询问的两个节点 $x,y$ 沿着重链向上跳,当跳到同一条重链上时深度较小的点为 LCA
与倍增法类似,每次都选择深度较大的节点向上跳
评论区(暂无评论)