LCA(Least Common Ancestors)
即最近公共祖先,是指这样一个问题:在有根树中,找出某两个结点u和v最近的公共祖先(另一种说法,离树根最远的公共祖先)。
一、在线算法ST算法
所谓在线即输入一个询问,要立即返回答案,才可进行下一次询问。
基础:dp(即rmq)
时间复杂度O(nlogn+m+n)
步骤:
1.将树看作一个无向图,从根节点开始深搜,得到一个遍历序列。
eg.
(1)深搜节点序列:1 3 1 2 5 7 5 6 5 2 4 2 1
(2)各点深度: 1 2 1 2 3 4 3 4 3 2 3 2 1
(3)第一次出现的下标: 1 4 2 11 5 8 6
2.在x~y区间中利用RMQ算法找到深度最小返回其下标。
Eg.求4和6的最近公共祖先
通过上一步求解我们知道它们在深搜序列中出现在8~11,即6,5,2,4。
这时候用到RMQ算法,维护一个dp数组保存其区间深度最小的下标,查找时返回即可。例子中我们找到深度最小的数为2,返回其下标10。
例题:
给你一棵有根树,要求你计算出m对结点的最近公共祖先。
#include
#include
#include
#include
#define N 200005
using namespace std;
int tot,head[N],ver[2*N],r[2*N],first[N],dp[2*N][18];
int n,m;
bool vis[N];
struct Node{
int to,next;
}e[2*N];
void insert(int x,int y)
{
e[++tot].to=y;
e[tot].next=head[x];
head[x]=tot;
}
void dfs(int u,int dep)
{
vis[u]=true;ver[++tot]=u;first[u]=tot;r[tot]=dep;
for(int k=head[u];k!=-1;k=e[k].next)
if(!vis[e[k].to])
{
int v=e[k].to;
dfs(v,dep+1);
ver[++tot]=u;r[tot]=dep;
}
}
void ST(int len)
{
for(int i=1;i<=len;i++)
dp[i][0]=i;
for(int j=1;(1<y)swap(x,y);
if(x==y)return ver[x];
int res=RMQ(x,y);
return ver[res];
}
int main()
{
scanf("%d%d",&n,&m);
memset(vis,false,sizeof(vis));
memset(head,-1,sizeof(head));
tot=0;
int x,y,root;
for(int i=1;i
又称作爬树法。
时间复杂度:预处理O(nlogn),每次询问O(logn)基础:dp,求节点在树中的深度。
步骤:
1.添加边,并预处理p数组。
void addedge(int x,int y)
{
e[++tot].v=y;
e[tot].next=head[x];
head[x]=tot;
}
p[i][j]表示i的2^j倍祖先。
p[i][j]=prt[i],j=0
p[p[i][j-1]][j-1],j>0
2.求每个点在树中的深度d[i]。
3.对于每个询问a,b
首先判断d[a]
将a 的深度不断降低,调到与b相同的深度。
这时将a,b同时调整,直到两个变量的父亲相同,即当p[a][i]!=p[b][i],则a=p[a][i],b=p[b][i],i--。
最后p[a][0]或p[b][0]为答案。
eg.
对于上面的一棵树,我们要询问5和9的最近公共祖先
首先预处理出p[5][0]=3,p[5][1]=1;
p[9][0]=7;p[9][1]=4;
d[5]=3;d[9]=4;
然后将9调至与5同深度。
int k=trunc(log2(4));
for(int i=k;i>=0;i--)
if(d[a]-(1<=d[b])a=p[a][i];
9->7。
接下来将5,7同时向上调整,直到它们的父亲相同,即变为3,4.。
输出3或4的父亲,即1。
例题:
给你一棵有根树,要求你计算出 m 对结点的最近公共祖先。
#include
#include
#include
#include
#define N 200005
using namespace std;
struct Node{
int v,next;
}e[N];
int n,m,tot;
int head[N],d[N];
bool vis[N];
int par[N][20];
void addedge(int x,int y)
{
e[++tot].v=y;
e[tot].next=head[x];
head[x]=tot;
}
void dfs(int u,int dep)
{
vis[u]=true;d[u]=dep;
for(int k=head[u];k;k=e[k].next)
if(!vis[e[k].v])
{
int v=e[k].v;
dfs(v,dep+1);
}
}
void prepare()
{
for(int j=1;(1<=0;i--)
if(d[a]-(1<=d[b])a=par[a][i];
if(a==b)return a;
for(int i=k;i>=0;i--)
{
if(par[a][i]!=-1&&par[a][i]!=par[b][i])
a=par[a][i],b=par[b][i];
}
return par[a][0];
}
int main()
{
scanf("%d%d",&n,&m);
tot=0;
memset(par,-1,sizeof(par));
memset(vis,0,sizeof(vis));
int x,y;
for(int i=1;i
给定一个包含n个节点的树,节点编号为1..n。其中,节点1为树根。
你的任务是给定这棵树的两个节点,快速计算出他们公共祖先的个数。
(第一行一个整数n(1≤n≤50,000),表示树的节点个数。接下来的n行,第i行表示节点i的信息。第i行第一个数字k,表示节点i拥有孩子的个数,接着k个数字,表示这个节点所拥有的孩子的编号。如果k=0,表示该节点是叶节点。注意,我们假定节点是节点本身的祖先。第n+2行是一个整数m(1≤m≤30,000),表示有m个查询。接下去m行,每行两个数字x,y,表示该查询的两个节点的编号。)
两个节点的公共祖先的个数即它们的最近公共祖先的深度,因为显然最近公共祖先以上都为两节点的公共祖先。
#include
#include
#include
#include
#define N 50010
using namespace std;
int n,q,tot;
struct Node{
int u,v,next;
}e[N];
int head[N],d[N];
bool vis[N];
int p[N][20];
void addedge(int u,int v)
{
e[++tot].u=u;e[tot].v=v;
e[tot].next=head[u];
head[u]=tot;
}
void dfs(int x,int dep)
{
vis[x]=1;d[x]=dep;
for(int i=head[x];i!=-1;i=e[i].next)
if(!vis[e[i].v])
dfs(e[i].v,dep+1);
}
void prepare()
{
for(int j=1;(1<=0;i--)
if(d[a]-(1<=d[b])a=p[a][i];
if(a==b)return a;
for(int i=k;i>=0;i--)
{
if(p[a][i]!=-1&&p[a][i]!=p[b][i])
a=p[a][i],b=p[b][i];
}
return p[a][0];
}
int main()
{
scanf("%d",&n);
memset(head,-1,sizeof(head));
memset(p,-1,sizeof(p));
tot=0;
for(int i=1;i<=n;i++)
{
int k,v;
scanf("%d",&k);
for(int j=1;j<=k;j++)
{
scanf("%d",&v);
addedge(i,v);
p[v][0]=i;
}
}
memset(vis,false,sizeof(vis));
dfs(1,1);
scanf("%d",&q);
prepare();
for(int i=1;i<=q;i++)
{
int u,v;
scanf("%d%d",&u,&v);
printf("%d\n",d[lca(u,v)]);
}
return 0;
}
三、
离线Tarjan求LCA
所谓离线是指在读取完全部的询问后再统一处理的算法。
基础:深度优先搜索的思想,并查集
时间复杂度:O(n+q)
基于深度优先搜索的框架,对于新搜索到的一个结点,首先创建由这个结点构成的集合,再对当前结点的每一个子树进行搜索,每搜索完一棵子树,则可确定子树内的LCA询问都已解决。其他的LCA询问的结果必然在这个子树之外,这时把子树所形成的集合与当前结点的集合合并,并将当前结点设为这个集合的祖先。之后继续搜索下一棵子树,直到当前结点的所有子树搜索完。这时把当前结点也设为已被检查过的,同时可以处理有关当前结点的LCA询问,如果有一个从当前结点到结点v的询问,且v已被检查过,则由于进行的是深度优先搜索,当前结点与v的最近公共祖先一定还没有被检查,而这个最近公共祖先的包含v的子树一定已经搜索过了,那么这个最近公共祖先一定是v所在集合的祖先。
补充:上面提到的询问(x,y)中,y是已处理过的结点。那么,如果y尚未处理怎么办?其实很简单,只要在询问列表中加入两个询问(x, y)、(y,x),那么就可以保证这两个询问有且仅有一个被处理了(暂时无法处理的那个就pass掉)。而形如(x,x)的询问则根本不必存储。
(1)读入数据,建立树结构,并记录下询问序列Q[],若有(u,v)的询问,则(u,v)和(v,u)都要记录。
(2)Tarjan(x)算法
①建立集合,自己为自己的父亲prt[x]=x;
②对当前节点x的每个儿子节点y进行深搜,并prt[y]=x;
③设置访问标记mark[x]=1,查找所有与x有关的回答,若另一点已经访问了,则另一个点的祖先就是他们的最经公共祖先。
(3)输出答案;
例题:
【USACO2004 FEB】距离查询
读入一棵无根树,求树上两点的最短距离。
因为是无根树,我们不妨设1为根,用d[i]表示点i到根的距离,求树上两点距离即求两点的LCA,用d[a]+d[b]-2*d[lca(a,b)]即可算出答案。
#include
#include
#include
#define N 40010
#define M 10010
using namespace std;
int head[N],_head[N];
struct Node{
int u,v,w,next;
}e[2*N];
struct ask{
int u,v,lca,next;
}ea[2*M];
int dir[N],fa[N],ance[N];
bool vis[N];
void add_edge(int u,int v,int w,int &k)
{
e[k].u=u;e[k].v=v;e[k].w=w;
e[k].next=head[u];
head[u]=k++;
e[k].u=v;e[k].v=u;e[k].w=w;
e[k].next=head[v];
head[v]=k++;
}
void add_ask(int u,int v,int &k)
{
ea[k].u=u;ea[k].v=v;ea[k].lca=-1;
ea[k].next=_head[u];_head[u]=k++;
ea[k].u=v;ea[k].v=u;ea[k].lca=-1;
ea[k].next=_head[v];_head[v]=k++;
}
int find(int x)
{
return x==fa[x]?x:fa[x]=find(fa[x]);
}
void uni(int x,int y)
{
int a=find(x);
int b=find(y);
fa[b]=a;
}
void tarjan(int u)
{
vis[u]=true;
fa[u]=u;
for(int k=head[u];k!=-1;k=e[k].next)
if(!vis[e[k].v])
{
int v=e[k].v,w=e[k].w;
dir[v]=dir[u]+w;
tarjan(v);
uni(u,v);
}
for(int k=_head[u];k!=-1;k=ea[k].next)
if(vis[ea[k].v])
{
int v=ea[k].v;
ea[k].lca=ea[k^1].lca=find(v);
}
}
int main()
{
int n,q,m,tot;
char h;
scanf("%d%d",&n,&m);
memset(head,-1,sizeof(head));
memset(_head,-1,sizeof(_head));
tot=0;
for(int i=1;i<=m;i++)
{
int u,v,w;
scanf("%d%d%d %c",&u,&v,&w,&h);
add_edge(u,v,w,tot);
}
scanf("%d",&q);
tot=0;
for(int i=1;i<=q;i++)
{
int u,v;
scanf("%d%d",&u,&v);
add_ask(u,v,tot);
}
memset(vis,0,sizeof(vis));
dir[1]=0;
tarjan(1);
for(int i=1;i<=q;i++)
{
int s=i*2-1,u=ea[s].u,v=ea[s].v,lca=ea[s].lca;
printf("%d\n",dir[u]+dir[v]-2*dir[lca]);
}
return 0;
}