树的最近公共祖先(Lowest Common Ancestor)问题是树结构上最经典的问题之一。
给一棵树T,每个询问形如:“点u和点v 的公共祖先是哪个点?”,此问题的答案被记为LCA(u, v)。LCA问题的算法分为在线和离线(Tarjan算法)两种,前者要求在回答后一个问题之前必须给出前一个问题的输出,而离线问题允许在读入所有询问之后一次性给出所有问题的答案。
给一棵树T,每个询问形如:“点u和点v 的公共祖先是哪个点?”,此问题的答案被记为LCA(u, v)。LCA问题的算法分为在线和离线(Tarjan算法)两种,前者要求在回答后一个问题之前必须给出前一个问题的输出,而离线问题允许在读入所有询问之后一次性给出所有问题的答案。
LCA问题的应用很多,例如它可以用来回答这样的询问:“点u和点v的距离是多少?”——由于在树中两点的简单路是唯一的,所以这个距离等于u到LCA(u, v)再到v的距离,关键仍然是LCA。
Tarjan离线算法是基于树的dfs和并查集的,我们令x=LCA(u, v),用这么一句话解释这个算法:如果我们在访问完节点u及其后代后,将u的祖先指向为x,那么在访问x的其它后代节点v时,LCA(u,v)就为u的(当前的)祖先点x
int find(int x) {return fa[x] == x ? x : fa[x] = find(fa[x]);}//x的祖先节点
void tarjan(int u)//在main中调用tarjan(root)
{
vis[u] = true;
int i, v;
for (i = 1; i <= query[u][0]; ++i)//query[u][0]表示形如LCA(u,?)的查询个数
{
v = query[u][i];
if (vis[v]) /*printf LCA(u,v)=find(v) or others*/
}
for (i = 1; i <= son[u][0]; ++i)//son[u][0]表示u的儿子个数
{
v = son[u][i];
if (!vis[v]) tarjan(v), fa[v] = u;
}
}
例题:
POJ 1470 Closest Common Ancestors
此题样例的函数调用过程如下:
tarjan(5)
tarjan(1)
++sum[find(5)->5];
//1 done
fa[1]=5;
tarjan(4)
++sum[find(1)->find(5)->5];
//4 done
fa[4]=5;
tarjan(2)
++sum[find(4)->find(5)->5];
tarjan(3)
++sum[find(2)->2];//注意此时fa[2]尚未指向5
++sum[find(1)->find(5)->5];
++sum[find(4)->find(5)->5];
//3 done
fa[3]=2;
//2 done
fa[2]=5;
//5 done
代码实现:
/*375ms,6780KB*/
#include<cstdio>
#include<cstring>
const int mx = 905;
int sum[mx], son[mx][mx], query[mx][mx], fa[mx];
bool vis[mx], hasroot[mx];
int find(int x) {return fa[x] == x ? x : fa[x] = find(fa[x]);}
void tarjan(int u)
{
vis[u] = true;
int i, v;
for (i = 1; i <= query[u][0]; ++i)
{
v = query[u][i];///要想使sum增加,v必须是先前访问过的点
if (vis[v]) ++sum[find(v)];
}
for (i = 1; i <= son[u][0]; ++i)
{
v = son[u][i];
if (!vis[v]) tarjan(v), fa[v] = u;
}
}
inline void add(int a, int b)
{
++query[a][0];///我们用query[节点a][0]表示查询LCA(a,?)的个数
query[a][query[a][0]] = b;///记录这一查询LCA(a,b)
}
int main()
{
int n, i, j, k, Q;
while (~scanf("%d", &n))
{
memset(vis, 0, sizeof(vis));
memset(sum, 0, sizeof(sum));
memset(hasroot, 0, sizeof(hasroot));
for (i = 1; i <= n; ++i)
{
fa[i] = i, query[i][0] = 0;
scanf("%d", &j);
while (getchar() != '(');
scanf("%d", &son[j][0]);///注意,我们用son[节点编号][0]表示儿子个数
while (getchar() != ')');
for (k = 1; k <= son[j][0]; ++k)
{
scanf("%d", &son[j][k]);///记录此节点的儿子节点编号
hasroot[son[j][k]] = true;
}
}
scanf("%d", &Q);
while (Q--)
{
while (getchar() != '(');
scanf("%d%d", &i, &j);
while (getchar() != ')');
add(i, j), add(j, i);///双向添加查询
}
for (i = 1; i <= n; ++i)
if (!hasroot[i]) {tarjan(i); break;}///从根节点进入
for (i = 1; i <= n; ++i)
if (sum[i]) printf("%d:%d\n", i, sum[i]);
}
return 0;
}