需要知识
dfs序和并查集伪代码
tarjan(u) { for each(u, v) { // 枚举u的子节点v tarjan(v) merge(v, u) // 把v合并到u上 vis[v] = 1 } for each(u, v) // 枚举和u相关的询问 if (vis[v]) LCA(u, v) = find(v) }
思路
总思路就是每进入一个节点u的深搜,就把整个树的一部分看作以节点u为根节点的小树,再搜索其他的节点。每搜索完一个点后,如果该点和另一个已搜索完点为需要查询LCA的点,则这两点的LCA为另一个点的现在的祖先。
1.先建立两个链表,一个为树的各条边,另一个是需要查询最近公共祖先的两节点。
2.建好后,从根节点开始进行一遍深搜。
3.先把该节点u的father设为他自己(也就是只看大树的一部分,把那一部分看作是一棵树),搜索与此节点相连的所有点v,如果点v没被搜索过,则进入点v的深搜,深搜完后把点v的father设为点u。
4.深搜完一点u后,开始判断节点u与另一节点v是否满足求LCA的条件,满足则将结果存入数组中。
5.搜索完所有点,自动退出初始的第一个深搜,输出结果。
举个栗子
• 询问<E,G>, <F,D>, <K,J>的LCA
• f[]数组为并查集的父节点数组,初始化f[i] = i
• vis[]数组为是否访问过的数组,初始化为0
• A->B->E,发现E没有子节点,寻找与其相关的询问,发现有<E,G>,但是vis[G]=0,所以不操作,返回B,vis[E]=1,f[E]=B.
• 搜索F,发现有询问<F,D>,但是vis[D]=0,所以不操作,返回B,vis[F]=1, f[F]=B.
• 搜索G,发现有询问<E,G>,且vis[E]=1,得到LCA(E,G)=find(E)=B,然后返回B, vis[G]=1, f[G]=B.
• B搜索完毕,返回A,vis[B]=1, f[B]=A.
• A->C->H->C->A,vis[C]=vis[H]=1, f[H]=C, f[C]=A.
• A->D->I->K,发现询问<K,J>,但vis[J]=0,所以不操作,返回I,vis[K]=1, f[K]=I.
• 搜索L和M,无操作,f[L]=f[M]=I,vis[L]=vis[M]=1,返回I
• I搜索完了,返回D,f[I]=D, vis[I]=1
• 搜索J, 发现询问<K,J>,且vis[K]=1,得到LCA(K,J)=find(K)=D, 返回D.
• D搜索完了,发现询问<F,D>,且vis[F]=1,得到LCA(F,D)=find(F)=A, 返回A.
• A搜索完了,没有找到询问,算法结束.
• 我们在O(n + q)的时间复杂度内完美的解决了离线查询LCA问题。
Tarjan算法代码实现
• 树的存储结构:
structedge{ int v,next; }e[M]; int h[N],etot;
• 采用邻接链表来存树,其中h[]是头数组,e[]是边数组
• 添加边
void add_edge(int x, int y){ e[etot].v =y; e[etot].next =h[x]; h[x] = etot++; }
• h[]初始化为-1,etot初始化为0
• 询问也要按照邻接链表存起来
struct query{ int v, next,ans; }q[M]; int Q[N],qtot;
• Q[]初始化为-1,qtot初始化为0
• 对于每一个询问(u, v),调用add_query(u, v), add_query(v, u)
void add_query(int u, int v){ q[qtot].v =v; q[qtot].next =Q[u]; qtot++; }
• 维护一个并查集,f[]记录每个点的父亲,初始f[i]=i
int find(int x){ if (x == f[x]) returnx; else return f[x] =find(f[x]); } void merge(int v, intu){ v =find(v); u =find(u); if (v != u) fa[v] =u; }
• Tarjan算法主体,dfs的过程中合并父亲,回答询问。
void tarjan(int u, int fa){ for (int i = h[u]; i != -1; i = e[i].next){ if (e[i].v != fa){ tarjan(e[i].v,u); merge(e[i].v,u); vis[e[i].v] =1; } } for (int i = Q[u]; i != -1;i =q[i].next) if(vis[[i].v]) q[i].ans =find(q[i].v); }