- 求最近公共祖先的算法
一.向上标记法
从x向上走到根节点, 并标记路径上经过的点
从y向上走到根节点, 当遇到第一个被标记的点就找到了LCA(x, y)
二.树上倍增发
时间复杂度: O(mlogn)
用 f[x, k] 表示x向上走2^k到达的节点, 很容易知道 , f[x, 0] 就是x的父节点, 递推关系就是, f[x, k] = f[ f[x, k - 1], k - 1]
用depth[x], 存x的深度
步骤
1.预处理 f 数组和 depth 数组
2.实现目标函数: 保证 depth[x] > depth[y], 如果不是, 就想换x, y的值
3.用二进制拆分的思想, 把 x 调到和 y 同一个深度
4.如果此时 x == y, 那么y就是他们的公共祖先
5. 如果不相等,就把x 和 y 同时往上调, 并且保证他们不会相遇
6. 这样我们到达的就是他们的LCA的子节点, 最后结果就是f[x, 0]
对应步骤的代码
1.预处理部分
用bfs实现, 深度就是当前节点的父节点深度+1, f 数组就是利用那个递推式实现, 这里的15是取值范围的最大值的位数, 由题目而定
void bfs() //步骤1 预处理
{
memset(depth, 0x3f, sizeof depth);
depth[root] = 1;
depth[0] = 0;
queue<int> q;
q.push(root);
while(q.size())
{
int x = q.front();
q.pop();
for(int i = h[x]; ~i; i = ne[i])
{
int y = e[i];
if(depth[y] > depth[x] + 1)
{
depth[y] = depth[x] + 1;
q.push(y);
f[y][0] = x;
for(int j = 1; j <= 15; j++)
f[y][j] = f[f[y][j - 1]][j - 1];
}
}
}
}
2.lca函数部分
这里是求 x, y 的LCA
int lca(int x, int y)
{
if(depth[x] > depth[y]) //步骤2
swap(x, y);
for(int i = 15; i >= 0; i--) // 步骤3
{
if(depth[f[y][i]] >= depth[x])
y = f[y][i];
}
if(x == y) return x; //步骤4
for(int i = 15; i >= 0; i--) //步骤5
{
if(depth[f[x][i]] != depth[f[y][i]])
{
x = f[x][i];
y = f[y][i];
}
}
return f[y][0]; //步骤6 返回最后结果
}
三:Tarjan 算法
时间复杂度:O(n + m)
该算法的本质是用并查集对向上标记法进行优化, 达到线性的时间复杂度
在实现算法的过程中, 我们把树中的节点分为3类:
- 正在访问的节点, 以及它的到根节点这条路径上的节点, 标记为1
- 已经访问完毕, 并且已经回溯的节点吗,标记为2
- 还没有访问的节点, 不标记
由于这是一种离线算法, 我们需要把询问一次性全部读入, 然后再执行算法
3中节点解释
代码实现
void tarjan(int x)
{
vis[x] = 1;
for(int i = h[x]; ~i; i = ne[i])
{
int y = e[i];
if(!vis[y])
{
tarjan(y);
f[y] = x;
}
}
for(int i = 0; i < q[x].size(); i++)
{
PII t = q[x][i];
int y = t.first;
int id = t.second;
if(vis[y] == 2)
{
int father = find(y);
//此时这个father就是LCA答案
}
}
vis[x] = 2;
}