LCA,最近公共祖先,实现有多种不同的方法,对于树上的问题有着广泛的应用,比如树上的最短路径。
常用的解决LCA问题的算法有:Tarjan算法,Doubly/倍增算法,转化为RMQ问题等。
本文介绍基于DFS+二分搜索的在线算法,Doubly/倍增算法。(此处二分不同于二分查找算法那种)
算法初探:
对于已知的一棵树(已编号),若记录点v到根的深度为depth[v]。那么,如果节点w是u和v的祖先的话,让v向上走depth[v] - depth[w]步,u向上走depth[u] - depth[w]步,就都会走到w。因此,首先让u个v中深度较深的先向上走|depth[u] - depth[v]|步,再一步一步向上走,直到走到同一节点,就可以在O(depth[u] + depth[v])时间内求出LCA。
主要代码:
//输入
vector<int> tree[MAX_V];//vector存树
int root;//根节点
int parent[MAX_V];//父亲节点
int depth[MAX_V];//节点的深度
void dfs(int v, int fa) {
parent[v] = fa;
for (int i = 0; i < tree[v].size(); i++) {
if (tree[v][i] != fa) {
depth[tree[v][i]] = depth[v] + 1;
dfs(tree[v][i], v);
}
}
}
//预处理
void init() {
depth[root] = 0;
dfs(root, -1);
}
//计算u和v的LCA
int lca(int u, int v) {
//先让u和v先走到同一深度
while (depth[u] > depth[v]) u = parent[u];
while (depth[v] > depth[u]) v = parent[v];
//让u和v走到同一深度
while (u != v) {
u = parent[u];
v = parent[v];
}
return u;
}
算法改进->倍增算法
由于节点最大深度为N,此思路对于Q次查询的复杂度为O(Q*N),如果Q相对于N较大,此算法最少会达到O(N^2)级别,算法有没有可以改进的地方呢?
在节点向上走的过程,是一步一步走的,我们可以由此改进算法,减少走的次数。
对于每个节点,记录其往上走1个节点,2个节点,4个节点,8个节点,16个节点……2^k个节点所达到的节点编号,具体为利用节点其父亲节点信息,可以通过parent2[v] = parent[parent[v]]得到其向上走两步的节点,再通过这一信息,得到parent4[v] = parent2[parent2[v]],即其向上走四步的节点,以此类推......记录节点向上走2^k步的节点为parent[k][v]。有了k = (int)floor(logN / log(2.0))以内的所有信息,就可以二分搜索了。
代码:
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
using namespace std;
int n;//节点数量 0 ~ n - 1编号
int cnt, head[100005];//前向星存树
int depth[100005];//深度
int parent[20][100005];//parent[i][v]表示v节点往上走2^i步到达的顶点
struct node {
int to, next;
}edge[200010];
void add(int x, int y) {//树加边
cnt++;
edge[cnt].to = y;
edge[cnt].next = head[x];
head[x] = cnt;
}
void dfs(int x, int fa) {//DFS,搜索得每个点的深度 O(n)
parent[0][x] = fa;
for (int i = head[x]; i; i = edge[i].next) {
int y = edge[i].to;
if (y == fa) continue;
depth[y] = depth[x] + 1;
dfs(y, x);
}
}
void init() {//预处理,打2^k表 O(nlogn)
memset(depth, 0, sizeof(depth));
memset(parent, -1, sizeof(parent));
dfs(0, -1);//以0为根节点 预处理出parent[0]和depth
int k = (int)floor(log(n) / log(2.0));
for (int i = 0; i + 1 <= k; i++) {
for (int j = 0; j < n; j++) {
if (parent[i][j] == -1) parent[i + 1][j] = -1;
else parent[i + 1][j] = parent[i][parent[i][j]];//倍增思想
}
}
}
int lca(int x, int y) {//LCA O(logn)
if (depth[x] > depth[y]) swap(x, y);
int k = (int)floor(log(n) / log(2.0));
for (int i = 0; i <= k; i++) {//让x,y走到同一高度
if ((depth[y] - depth[x]) >> i & 1) {
y = parent[i][y];
}
}
if (x == y) return x;
for (int i = k; i >= 0; i--) {//二分搜索 二分搜索指的是下一个数值对应的步数是现在的一半,即二分了
if (parent[i][x] != parent[i][y]) {
x = parent[i][x];
y = parent[i][y];
}
}
return parent[0][x];
}
int main() {
int q;
int x, y;
while(scanf("%d%d%d",&n,&q)!=EOF) {
cnt = 0;
memset(head, 0, sizeof(head));
for (int i = 1; i < n; i++) {//n - 1条边
scanf("%d%d", &x, &y);
add(x, y);
add(y, x);
}
init();
for (int i = 0; i < q; i++) {
scanf("%d%d", &x, &y);
printf("%d\n", lca(x, y));
}
}
return 0;
}
由代码实现可知,改进后的算法,即Doubly算法的复杂度为O(nlogn + Q*logn),Q为查询次数。注:这里的2^i和二进制位密切相关,存储向上走2^i步到达的节点不是凭空而来的,目的就是之后走的步数数值的二进制位与这个相关联,相当于把数值的每个二进制位分离出来处理,减少计算量,就像快速幂计算。