三种LCA算法(一):Doubly算法(倍增算法)

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步到达的节点不是凭空而来的,目的就是之后走的步数数值的二进制位与这个相关联,相当于把数值的每个二进制位分离出来处理,减少计算量,就像快速幂计算。


  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值