LCA问题
最近公共祖先是指对于一棵有根树T的两个节点u和v,它们的LCA(T,u,v)表示一个节点x,x是u,v的祖先且x的深度尽可能大。对于LCA问题有多种解法,Tarjan、倍增、RMQ、树剖等...
首先关注离线求解算法,也就是将询问都存储起来,处理完之后一次性回答所有询问。Tarjan算法+并查集可以以O(n+Q)的复杂度来处理,Q是查询次数。利用集合,在深搜过程中将当前子树构成一个集合。向上回溯,当前节点的所有子树都已遍历完后,将当前节点和所有子树合为一个集合。处理和当前节点u有关的查询,如果另一个节点是被访问过的,那么lca就是v节点所在集合当前的祖先;如果另一个节点未被访问,则先跳过。这样可以保证在退出一棵树时,所有能够完成的查询都已处理。
//主要部分的代码
void init() //初始化,为并查集赋初值,清空访问记录
{
for(int i=1;i<=n;i++){
root[i]=i;
mark[i]=false;
}
}
int findroot(int x) //并查集找根
{
if(root[x]==x) return x;
return root[x]=findroot(root[x]);
}
void union(int x,int y)
{
int rx=findroot(x),ry=findroot(y);
if(rx!=ry) root[rx]=ry; //注意合并顺序,和dfs中的传参顺序有关
}
void dfs(int x)
{
mark[x]=true;
for(int i=0;i<edges[x].size();i++){ //依次访问子树,并在回溯时合并集合
int next=edges[x][i];
if(mark[next]) continue;
dfs(next);
union(next,x);
}
for(set<int>::iterator it=query[x].begin();it!=query[x].end();it++){
int v=*it;
if(mark[v]){
ans[u][v]=ans[v][u]=findroot(v); //答案即为findroot(v)
query[v].erase(query[v].find(u)); //删掉v中的相关搜索
}
}
}
在线算法:倍增,复杂度O(logn)。由于一层一层地向上查找太慢了,可以以2^k为步长向上查找。主要分为两部分,第一部分,先将深度不同的两个点拉到同一深度;第二部分,以尽可能大的步长向上寻找,循环找到最后的结果应当是lca的下一层节点,再向上找父节点即可完成。与暴力方法思路一致,但是进行了优化,编写也比较简单。
int t=log2(n); //2^20>maxn,那么向上查找的最大步长也就是20
void dfs(int x,int pre)
{
dep[x]=dep[pre]+1;
fa[x][0]=pre;
for(int i=t;i>=0;i--){
fa[fa[x][i-1]]=fa[x][i-1];
}
for(int i=0;i<edges[x].size();i++){
int next=edges[x][i];
if(next==pre) continue;
dfs(next);
}
}
int lca(int a,int b)
{
if(dep[b]>dep[a]) swap(a,b); //使dep[a]总是较小
for(int i=t;i>=0;i--){
if(dep[fa[a][i]]<=dep[b]) a=fa[a][i]; //向上寻找,深度不小于dep[b]即可向上,最后总能使dep[a]==dep[b]
}
if(a==b) return a; //若深度相同时重合,直接返回
for(int i=t;ii>=0;i--){
if(dep[fa[a][i]]!=dep[fa[b][i]]){ //向上寻找,不超过或等于lca的深度
a=fa[a][i];b=fa[b][i];
}
}
return fa[a][0];
}
RMQ+LCA的做法,主要思想是RMQ记录dfs过程中,u、v两点之间出现过的深度最深的点,也就是维护在u后面的一定区域内的深度最深的点。对u、v区间内维护的记录进行O(1)查询,取出深度最深的点就是lca。POJ2763,POJ3321
#include<stdio.h>
#include<string.h>
const int maxv=200005,maxe=400005;
int n,m,k,etot,head[maxv],fid[maxv],sid[maxv],vs[maxv],sum[maxv],vis[maxv];
struct e{
int next,to;
}edge[maxe];
void addedge(int a,int b){
edge[etot].next=head[a];
edge[etot].to=b;
head[a]=etot++;
}
int lowbit(int x){
return x&(-x);
}
void modify(int i,int x){
while(i<=k){
sum[i]+=x;
i+=lowbit(i);
}
}
int query(int i){
int res=0;
while(i>0){
res+=sum[i];
i-=lowbit(i);
}
return res;
}
void dfs(int u,int p){
fid[u]=k;
vs[k++]=u;
for(int i=head[u];i!=-1;i=edge[i].next){
int v=edge[i].to;
if(v!=p){
dfs(v,u);
}
}
sid[u]=k;
vs[k++]=u;
}
int main(){
while(scanf("%d",&n)!=EOF){
etot=0;
memset(sum,0,sizeof(sum));
for(int i=1;i<=n;i++){
vis[i]=head[i]=-1;
}
for(int i=0;i<n-1;i++){
int a,b;
scanf("%d%d",&a,&b);
addedge(a,b);
addedge(b,a);
}
k=1;
dfs(1,0);
for(int i=1;i<=k;i++) modify(i,1);
scanf("%d",&m);
for(int i=0;i<m;i++){
char str[5];int tmp;
scanf("%s%d",str,&tmp);
if(str[0]=='Q'){
printf("%d\n",(query(sid[tmp])-query(fid[tmp]-1))/2);
}
else if(str[0]=='C'){
modify(fid[tmp],vis[tmp]);
modify(sid[tmp],vis[tmp]);
vis[tmp]=-vis[tmp];
}
}
}
return 0;
}
LCA问题,除过模板题之外,其他的题目可能会和集中类型相关:
1)求两点之间路径。树的深搜过程中可以保存每个节点的父节点,找到lca之后即可向上回溯
2)多对点求lca,求树上每个点被访问的次数。利用树上差分进行计数,最后深搜一遍,在回溯时得到当前节点的最终答案。
3)求两对节点的最短路有没有相交。若相交,则必然有lca1位于c、d之间的路径上或lca2位于a、b之间的路径上,有dis(c,lca1)+dis(d,lca1)==dis(c,d)或是dis(a,lca2)+dis(b,lca2)==dis(a,b)为真,则说明相交
也可以用dis(a,b)+dis(c,d)>=dis(a,c)+dis(b,d)来判断,true就是相交