LCA 问题,即Least Common Ancestors(最近公共祖先)的意思是:给定一有根树,求其两个节点最近的公共祖先;节点的祖先即从节点至根的路径上的节点的集合。
离线算法,就是要将所有询问先存起来,一起处理,然后再一起输出,与之相对应的是在线算法。在线算法,每给一个询问便可以立即求出答案。
Tarjan算法利用并查集优越的时空复杂度,我们可以实现LCA问题的O(n+Q)算法,这里Q表示询问的次数。Tarjan算法基于深度优先搜索的框架,对于新搜索到的一个结点,首先创建由这个结点构成的集合,再对当前结点的每一个子树进行搜索,每搜索完一棵子树,则可确定子树内的LCA询问都已解决。其他的LCA询问的结果必然在这个子树之外,这时把子树所形成的集合与当前结点的集合合并,并将当前结点设为这个集合的祖先。之后继续搜索下一棵子树,直到当前结点的所有子树搜索完。这时把 当前结点也设为已被检查过的,同时可以处理有关当前结点的LCA询问,如果有一个从当前结点到结点v的询问,且v已被检查过,则由于进行的是深度优先搜索,当前结点与v的最近公共祖先一定还没有被检查,而这个最近公共祖先的包涵v的子树一定已经搜索过了,那么这个最近公共祖先一定是v所在集合的祖先。
首先,Tarjan算法是一种离线算法,也就是说,它要首先读入所有的询问(求一次LCA叫做一次询问),然后并不一定按照原来的顺序处理这些询问。而打乱这个顺序正是这个算法的巧妙之处。看完下文,你便会发现,如果偏要按原来的顺序处理询问,Tarjan算法将无法进行。
Tarjan算法是利用并查集来实现的。它按DFS的顺序遍历整棵树。对于每个结点x,它进行以下几步操作:
* 计算当前结点的层号lv[x],并在并查集中建立仅包含x结点的集合,即root[x]:=x。
* 依次处理与该结点关联的询问。
* 递归处理x的所有孩子。
* root[x]:=root[father[x]](对于根结点来说,它的父结点可以任选一个,反正这是最后一步操作了)。
现在我们来观察正在处理与x结点关联的询问时并查集的情况。由于一个结点处理完毕后,它就被归到其父结点所在的集合,所以在已经处理过的结点中(包括 x本身),x结点本身构成了与x的LCA是x的集合,x结点的父结点及以x的所有已处理的兄弟结点为根的子树构成了与x的LCA是father[x]的集合,x结点的父结点的父结点及以x的父结点的所有已处理的兄弟结点为根的子树构成了与x的LCA是father[father[x]]的集合……(上面这几句话如果看着别扭,就分析一下句子成分,也可参照右面的图)假设有一个询问(x,y)(y是已处理的结点),在并查集中查到y所属集合的根是z,那么z 就是x和y的LCA,x到y的路径长度就是lv[x]+lv[y]-lv[z]*2。累加所有经过的路径长度就得到答案。 现在还有一个问题:上面提到的询问(x,y)中,y是已处理过的结点。那么,如果y尚未处理怎么办?其实很简单,只要在询问列表中加入两个询问(x, y)、(y,x),那么就可以保证这两个询问有且仅有一个被处理了(暂时无法处理的那个就pass掉)。而形如(x,x)的询问则根本不必存储。 如果在并查集的实现中使用路径压缩等优化措施,一次查询的复杂度将可以认为是常数级的,整个算法也就是线性的了。
上面内容来自NOCOW
先来一道裸题
Nearest Common Ancestors
#include
#include
#include
#include
#include
#include
#include
#include
#define msc(X) memset(X,-1,sizeof(X))
#define ms(X) memset(X,0,sizeof(X))
typedef long long LL;
using namespace std;
vector<int> son[10003],que[10003];
bool vs[10003];
int n,pre[10003],ancr[10003];
int fnd(int node)//并查集操作
{return pre[node]==node?node:(pre[node]=fnd(pre[node]));}
void join(int x,int y)//并查集操作
{
int fx=fnd(x),fy=fnd(y);
if(fx!=fy) pre[fx]=fy;
}
void LCA(int root)
{
ancr[root]=root;
int sz=son[root].size();
for(int i=0;i/*有ancr改变join函数的参数顺序无所谓,也就是说ancr其实也可以不用,但此时就必须注意join形参的顺序,在第四个例题hdu2874中采用此法*/
ancr[fnd(son[root][i])]=root;
}
vs[root]=true;//一定要放在循环后面
sz=que[root].size();
for(int i=0;iif(vs[que[root][i]]){
printf("%d\n",ancr[fnd(que[root][i])] );
return ;
}
}
int main(int argc, char const *argv[])
{
int t,head;
cin>>t;
while(t--)
{
scanf("%d",&n);
ms(vs);
ms(ancr);
for(int i=1;i<=n;i++)
pre[i]=i,son[i].clear(),que[i].clear();
for(int i=1;iint fr,sr;
scanf("%d %d",&fr,&sr);
son[fr].push_back(sr);
vs[sr]=true;
}
for(head=1;head<=n;head++) if(!vs[head]) break;
int a,b;
scanf("%d %d",&a,&b);
que[a].push_back(b),que[b].push_back(a);
ms(vs);
LCA(head);
}
return 0;
}
第二道也可以算是裸题
Closest Common Ancestors
#include
#include
#include
#include
#include
#include
#include
#include
#define msc(X) memset(X,-1,sizeof(X))
#define ms(X) memset(X,0,sizeof(X))
typedef long long LL;
using namespace std;
vector <int > node[903],qs[903];
int pre[903],ancr[903];
LL ans[903];
bool vs[903];
int fnd(int node)
{return pre[node]==node?node:(pre[node]=fnd(pre[node]));}
void join(int x,int y)
{
int fx=fnd(x),fy=fnd(y);
if(fx!=fy) pre[fx]=fy;
}
void LCA(int nd)
{
ancr[nd]=nd;
int sz=node[nd].size();
for(int i=0;itrue;
sz=qs[nd].size();
for(int i=0;iif(vs[qs[nd][i]])
ans[ancr[fnd(qs[nd][i])]]++;
}
int main(int argc, char const *argv[])
{
int n;
while(scanf("%d",&n)!=EOF)
{
ms(vs);
ms(ancr);
ms(ans);
for(int i=1;i<=n;i++)
{pre[i]=i;node[i].clear(),qs[i].clear();}
for(int i=1;i<=n;i++){
int nd,nm,sr;
scanf("%d:(%d)",&nd,&nm);
while(nm--)
{
scanf("%d",&sr);
node[nd].push_back(sr);
vs[sr]=true;
}
}
int p,a,b;
scanf("%d",&p);
while(p--)
{
while(getchar()!='(') continue;
scanf("%d %d)",&a,&b);
qs[a].push_back(b),qs[b].push_back(a);
}
for(p=1;p<=n;p++) if(!vs[p]) break;
ms(vs);
LCA(p);
for(int i=1;i<=n;i++) if(ans[i])
printf("%d:%lld\n",i,ans[i] );
}
return 0;
}
CD操作
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define msc(X) memset(X,-1,sizeof(X))
#define ms(X) memset(X,0,sizeof(X))
typedef long long LL;
using namespace std;
struct _Query
{
int to,id;
}tmp;
struct _out
{
int frm,to;
}out[100005];
vector<int> node[100005];
vector<struct _Query> qs[100005];
int ancr[100005],pre[100005],dis[100005],ans[100005];
bool vs[100005];
int fnd(int nd)
{return pre[nd]==nd?nd:(pre[nd]=fnd(pre[nd]));}
void join(int x,int y)
{
int fx=fnd(x),fy=fnd(y);
pre[fx]=fy;
}
void LCA(int nd,int num)
{
ancr[nd]=nd;
dis[nd]=num;
int sz=node[nd].size();
for(int i=0;i1);
join(nd,node[nd][i]);
ancr[fnd(node[nd][i])]=nd;
}
vs[nd]=true;
sz=qs[nd].size();
for(int i=0;iif(vs[qs[nd][i].to]){
ans[qs[nd][i].id]=ancr[fnd(qs[nd][i].to)];
}
}
int main(int argc, char const *argv[])
{
int t;
cin>>t;
while(t--)
{
int n,m,k=0;
scanf("%d %d",&n,&m);
map<string,int > mymap;
ms(vs);
ms(ancr);
ms(ans);
ms(dis);
mymap.clear();
for(int i=1;i<=n;i++)
pre[i]=i,node[i].clear(),qs[i].clear();
for(int i=1;ichar ss1[42],ss2[42];
scanf("%s %s",ss1,ss2);
if(mymap.find(ss1)==mymap.end()) mymap[ss1]=++k;
if(mymap.find(ss2)==mymap.end()) mymap[ss2]=++k;
vs[mymap[ss1]]=true;
node[mymap[ss2]].push_back(mymap[ss1]);
}
for(int i=1;i<=m;i++)
{
char ss1[42],ss2[42];
scanf("%s %s",ss1,ss2);
out[i].frm=mymap[ss1],out[i].to=mymap[ss2];
tmp.to=mymap[ss2],tmp.id=i;
qs[mymap[ss1]].push_back(tmp);
tmp.to=mymap[ss1],tmp.id=i;
qs[mymap[ss2]].push_back(tmp);
}
int head;
for(head=1;head<=n;head++) if(!vs[head]) break;
ms(vs);
LCA(head,0);
for(int i=1;i<=m;i++)
{
if(out[i].frm==out[i].to) printf("0\n");
else if(out[i].frm==ans[i]) printf("1\n");
else if(out[i].to==ans[i])
printf("%d\n",dis[out[i].frm]-dis[ans[i]] );
else printf("%d\n",dis[out[i].frm]-dis[ans[i]]+1 );
}
}
return 0;
}
Connections between cities
tarjan的话卡vector, 会MLE, 别图省事用链式前向星做吧。
网上查了一下很多用离线算法做的都mle(上面的原因),本题和标准的LCA模板应用有了不小的区别 ,而且本题没有必要去求出公共祖先,但这道题仍可以帮助更深入理解lca离线算法。
#include
#include
#include
#include
#include
#include
#include
#include
#define msc(X) memset(X,-1,sizeof(X))
#define ms(X) memset(X,0,sizeof(X))
typedef long long LL;
using namespace std;
const int maxn=10004,maxq=1000010;
struct _a_c{
int id,next;
int q;
}qy[2*maxq];
struct _n_d{
int to,next,val;
}edg[2*maxn];
int head[maxn],tot,qt,hq[maxn],
pre[maxn],tree[maxn],dis[maxn];
int ans[maxq];
bool vs[maxn];
void addedg(int u,int v,int k)
{
edg[tot].to=v;
edg[tot].next=head[u];
edg[tot].val=k;
head[u]=tot++;
}
void addqury(int u,int v,int index)
{
qy[qt].q=v;
qy[qt].next=hq[u];
qy[qt].id=index;
hq[u]=qt++;
}
int fnd(int nd)
{return pre[nd]==nd?nd:(pre[nd]=fnd(pre[nd]));}
void join(int x,int y)
{
int fx=fnd(x),fy=fnd(y);
if(fx!=fy) pre[fx]=fy;
}
void LCA(int nd,int d,int rt)
{
vs[nd]=true;
dis[nd]=d;
tree[nd]=rt;
for(int i=head[nd];i!=-1;i=edg[i].next)
{
int x=edg[i].to;
if(vs[x]) continue;
LCA(x,d+edg[i].val,rt);
join(x,nd);//参数不可换
}
//vs[nd]=true;
for(int i=hq[nd];i!=-1;i=qy[i].next){int nd2=qy[i].q;
if(vs[nd2]&&tree[nd]==tree[nd2])
ans[qy[i].id]=dis[nd]+dis[nd2]-2*dis[fnd(nd2)];
}
}
int main(int argc, char const *argv[])
{
int n,m,c;
while(scanf("%d %d %d",&n,&m,&c)!=EOF)
{
msc(ans);
msc(head);
msc(hq);
ms(vs);
ms(dis);
tot=qt=0;
for(int i=1;i<=n;i++) pre[i]=tree[i]=i;
//本来是有这句话的,不知道为什么这句话会突然消失了,让我debug两天。。
while(m--)
{
int x,y,k;
scanf("%d %d %d",&x,&y,&k);
addedg(x,y,k);
addedg(y,x,k);
}
for(int i=1;i<=c;i++)
{
int x,y;
scanf("%d %d",&x,&y);
addqury(x,y,i);
addqury(y,x,i);
}
for(int i=1;i<=n;i++)
if(!vs[i]) LCA(i,0,i);
for(int i=1;i<=c;i++)
if(ans[i]!=-1) printf("%d\n",ans[i] );
else puts("Not connected");
}
return 0;
}
其实就是将LCA问题转化成RMQ。
1.对有根树T进行DFS,将遍历到的结点按照顺序记下,我们将得到一个长度为2N – 1的序列,称之为T的欧拉序列F。
2.每个结点都在欧拉序列中出现,我们记录结点u在欧拉序列中第一次出现的位置为pos(u)。
3. 根据DFS的性质,对于两结点u、v,从pos(u)遍历到pos(v)的过程中经过LCA(u, v)有且仅有一次,且深度是深度序列B[pos(u)…pos(v)]中最小的,然后就转化成lca啰!
LCA(T, u, v) = RMQ(B, pos(u), pos(v))
在线算法
#include
#include
#include
#include
#include
#include
#include
#include
#define msc(X) memset(X,-1,sizeof(X))
#define ms(X) memset(X,0,sizeof(X))
typedef long long LL;
using namespace std;
struct _Edg
{
int to,next;
}edg[10013*2];
bool vs[10013];
int tot,tt,head[10013],vr[2*10013],frt[10013],R[2*10013],dp[2*10013][16];
//vr:欧拉序列编号,R:深度,frt:点编号位置
void addedg(int u,int v)
{
edg[tot].to=v;
edg[tot].next=head[u];
head[u]=tot++;
}
void dfs(int u,int dph)
{
vs[u]=true;vr[++tt]=u;frt[u]=tt;R[tt]=dph;
for(int k=head[u];k!=-1;k=edg[k].next)
if(!vs[edg[k].to]){
dfs(edg[k].to,dph+1);
vr[++tt]=u;R[tt]=dph;
}
}
void ST(int n)
{
for(int i=1;i<=n;i++) dp[i][0]=i;
for(int j=1;(1<for(int i=1;i<=n;i++)
if(i+(1<<(j-1))<=n)
{
int a=dp[i][j-1],b=dp[i+(1<<(j-1))][j-1];
dp[i][j]=(R[a]else dp[i][j]=dp[i][j-1];
}
void query(int a,int b)
{
int k=0;
while((1<<(k+1))<=b-a+1) k++;
int ra=dp[a][k],rb=dp[b-(1<1][k];
if(R[ra]>R[rb]) printf("%d\n",vr[rb] );
else printf("%d\n",vr[ra] );
}
int main(int argc, char const *argv[])
{
int t;
cin>>t;
while(t--)
{
int n;
scanf("%d",&n);
tot=tt=0;
ms(dp);
ms(vs);
msc(head);
for(int i=1;iint a,b;
scanf("%d %d",&a,&b);
vs[b]=true;
addedg(a,b);
addedg(b,a);
}
int a,b;
for(a=1;a<=n;a++) if(!vs[a]) break;
ms(vs);
dfs(a,1);
ST(2*n-1);
scanf("%d %d",&a,&b);
if(frt[a]else query(frt[b],frt[a]);
}
return 0;
}
在线算法
#include
#include
#include
#include
#include
#include
#include
#include
#define msc(X) memset(X,-1,sizeof(X))
#define ms(X) memset(X,0,sizeof(X))
typedef long long LL;
using namespace std;
const int maxn=905;
struct _Edg
{
int to,next;
}edg[2*maxn];
int head[maxn],tot,frt[maxn],vr[2*maxn],cnt,R[2*maxn],dp[2*maxn][11],ans[maxn];
bool vs[maxn];
void addedg(int u,int v)
{
edg[tot].to=v;
edg[tot].next=head[u];
head[u]=tot++;
}
void dfs(int u,int dph)
{
vs[u]=true;vr[++cnt]=u;R[cnt]=dph;frt[u]=cnt;
for(int i=head[u];i!=-1;i=edg[i].next)
if(!vs[edg[i].to])
{
dfs(edg[i].to,dph+1);
vr[++cnt]=u;R[cnt]=dph;
}
}
void ST(int n)
{
for(int i=1;i<=n;i++ ) dp[i][0]=i;
for(int j=1;(1<for(int i=1;i<=n;i++)
if(i+(1<<(j-1))<=n){
int a=dp[i][j-1],b=dp[i+(1<<(j-1))][j-1];
dp[i][j]=R[a]else dp[i][j]=dp[i][j-1];
}
void query(int a,int b)
{
int k=0;
while((1<<(k+1))<=b-a+1) k++;
int ra=dp[a][k],rb=dp[b-(1<1][k];
if(R[ra]else ans[vr[rb]]++;
}
int main(int argc, char const *argv[])
{
int n;
while(scanf("%d",&n)!=EOF)
{
ms(vs);
ms(ans);
msc(head);
ms(frt);
ms(dp);
ms(R);
tot=cnt=0;
for(int i=1;i<=n;i++){int np,nm;
scanf("%d:(%d)",&np,&nm);
while(nm--)
{
int sn;
scanf("%d",&sn);
addedg(np,sn);
addedg(sn,np);
vs[sn]=true;
}
}
int head=0;
while(++head<=n) if(!vs[head]) break;
ms(vs);
dfs(head,1);
ST(2*n-1);
int p;
scanf("%d",&p);
while(p--)
{
while(getchar()!='(') continue;
int a,b;
scanf("%d %d)",&a,&b);
if(frt[a]else query(frt[b],frt[a]);
}
for(int i=1;i<=n;i++) if(ans[i])
printf("%d:%d\n",i,ans[i] );
}
return 0;
}
LCA在线算法(倍增)
倍增法:
基本思想是:
deep[i] 表示 i节点的深度, fa[i,j]表示 i 的 2^j (即2的j次方) 倍祖先,那么fa[i , 0]即为节点i 的父亲,然后就有一个递推式子:
fa[i,j]= fa [ fa [i,j-1] , j-1 ]
设tmp = fa [i, j - 1] ,tmp2 = fa [tmp, j - 1 ] ,即tmp 是i 的第2 ^ (j - 1) 倍祖先,tmp2 是tmp 的第2 ^ (j - 1) 倍祖先 , 所以tmp2 是i 的第 2 ^ (j - 1) + 2 ^ (j - 1) = 2^ j 倍祖先
这样子一个O(NlogN)的预处理求出每个节点的 2^k 的祖先
然后对于每一个询问的点对a, b的最近公共祖先就是:
先判断是否 d[x]< d[y] ,如果是的话就交换一下(保证 x 的深度大于 y 的深度), 然后把 x 调到与 y 同深度, 同深度以后再把a, b 同时往上调,调到有一个最小的 j 满足fa [x,j] != fa [y,j] (x,y是在不断更新的), 最后再把(x,y)往上调(x=p[x,0], y=p[y,0]) ,一个一个向上调直到x = y, 这时 x或y 就是他们的最近公共祖先。
inline void dfs(int u)
{
int i;
for(i=head[u];i!=-1;i=next[i])
{
if (!deep[to[i]])
{
deep[to[i]] = deep[u]+1;
p[to[i]][0] = u; //p[x][0]保存x的父节点为u;
dfs(to[i]);
}
}
}
void init()
{
int i,j;
//p[i][j]表示i结点的第2^j祖先
for(j=1;(1<< j)<=n;j++)
for(i=1;i<=n;i++)
if(p[i][j-1]!=-1)
p[i][j]=p[p[i][j-1]][j-1];//i的第2^j祖先就是i的第2^(j-1)祖先的第2^(j-1)祖先
}
int lca(int a,int b)//最近公共祖先
{
int i,j;
if(deep[a]<deep[b])swap(a,b);
for(i=0;(1<
i--;
//使a,b两点的深度相同
for(j=i;j>=0;j--)
if(deep[a]-(1<<j)>=deep[b])
a=p[a][j];
if(a==b)return a;
//倍增法,每次向上进深度2^j,找到最近公共祖先的子结点
for(j=i;j>=0;j--)
{
if(p[a][j]!=-1&&p[a][j]!=p[b][j])
{
a=p[a][j];
b=p[b][j];
}
}
return p[a][0];
}