码迷,mamicode.com
首页 > 其他好文 > 详细

LCA(最近公共祖先)总结

时间:2021-07-02 15:51:34      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:ast   add   需要   访问   总结   空格   pre   span   贪心   

先留个坑

我回来了(

什么是 LCA

对于有根树T的两个结点u、v,最近公共祖先LCA(u,v)表示一个结点x,满足x是u和v的祖先且x的深度尽可能大。在这里,一个节点也可以是它自己的祖先。

以上, 百度百科中的定义, 不仅是你, 我也看不懂.
简单点说, 就是一棵有根树中两个节点的最近公共祖先, 下面我还是画图举例说明.

这次用的是这个画的树, 感觉有点奇怪不过没办法了(

技术图片

若上图中树根为1, 则有:
\(lca(3, 7)=1\)
\(lca(3, 2)=2\)
\(lca(3, 4)=2\)
等等

程序问题

洛谷上的模板题

tarjan 法


好处是速度更快, 时间复杂度\(O(n + m)\)

可以根据这篇blog中的图分析问题, 强烈建议带上纸笔跟着文字画一画.

思路讲一下:

首先保存每一个节点的父亲与儿子, 读取问题之后用 dfs 遍历的同时得出答案, 是一个离线算法.

P.S. 用到了并查集的思想.

我们初始化每个节点的父亲(fa[])为它自身, 当节点访问完后再更新他真正的父亲(树根的父亲是他自己). 对于每一个访问过的节点 \(i\), 如果 \(fa[i] = i\), 那么这个节点就没有访问完, 就一定是当前正在访问中的节点的祖先.

而在 dfs 搜索节点 cur 的时候, 我们需要做这些事:

  1. 遍历他所有的儿子.
  2. 查看有没有与当前节点有关的问题 \((cur, x)\), 如果有且 x 访问过, 我们便可以从 x 节点开始向上寻找第一个 \(fa[i] = i\) 的节点, 这个节点便是这 (cur, x) 的最近公共祖先.
  3. 更新节点 cur 的状态.

具体的还是看代码比较好吧(

#include <stdio.h>
#include <iostream>
#include <vector>
using namespace std;
int n, m, s;
int fa[500003];
int head[500003], next[1000003], val[1000003], cnt = 0;
struct NODE {
	int x;
	int id;
};
vector <NODE> q[500003];
int ans[500003];
bool vis[500003];		// 其实这东西似乎可以不要, 不过用了之后似乎会便于理解? 
inline void add (int x, int y) {
	val[++cnt] = y;
	next[cnt] = head[x];
	head[x] = cnt;
	return;
}
inline void init () { 
	for (int i = 1; i <= n; i++)
		fa[i] = i;
	return;
}
inline int find (int x) {		// 向上寻找 
	if (fa[x] != x)
		return find(fa[x]);
	return x;
}
inline void tarjan (int cur, int father) {		// dfs 
	// 1. 访问所有儿子 
	for (int i = head[cur]; i; i = next[i]) {
		int x = val[i];
		if (x == father)
			continue;
		tarjan(x, cur);
	}
	
	// 2. 查看所有与当前节点有关系的问题并回答能回答的 
	for (int i = 0; i < q[cur].size(); i++) {
		int x = q[cur][i].x;
		if (vis[x]) 
			ans[q[cur][i].id] = find(x);
	}
	
	// 3. 更新当前节点的信息 
	vis[cur] = true;
	fa[cur] = cur ==  s ? s : father;		// 注意 s (树根) 的父亲是他自己 
	return;
}
int main() {
	scanf("%d %d %d", &n, &m, &s);
	for (int i = 1; i < n; i++) {
		int a, b;
		scanf("%d %d", &a, &b);
		add(a, b);
		add(b, a);
	}
	for (int i = 1; i <= m; i++) {
		NODE nd;
		nd.id = i;
		int x, y;
		scanf("%d %d", &x, &y);
		nd.x = x, q[y].push_back(nd);
		nd.x = y, q[x].push_back(nd);
	}
	init();
	tarjan(s, 0);
	for (int i = 1; i <= n; i++)
		printf("%d\n", ans[i]);
	return 0;
}

倍增法


与这个算法有一点关系的东西(ST表)

慢一点, 但我觉得更好写一些.

直接贴源代码了, 不是很难, 注释(虽然只有核心代码写了一些)写的应该比较清楚, 实在不行看洛谷题解吧, 我懒得写了(

#include <stdio.h>
#include <iostream>
using namespace std;
int n, m, s;
int head[500003], val[1000003], last[1000003], cnt = 0;
int fa[500003][50], depth[500003];
int lg[500003];
inline void add(int x, int y) {
	val[++cnt] = y;
	last[cnt] = head[x];
	head[x] = cnt;
	return;
}
inline int lca(int x, int y) {
	// 1. 使得较深的那个节点向上移, 直至两个节点高度相同 
	if (depth[x] < depth[y])
		swap(x, y);
	while (depth[x] > depth[y]) 
		x = fa[x][lg[depth[x] - depth[y]] - 1];
	
	// 2. 两个节点一起向上移, 直至两个节点重合
	if (x == y)		// 特判, 即 y 是 x 的祖先的情况 
		return x;
	for (int k = lg[depth[y]] - 1; k >= 0; k--) 
		if (fa[x][k] != fa[y][k])
			x = fa[x][k], y = fa[y][k]; 
	return fa[x][0];
}
void dfs(int cur, int father, int high) {		// 获取所有节点的祖先和深度 
	depth[cur] = high;
	fa[cur][0] = father;
	for (int i = 1; i <= lg[depth[cur]]; i++)
		fa[cur][i] = fa[fa[cur][i-1]][i-1];
	for (int i = head[cur]; i; i = last[i])
		if (val[i] != father)
			dfs(val[i], cur, high + 1);
	return;
}
inline void init() {
	for (int i = 1; i <= n; i++)
		lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
	return; 
}
int main() {
	scanf("%d %d %d", &n, &m, &s);
	init();
	for (int i = 1; i < n; i++) {
		int x, y;
		scanf("%d %d", &x, &y);
		add(x, y);	add(y, x);
	}
	dfs(s, 0, 1);
	for (int i = 1; i <= m; i++) {
		int x, y;
		scanf("%d %d", &x, &y);
		printf("%d\n", lca(x, y));
	}
	return 0;
}

咕咕咕 应该还有后续内容(例题), 等着吧(

例题 [BZOJ1832 [AHOI2008]聚会]

题目描述

Y岛风景美丽宜人,气候温和,物产丰富。
Y岛上有N个城市(编号1,2,…,N),有N-1条城市间的道路连接着它们。
每一条道路都连接某两个城市。
幸运的是,小可可通过这些道路可以走遍Y岛的所有城市。
神奇的是,乘车经过每条道路所需要的费用都是一样的。
小可可,小卡卡和小YY经常想聚会,每次聚会,他们都会选择一个城市,使得3个人到达这个城市的总费用最小。
由于他们计划中还会有很多次聚会,每次都选择一个地点是很烦人的事情,所以他们决定把这件事情交给你来完成。
他们会提供给你地图以及若干次聚会前他们所处的位置,希望你为他们的每一次聚会选择一个合适的地点。

输入格式

第一行两个正整数,N和M,分别表示城市个数和聚会次数。
后面有N-1行,每行用两个正整数A和B表示编号为A和编号为B的城市之间有一条路。
再后面有M行,每行用三个正整数表示一次聚会的情况:小可可所在的城市编号,小卡卡所在的城市编号以及小YY所在的城市编号。

输出格式

一共有M行,每行两个数Pos和Cost,用一个空格隔开,表示第i次聚会的地点选择在编号为Pos的城市,总共的费用是经过Cost条道路所花费的费用。

输入样例#1

6 4 
1 2 
2 3 
2 4 
4 5 
5 6 
4 5 6 
6 3 1 
2 4 4 
6 6 6

输出样例#1

5 2 
2 5 
4 1 
6 0

数据范围

N≤500000,M≤500000

可以画图, 发现三个节点的 lca 中至少有两个相同, 分类讨论:

  1. 三个都相同, 则他们的 lca 即为聚会地点
  2. 有两个相同, 令其为 x, 另一个为 y, 则 x 一定是最佳的聚会地点, 具体的话可以画图, 用一个类似于贪心的思想去证明

所以我们只需要求出三个节点中两两的 lca, 然后几个 if 判断即可, 距离的话加一个前缀和(深度)就可以了.

代码如下

#include <stdio.h>
#include <iostream>
using namespace std;
int n, m;
int head[500003], val[1000003], last[1000003], cnt = 0;
int fa[500003][50], depth[500003];
int lg[500003];
inline void add(int x, int y) {
	val[++cnt] = y;
	last[cnt] = head[x];
	head[x] = cnt;
	return;
}
inline int lca(int x, int y) {
	if (depth[x] < depth[y])
		swap(x, y);
	while (depth[x] > depth[y]) 
		x = fa[x][lg[depth[x] - depth[y]] - 1];
	
	if (x == y)
		return x;
	for (int k = lg[depth[y]] - 1; k >= 0; k--) 
		if (fa[x][k] != fa[y][k])
			x = fa[x][k], y = fa[y][k]; 
	return fa[x][0];
}
void dfs(int cur, int father, int high) {
	depth[cur] = high;
	fa[cur][0] = father;
	for (int i = 1; i <= lg[depth[cur]]; i++)
		fa[cur][i] = fa[fa[cur][i-1]][i-1];
	for (int i = head[cur]; i; i = last[i])
		if (val[i] != father)
			dfs(val[i], cur, high + 1);
	return;
}
inline void init() {
	for (int i = 1; i <= n; i++)
		lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
	return; 
}
int main() {
	scanf("%d %d", &n, &m);
	init();
	for (int i = 1; i < n; i++) {
		int x, y;
		scanf("%d %d", &x, &y);
		add(x, y);	add(y, x);
	}
	dfs(1, 0, 1);
	for (int i = 1; i <= m; i++) {
		int x, y, z;
		scanf("%d %d %d", &x, &y, &z);
		int lca1 = lca(x, y), lca2 = lca(y, z), lca3 = lca(z, x), ans;
		if (lca1 == lca2)
			ans = lca3;
		else if(lca1 == lca3)
			ans = lca2;
		else
			ans = lca1;
		printf("%d %d\n", ans, depth[x] + depth[y] + depth[z] - depth[lca1] - depth[lca2] - depth[lca3]);
	}
	return 0;
}

咕咕咕, 后面还会有例题的.

LCA(最近公共祖先)总结

标签:ast   add   需要   访问   总结   空格   pre   span   贪心   

原文地址:https://www.cnblogs.com/RuiyangWu/p/14944853.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!