OI退役笔记-019:图论(六)LCA

先放模板有时间补充:

算法的在线与离线

离线算法
算法设计策略都是基于在执行算法前输入数据已知的基本假设,也就是说,对于一个离线算法,在开始时就需要知道问题的所有输入数据,而且在解决一个问题后就要立即输出结果,通常将这类具有问题完全信息前提下设计出的算法成为离线算法(Offline Algorithms)

在线算法
在计算机科学中,一个在线算法是指它可以以序列化的方式一个个的处理输入,也就是说在开始时并不需要已经知道所有的输入。相对的,对于一个离线算法,在开始时就需要知道问题的所有输入数据,而且在解决一个问题后就要立即输出结果。例如,选择排序在排序前就需要知道所有待排序元素,然而插入排序就不必。
因为在线算法并不知道整个的输入,所以它被迫做出的选择最后可能会被证明不是最优的,对在线算法的研究主要集中在当前环境下怎么做出选择。对相同问题的在线算法和离线算法的对比分析形成了以上观点。如果想从其他角度了解在线算法可以看一下 流算法(关注精确呈现过去的输入所使用的内存的量),动态算法(关注维护一个在线输入的结果所需要的时间复杂度)和在线机器学习。

本段文字摘录与网络,但原作者链接访问不了了
【原作者】 【转载】

tarjan 算法求解 LCA

tarjan

#include <cstdio>

struct edge
{
	int nex, to;
} e[2001], l[2001];

int head[1001], lis[1001], tot1, tot2, f[1001], n, m, q;
bool vis[1001];

int find(int x)
{
	return ((x == f[x]) ? x : find(f[x]));
}

void merge(int a, int b)
{
	if((a = find(a)) == (b = find(b)))	return;
	f[b] = a;
}

inline void adde(int a, int b)
{
	e[++tot1].nex = head[a];
	e[tot1].to = b;
	head[a] = tot1;
	
	e[++tot1].nex = head[b];
	e[tot1].to = a;
	head[b] = tot1;
}

inline void addq(int a, int b)
{
	l[++tot2].nex = lis[a];
	l[tot2].to = b;
	lis[a] = tot2;
	
	l[++tot2].nex = lis[b];
	l[tot2].to = a;
	lis[b] = tot2;
}

inline void tarjan_lca(int x)
{
	
	vis[x] = 1;
	for (register int i = head[x]; i; i = e[i].nex)
	{
		if (vis[e[i].to])	continue;
		tarjan_lca(e[i].to);
		f[e[i].to] = x;
	}
	
	for (register int i = lis[x]; i; i = l[i].nex)
	{
		if (vis[l[i].to])
		{
			// find(l[i].to) is the LCA
			printf("%d<-->%d: %d\n", x, l[i].to, find(l[i].to));
		}
	}
}

int main()
{
	scanf("%d%d%d", &n, &m, &q);
	
	for (int x, y, i = 1; i <= m; ++i)
	{
		scanf("%d%d", &x, &y);
		adde(x, y);
	}
	
	for (int x, y, i = 1; i <= q; ++i)
	{
		scanf("%d%d", &x, &y);
		addq(x, y);
	}
	
	for (int i = 1; i <= n; ++i)
	{
		f[i] = i;
	}
	
	tarjan_lca(1);
}

/*
Input:
9 8 4
1 2
2 4
2 5
5 7
5 8
7 9
1 3
3 6
9 8
4 6
7 5
5 3

Ans:	(not output)
9 <--> 8 = 5
4 <--> 6 = 1
7 <--> 5 = 5
5 <--> 3 = 1

*/

倍增求解 LCA

普通的基本知识

倍增,是分治的一种算法。每次将问题分成完全独立的两半,利用两半的关系求解。
比如,快速幂就是一种倍增。
倍增的基本时间复杂度是 O(logn)。

普通倍增算法

倍增

下面用两张图解释:求 u 和 v 的最近公共祖先(root)

首先,先用 dfs(或者 bfs 等)算出每个点的深度(dep[i])、祖先节点信息(f[i][j] 表示 i 节点的第 2j 个祖先节点)。

之后,我们发现 u 和 v 节点的深度不同,同时明显可以得出,二者的 LCA 的深度一定大于等于 4。于是 u、v 的 LCA 可以转化为 u、t 的 LCA。而 v 和 t 的深度相差 13,因此我们跳跃 3 次,分别跳 8、4、1 层(每次跳跃的层数是不大于深度差的最大的、且只有 2 作为约数的数)。

