最近公共祖先
对于有根树T的两个结点u,v,最近公共祖先LCA(T,u,v)表示一个节点x,满足x是u,v的祖先并且x的深度尽可能大。
也可以把T理解为一个无向无环图,而LCA(T,u,v)即u到v的最短路上森度最小的点
所以,LCA主要就是求解当两个点仅有唯一一条确定的最短路径时的路径信息的问题。
树上倍增
如果我们直接一个一个结点遍历去寻找这样的祖先,复杂度是O(n)的。而在算法竞赛中,经常会有个数较多的询问。比如50000个点,50000个询问,这时直接遍历的方法耗时较多。
下面是一种被称作树上倍增的算法,实际上它的思想类似于二分枚举法,时间复杂度是log(n)的。
终究都要走到一起(树根),可以考虑第一个不同祖先,这是倍增法的思想
所谓倍增,就是两倍,即从一个节点逐渐往上找k级父亲(所跨层数为2k,20即为通常所说的父结点)
具体操作:先使两个结点深度相同(在同一层),然后将k从大到小进行枚举。之前要对2k结点作预处理。
往往题目中不光要求LCA,还询问路径上的一些信息(如权值之和或者权值最大),因此还要另外记录。
参考代码
存储结构
const int maxn=100000;//节点数
vector<Edge> edges;//边的具体信息
vector<int> G[maxn];//边的编号
const int maxlog=30;
int grand[maxn][maxlog];
int gdis[maxn][maxlog];
int gmax[maxn][maxlog];
int depth[maxn];
int n;//结点数
int s;//倍增最大步数
int root;//根节点
预处理
void addEdge(int u,int v,int w){//vector邻接表
edges.push_back(Edge(u,v,w));
edges.push_back(Edge(v,u,w));
int size=edges.size();
G[u].push_back(size-2);
G[v].push_back(size-1);
}
void dfs(int x)//预处理
{
for(int i=1;i<=s;++i){//k级父亲
grand[x][i]=grand[grand[x][i-1]][i-1];
gdis[x][i]=gdis[x][i-1]+gdis[grand[x][i-1]][i-1];
gmax[x][i]=max(gmax[x][i-1],gmax[grand[x][i-1]][i-1]);
if(!grand[x][i]) break;
}
for(int i=0;i<G[x].size();i++){
Edge & e=edges[G[x][i]];
if(e.to!=grand[x][0]){
depth[e.to]=depth[x]+1;
grand[e.to][0]=x;
gdis[e.to][0]=e.dist;
gmax[e.to][0]=e.dist;
dfs(e.to);
}
}
}
void init()//初始化
{
s=floor(log(n+0.0)/log(2.0));
depth[0]=-1;
memset(grand,0,sizeof(grand));
memset(gdis,0,sizeof(gdis));
memset(gmax,0,sizeof(gmax));
root=1;
dfs(root);//以1为根结点建树
}
核心代码
int lca(int a,int b,int &maxx,int &ans)//maxx,ans:最大值,路径权值和
{
if(depth[a]>depth[b]) swap(a,b);
ans=0;//路径权值和
maxx=gmax[b][0];
//for(int i=s;i>=0;i--)
// if(depth[a]<depth[b]&&depth[grand[b][i]]>=depth[a])
// ans+=gdis[b][i],maxx=max(maxx,gmax[b][i]),b=grand[b][i];
int dre=depth[b]-depth[a];
for(int i=s;i>=0;--i){
if(dre&(1<<i)) ans+=gdis[b][i],maxx=max(maxx,gmax[b][i]),b=grand[b][i];
}
if(a==b) return a;
for(int i=s;i>=0;i--)
if(grand[a][i]!=grand[b][i]){
ans+=gdis[a][i],ans+=gdis[b][i];
maxx=max(maxx,gmax[a][i]),maxx=max(maxx,gmax[b][i]);
a=grand[a][i],b=grand[b][i];
}
ans+=gdis[a][0];
ans+=gdis[b][0];
maxx=max(maxx,gmax[a][0]);
maxx=max(maxx,gmax[b][0]);
return grand[a][0];
}
总体示例(vector)
#include<bits/stdc++.h>
using namespace std;
struct Edge{
int from,to,dist;
Edge(int u,int v,int w):from(u),to(v),dist(w){
}
};
const int maxn=100000;//节点数
vector<Edge> edges;//边的具体信息
vector<int> G[maxn];//边的编号
void addEdge(int u,int v,int w){//vector邻接表
edges.push_back(Edge(u,v,w));
edges.push_back(Edge(v,u,w));
int size=edges.size();
G[u].push_back(size-2);
G[v].push_back(size-1);
}
const int maxlog=30;
int grand[maxn][maxlog];
int gdis[maxn][maxlog];
int gmax[maxn][maxlog];
int depth[maxn];
int n;//结点数
int s;//倍增最大步数
int root;//根节点
void dfs(int x)//预处理
{
for(int i=1;i<=s;++i){
grand[x][i]=grand[grand[x][i-1]][i-1];
gdis[x][i]=gdis[x][i-1]+gdis[grand[x][i-1]][i-1];
gmax[x][i]=max(gmax[x][i-1],gmax[grand[x][i-1]][i-1]);
if(!grand[x][i]) break;
}
for(int i=0;i<G[x].size();i++){
Edge & e=edges[G[x][i]];
if(e.to!=grand[x][0]){
depth[e.to]=depth[x]+1;
grand[e.to][0]=x;
gdis[e.to][0]=e.dist;
gmax[e.to][0]=e.dist;
dfs(e.to);
}
}
}
void init()
{
s=floor(log(n+0.0)/log(2.0));
depth[0]=-1;
memset(grand,0,sizeof(grand));
memset(gdis,0,sizeof(gdis));
memset(gmax,0,sizeof(gmax));
root=1;
dfs(root);//以1为根结点建树
}
int lca(int a,int b,int &maxx,int &ans)//maxx,ans:最大值,路径权值和
{
if(depth[a]>depth[b]) swap(a,b);
ans=0;//路径权值和
maxx=gmax[b][0];
//for(int i=s;i>=0;i--)
// if(depth[a]<depth[b]&&depth[grand[b][i]]>=depth[a])
// ans+=gdis[b][i],maxx=max(maxx,gmax[b][i]),b=grand[b][i];
int dre=depth[b]-depth[a];
for(int i=s;i>=0;--i){
if(dre&(1<<i)) ans+=gdis[b][i],maxx=max(maxx,gmax[b][i]),b=grand[b][i];
}
if(a==b) return a;
for(int i=s;i>=0;i--)
if(grand[a][i]!=grand[b][i]){
ans+=gdis[a][i],ans+=gdis[b][i];
maxx=max(maxx,gmax[a][i]),maxx=max(maxx,gmax[b][i]);
a=grand[a][i],b=grand[b][i];
}
ans+=gdis[a][0];
ans+=gdis[b][0];
maxx=max(maxx,gmax[a][0]);
maxx=max(maxx,gmax[b][0]);
return grand[a][0];
}
int main()
{
ios::sync_with_stdio(false);
int t;
cin>>t;
while(t--)
{
int maxx,sum;
edges.clear();
for(int i=0;i<maxn;++i){
G[i].clear();
}
int query,u,v,w;
cin>>n>>query;
for(int i=1;i<=n-1;++i){
cin>>u>>v>>w;
addEdge(u,v,w);
}
init();
for(int i=1;i<=query;++i){
cin>>u>>v;
lca(u,v,maxx,sum);
cout<<lca(u,v,maxx,sum)<<endl;
cout<<maxx<<endl;
cout<<sum<<endl;
}
}
return 0;
}
优化(链式向前星,数据量较大时可以优化常数)
//链式向前星也是存图的一种方式,用数组模拟邻接表,在点数较多时较vector优
int head[2*maxn];
void addEdge(int u,int v){//关键部分
edges[cnt].from=v;
edges[cnt].to=head[u];
head[u]=cnt++;
}