树链法
求最近公共祖先的树链部分
重儿子: 父节点的所有儿子中结点数目最多的节点
轻儿子:父节点中除重儿子以外的儿子
重边:父节点和重儿子连城的边
轻边:父节点和轻儿子连成的边
重链:由多重边连接而成的路径
1.整棵树会被剖分成若干条重链
2.轻儿子一定是每条重链的顶点
3.任意一条路径被切分成不超过logn条链
数组
fa[u] 存u的夫节点
dep[u] 存u的深度
son[u] 存u的重儿子
sz[u] 存以u为根的子树的节点数
top[u] 存u的所有重链的顶点
1.第一遍dfs,搞出fa,dep,son数组
2.第二遍dfs,搞出top数组
3.让两个游标沿着各自的重链向上跳,跳到同一条重链上时,深度较小的哪个游标所指向的点,就是LCAvector<int>e[N];
int fa[N],dep[N],son[N].sz[N];
int top[N];
void dfs1(inty u,int father){
fa[u]=father,dep[u]=dep[father]+1,sz[u]=1;
for(int v:e[v]){
if(v==father)continue;
dfs1(v,u);
sz[u]+=sz[v];//累加和
if(sz[son[u]]<sz[v])son[u]=v;//找重儿子
}
}
void dfs2(int v,int t){//找top
top[u]=t;//记录链头
if(!son[u])return;//叶节点没有重儿子
dfs2(son[u],t);//搜重儿子
for(int v:e[u]){
if(v==fa[u]||v==son[u])continue;
dfs2(v,v);//搜轻儿子
}
}int lca(int u,int v){
while(top[u]!=top[v]){if(dep[top[u]]<dep[top[v]])swap(u,v);
u=fa[top[u]];
}
return dep[u]<dep[v]?u:v; //最后深度小的为最近公共父节点
}
倍增法
两个节点的最近公共祖先(lowest Common Ancestor LCA)
就是这两个点的公共祖先里面,离他们最近的那个倍增算法是最经典的LCA算法
dep[u]存u点的深度
fa[u][i]存从u点向上跳2层的祖先结点 i=0 1 2 3..
例如,从节点9向上跳2^0层的祖先是3,跳2^1层的祖先是6,跳2^2层的祖先是1,跳2^3层的祖先是0(哨兵)1
/ \
5 4
/|\
2 6 7
/\
3 8
\
9
dfs一遍,创建ST表
倍增递推,fa[u][i]=fa[fa[u][i-1]][i-1]
分一半来跳
u->fa[u][i-1]->fa[fa[u][i-1]][i-1]
u->fa[u][i]
0 1 2 3...19
1 0 0 0 0...0
5 1 0 0 0...0
2 5 1 0 0...0
6 5 1 0 0...0
3 6 5 0 0...0
9 3 6 1 0...0
2.利用ST表求LCA
(1),第一阶段,将u,v跳到同一层
设u,v两点深度之差为y,将y进行二进制拆分,可以
将y次游标跳到优化为y的二进制表示所含1的个数
次游标跳跃,一定能跳到同一层例如:y=1019(0..0111111011)=512+256+..+8+2+1
不越界则跳,共跳9次到达
(2) 第二阶段,将u,v一起跳到LCA的下一层
从最大的i开始循环尝试,一直尝试到0,最后游标u,v一定能停在LCA的下一次
例如,v=1019(0..0111110111)
两游标会跳512+256+..+8+2=1018层,共跳8次到达LCA的下一层
const int N=5e5+10;
int n,m,s,a,b;
vector<int>e[N];
void dep[N],fa[N][20];void dfs(int u,int father){//当前节点,父节点
dep[u]=dep[father]+1;//深度为父节点的深度+1
fa[u][0]=father;//当前节点往上走2的0次方即为父节点
for(int i=1;i<19;i++)
fa[u][i]=fa[fa[u][i-1]][i-1];//分两半走
for(int v:e[v])//往下找子节点dfs
if(v!=father)//不是父节点才能dfs
dfs(v,u);
}int lcm(int u,int v){
if(dep[u]<dep[v])swap(u,v);//让u>v这样有助于操作
for(int i=19;i>=0;i--){
if(dep[fa[u][i]]<dep[v])//如果u即深度大的点比v的深度大,就让他往上走,直到两者深度相同
u=fa[u][i];
}
if(u==v)return v;//若是相等,直接返回
for(int i=19;i>=0;i--)
if(fa[u][i]!=fa[v][i])
u=fa[u][i];v=fa[v][i];//让两点往上走,始终保持两点父亲不相等,最后停在最近公共父节点的儿子
return fa[u][0];//返回最近公共父节点
}
重点
dep[u]有u点的深度
fa[u][i]存从u点向上跳2^i层的祖先结点打表:倍增递推,从大到小枚举
查询,二进制拆分,从大到小枚举
时间复杂度O((n+m)logn )
Tarjan
Tarjan(塔杨)算法是一种离线算法,巧妙利用并查集维护祖先结点
e[u]存树边,e[1]=5;e[5]=1
query[u]存查询,query[3]={4,1},query[4]={3,1}
fa[u]存父节点,fa[5]=1,fa[2]=5
vis[u]打标机,vis[5]=true
ans[i]存查询结果,ans[1]=1,ans[2]=51.从根开始深搜遍历,入u时打标记
2.枚举u的儿子v,遍历完v的子树,回u时,把v指向u
3.遍历完u的儿子们,离u时枚举以u为起点的查询,若终点v被搜过,则查找v的根,即u,v的LCA,答案记入ans[]
4.递归遍历完整颗树,得到全部查询答案
vector<int>e[N];//
vector<pair<int,int>>query[N];
int fa[N],vis[N],ans[M];
int find(int u){//没有回来的结点的父节点都是指向自身
if(u==fa[u])return u;//两点之间的最近公共父节点的父节点都是自身
return fa[u]=find(fa[u]);//当找到指向自身的父节点时,即为两点的最近公共父节点
}
int taryan(int u){
vis[u]=true;//入u标记u
for(auto v:e[u]){//遍历子节点
if(!vis[v]){//没被标记过
tarjan(v);//taryan递归
fa[v]=u;//递归出来后更新父节点
}
}
//查询需要找最近公共父节点的两点
for(auto:q:query[u]){//求与该点的其他点的最近公共父节点
int v=q.first,i-q.second;//first为另一点,second为两点问题的编号
if(vis[v])ans[i]=find(v);//如果另一点vis为true即已标记过,则可求最近公共父节点
}
}
//main里
for(int i=1;i>n;i++){//无向边都两边都放进去
scanf("%d%d",&a,&b);
e[a].push_bcak(b);
e[b].push)bcak(a);
}
for(int i=1;i<=m;i++){//一个查询问题各自存两个,问题编号一致
scanf("%d%d",&a,&b);
query(a).push_back({b,i});
query(b).push_back({a,i});
}
for(int i=1;i<=N;i++)fa[i]=i;//初始化父节点指向自身