现在,问题已经转化为 u 和 t 的 LCA 了。此时我们再次倍增,先可以访问到 f[t][1](绿色节点)和 f[u][1](紫色节点)发现此时 f[t][1] ≠ f[u][1],说明二者的 LCA 的深度比比 2 大。于是我们让新的 f[u][1] 代替 u、新的 f[t][1] 代替 t,求 f[u][1] 和 f[t][1] 的 LCA。以此类推,直到二者相等为止。
两张图解释
但是如果相等就结束了吗?其实不是。倍增是从远到进倍增的,当前所求的解一定是公共祖先,但不一定是最近公共祖先。于是相等时我们不能退出循环,要继续搜索,直到循环结束,才得到我们目标节点,该节点的父节点就是 LCA(最后 f[u][0] = f[t][0],因此 f[u][0] 即 u 的父节点才是 LCA)。

模板代码

#include <cstdio>

#define N	100001
#define M	500001
#define swap(a, b)	({(a) ^= (b); (b) ^= (a); (a) ^= (b);})

// 基本代码 
struct edge
{
	int to, nex;
} e[M << 1];

// dep[i] 表示 i 号节点的深度
 
// f[i][j] 表示 i 号节点的第 2 ^ j 个祖先 
// 特别地,f[i][0] 表示 i 号节点的父节点
// 因为 2 ^ (j - 1) + 2 ^ (j - 1) = 2 ^ j,
// 所以一个节点的 2 ^ j 代的祖先是其 2 ^ (j - 1) 代的祖先的第 2 ^ (j - 1) 个的祖先
// 于是有: f[i][j] = f[f[i][j - 1]][j - 1]
// 附:f[N][L],L 取 30 即可 

// log[i] 存储 log2(i) 的下取整 int 值
// 但不需要调用 log2(int) 函数,递推可求 
// 注意:log[L],L = N 
int tot, head[N], vis[N], dep[N], f[N][31], log[N];
int n, q, a, b;

inline void add(int x, int y)
{
	e[++tot].nex = head[x];
	e[tot].to = y;
	head[x] = tot;
	
	e[++tot].nex = head[y];
	e[tot].to = x;
	head[y] = tot;
}

// 深搜获取节点深度及祖先信息
// x 表示目标节点,pre 表示父节点 
void dfs(int x = 1, int pre = 0)
{
	// 已经得到深度及信息 
	if (vis[x])	return;
	
	vis[x] = 1;
	dep[x] = dep[pre] + 1;
	f[x][0] = pre;
	
	// 因为 x 的 2 ^ i 级祖先最多是根节点
	// 所以 2 ^ i <= dep[x],推出:i <= log2(dep[x]) 
	// 可以打表储存,log[i] = log(i) 
	for (int i = 1; i <= log[dep[x]]; ++i)
		// 状态转移
		f[x][i] = f[f[x][i - 1]][i - 1];
		
	for (int i = head[x]; i; i = e[i].next)
		dfs(e[i].to, x);
}

// 打表求 log[]
void mklog()
{
	for (int i = 2; i < N; ++i)
		log[i] = log[i / 2] + 1;
}

int LCA(int x, int y)
{
	// 不妨设 dep[x] >= dep[y]
	// 则先要让 y 跳到与 x 节点同样深度的父节点 
	if (dep[x] < dep[y])
		swap(x, y);
		
	while (dep[x] != dep[y])
		// dep[x] - dep[y] 是高度差
		// 比如深度相差 l,每次跳 k 阶 
		// 满足 k = 2 ^ a 且 k <= dep[x] - dep[y], 2 ^ (a + 1) > dep[x] - dep[y] 
		x = f[x][log[dep[x] - dep[y]]];
	
	// 调整深度后两点相遇 
	if (x == y)	return x;
	
	// 逆向思考:如何在 a、b 不相遇的情况下跳到尽可能高的位置
	// 该位置的父节点即为 LCA
	// 从可能跳的最大步数 log[dep[x]], 开始,不断减小步数到 0
	// 此处用 k 的原因是 k 表示幂 
	for (int k = log[dep[x]]; k >= 0; --k)
		// 未找到 
		if (f[x][k] != fa[y][k])
		{
			// 跳跃 
			x = f[x][k];
			y = f[y][k];
		}
		// 如果找到一点满足要求,该点可能不是最近的
		// 因此需要继续减半查找
	
	// 搜索结束,当前点的父节点即为 LCA 
	return f[x][0];
}

int main()
{
	// 读入树
	// 按无向图方法存边 
	
	mklog();
	// 对于一棵树,任意节点可看做根节点
	// 这里用 1 做根节点 
	dfs();
	
	// 读入问题
	
	// LCA(x, y) 即为最近公共祖先 
	// 附:x、y 两点距离:dep[a] + dep[b] - 2 * dep[LCA(a, b)] 
}

ST 表前置知识——RMQ

ST 表详解

ST 表

作者:Rotch
日期:2021-05-13
修改:2020-05-17

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值