LCA总结

本文详细介绍了树的最近公共祖先(LCA)算法,包括朴素算法、优化算法、倍增算法和Tarjan算法。还探讨了LCA在求两点距离、树上差分和次小生成树等问题中的应用。通过对树结构的深度优先遍历和二进制拆分等技术,提高了查询效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

LCA

一、定义

给定一颗有根数,若节点 zzz 即使节点 xxx 的祖先,又是节点 yyy 的祖先,则称 zzzxxxyyy 的公共祖先;

xxxyyy 的所有公共祖先中,深度最大的一个则为 xxxyyy 的最近公共祖先(LCA, Lowest Common Ancestor);

如图所示,

定义举例

  1. bbbccc 的 LCA 为 aaa
  2. dddccc 的 LCA 为 aaa ;

LCA 是唯一的;

二、特点

对于两个节点 uuuvvv ,它们的相互关系与 LCA 必定是下列情况中的一种;

  1. vvvuuu 的子节点 (或子孙节点) , 则 LCA(u,v)LCA(u, v)LCA(u,v)uuu
  2. vvvuuu 的父节点 (或祖先节点) , 则 LCA(u,v)LCA(u, v)LCA(u,v)vvv
  3. uuu 是节点 www 的子节点 (或子孙节点) , vvvwww 的子节点 (或子孙节点) , 则 uuuvvv 的 LCA 为 www 的父节点 (或祖先节点) ;

根据以上情况,可得到结论;

给定树上的节点 uuu ,则 uuuuuu 的子树节点的 LCA 为 uuuuuu 的不同子树节点之间的 LCA 也为 uuu

三、朴素算法

1. 思路

通过搜索,可确定每个节点的父节点,再通过父节点进行逆向查找,从而得到一条从指定节点到根节点的路径,两个点的 LCA 即为两条路径的第一个交点;

2. 过程

u,vu,vu,v 的 LCA 时,可以先确定节点 uuu 与节点 vvv 的所有祖先节点,从 uuuvvv 从下向上对其父节点进行标记,第一个被两个节点标记的即为 u,vu, vu,v 的 LCA;

3.代码

nnn 个节点, mmm 条边,根结点为 rootrootroot 的树为例;

int n, m, root, father[MAXN];
vector < int > g[MAXN];
bool flag[MAXN];
void dfs(int i) { // dfs 预处理出节点的父节点
	flag[i] = true;
	for (int t = 0; t < g[i].size(); t++) {
		int v = g[i][t];
		if (!flag[v]) {
			father[v] = i;
			dfs(v);
		}
	}
	return;
}
int LCA(int u, int v) {
	bool f[MAXN] = { };
	while (u != root) { // 先标记 u 节点的父节点
		f[u] = true;
		u = father[u];
	}
	while (v != root) { // 标记 v 节点的父节点
		if (f[v] == true) return v; // 若被重复标记,则已找到
		v = father[v];
	}
	return root;
}

若有 qqq 个询问,则算法时间复杂度为 O(qnlogn)O(qnlogn)O(qnlogn)

4. 优化

朴素算法查询中,需要反复查询才能确定两个节点的 LCA,效率较低,则可优化为两点一起向上走;

过程

搜索预处理时,处理出节点的父节点以及节点的深度;

对于节点 u,vu, vu,v ,若两节点的深度不同,则先将深度较大的节点沿着其的父结点移动至两点的深度相同,接下来,则可将两个节点每次同时向上移动一个节点,直到到同一个节点为止,此节点则为 u,vu, vu,v 的 LCA;

与朴素算法相比,优化利用了节点的深度,从而避免了反复查询,其时间复杂度为 O(qn)O(qn)O(qn)

代码

nnn 个节点, mmm 条边,根结点为 rootrootroot 的树为例;

int n, m, root, father[MAXN], dep[MAXN];
vector < int > g[MAXN];
bool flag[MAXN];
void dfs(int i) { // dfs 预处理出节点的父节点以及节点的深度
	flag[i] = true;
	for (int t = 0; t < g[i].size(); t++) {
		int v = g[i][t];
		if (!flag[v]) {
            dep[v] = dep[i] + 1;
			father[v] = i;
			dfs(v);
		}
	}
	return;
}
int LCA(int u, int v) {
	if (dep[u] < dep[v]) swap(u, v); // 为了方便处理,将 u 节点深度设为较大的
    int dis = dep[u] - dep[v]; // 计算两点深度差
    while (dis--) {
        u = father[u]; // 使两点深度相同
    }
    while (u != v) { // 再使两点一起向上走至同一个结点,即为 LCA
        u = father[u];
        v = father[v];
    }
    return u;
}

四、倍增

朴素算法中,由于每次只能向上跳动一个节点,其效率较低;

如果能向上跳动时,跳过某些明显不可能是最近公共祖先的节点,其求解速度会明显提高;

1. 思路

利用二进制拆分原理,在每次向上移动节点时,以2的次数幂移动;

假设总共 nnn 个节点,则最大的跳跃跨度幂次为为 i=⌈log∣n∣⌉i = \lceil log |n| \rceili=logn ,即从叶结点向上跳跃 2⌈log∣n∣⌉2^{\lceil log |n| \rceil}2logn 个结点一定可以到达根结点;

预处理时间复杂度为 O(nlogn)O(nlogn)O(nlogn) ,查询时间复杂度为 O(qlogn)O(qlogn)O(qlogn)

2. 过程

预处理

预处理出节点的深度以及节点第 2i2^i2i 个祖先;

则可用树形动态规划进行处理;

状态

dp[i][j]dp[i][j]dp[i][j] 节点 iii 的第 2j2^j2j 个祖先;

jjj 开到 32 即可 (231=2147483648)(2^{31} = 2147483648)(231=2147483648)

转移

根据2的幂次特性, 2i=2i−1+2i−12^i = 2^{i - 1} + 2^{i - 1}2i=2i1+2i1 ,节点 iii 的第 2j2^j2j 个结点即为节点 iii 的第 2i−12^{i - 1}2i1 个祖先节点的第 2i−12^{i - 1}2i1 个祖先节点;
dp[i][j]=dp[dp[u][j−1]][j−1] dp[i][j] = dp[dp[u][j - 1]][j - 1] dp[i][j]=dp[dp[u][j1]][j1]
由于有2的幂次特性以及二进制拆分原理,可分别保证该 DP 的转移与正确性;

查询
思路

对于查询 (u,v)(u, v)(u,v)

先将 uuuvvv 调整为相同深度,即将深度较大的节点每次向上跳 2log2(dep[u]−dep[v])2^{log2(dep[u] - dep[v])}2log2(dep[u]dep[v]) 步,跳到两节点深度相同;

将两点同此向上跳 2jj∈(0,log2[dep[v]])2^j j\in (0, log2[dep[v]])2jj(0,log2[dep[v]]) 步,如果两点跳到的祖先结点不同,则当前点不可能为两点 LCA,可继续向上跳;当两点跳到相同祖先结点时,当前结点即为两点 LCA;

对于 jjj ,由于跳跃步数为2的幂次呈指数级增长,所以指数越大,增长越快,所以从大向小枚举,可使枚举次数更少

证明

如果倍增跳两点跳到的点不同,应继续向上跳

假设 u,vu, vu,v 分别跳到了 (j,k)(j, k)(j,k)LCA(u,v)LCA(u, v)LCA(u,v)iii ;

  1. dep[j]=dep[k]>dep[i]dep[j] = dep[k] > dep[i]dep[j]=dep[k]>dep[i] 时;

倍增正确性证明1

iii 点深度较 j,kj, kj,k 浅,(j,k)(j, k)(j,k) 应继续向上跳;

  1. dep[j]=dep[k]<dep[i]dep[j] = dep[k] < dep[i]dep[j]=dep[k]<dep[i] 时;

倍增正确性证明2

由于此结构为有根树,对于这种情况,其树根只能是 iii ,又因为向上跳的 2jj∈(0,log2[dep[v]])2^j j\in (0, log2[dep[v]])2jj(0,log2[dep[v]]) 步,则矛盾;

3. 代码

int n, m, log1[MAXN], dep[MAXN], dp[MAXN][MAXX], root;
vector < int > g[MAXN];
bool flag[MAXN];
void logset() { // 预处理 log
	log1[1] = 0;
	for (int i = 2; i <= MAXN; i++) {
		log1[i] = log1[i / 2] + 1;
	}
	return;
}
void dfs(int i) {
	flag[i] = true;
	for (int t = 0; t < g[i].size(); t++) {
		int v = g[i][t];
		if (!flag[v]) {
			dep[v] = dep[i] + 1; // 预处理深度
			dp[v][0] = i; // v 节点向上跳1步,即为其父节点 i
			for (int j = 1; j <= log1[dep[v]]; j++) {
				dp[v][j] = dp[dp[v][j - 1]][j - 1];
			}
			dfs(v);
		}
	}
	return;
}
int LCA(int u, int v) {
	if (dep[u] < dep[v]) swap(u, v); // 为了方便处理,将 u 节点深度设为较大的
	while (dep[u] != dep[v]) { // 将两点跳到相同深度
		u = dp[u][log1[dep[u] - dep[v]]]; // 每次最多跳 log2(dep[u] - dep[v]) 步
	}
	if (u == v) return u; // 已经找到 LCA
	for (int i = log1[dep[u]]; i >= 0; i--) { // 倒序枚举,枚举次数较正向枚举较小
		if (dp[u][i] != dp[v][i]) {
			u = dp[u][i], v = dp[v][i]; // 同时向根结点条
		}
	}
	return dp[u][0];
}

五、Tarjan 算法

1. 思路

若已知所有寻要查询的 LCA 节点对,则可使用更为高效的离线算法 Tarjan 算法;

Tarjan 算法结合了 DFS 遍历图以及 LCA 的如下特点;

对于节点 uuuuuu 的任意一个子节点 vvv ,以下两者相同

  1. vvv 与树中除了以 uuu 为根的子树以外的其他任意节点 xxx 的 LCA;
  2. uuu 与树中除了以 uuu 为根的子树以外的其他任意节点 xxx 的 LCA;

所以,可将 uuu 与以 uuu 为根的子树看作一个结点的集合,将 uuu 看作这个集合的代表,即集合中的节点与集合之外的树中任意节点的 LCA 等同于 uuu 与集合之外的树中任意节点的 LCA ;

由于树具有 “自相似” 的内部结构,即可将树看作很多个类似集合的组合,使用集合的代表进行 LCA 求解来得到集合中节点的解;

2. 过程

利用 DFS 遍历与并查集实现,并查集维护节点关系;

即在 DFS 遍历时,将根结点与其子节点合并;

由于两个节点通过其 LCA 被合并,所以在合并完子树后,遍历与当前节点有关的查询,若查询的另一个节点也已访问,则两点此时一定有且仅通过其的 LCA 合并,所以节点对所在并查集的根结点即为节点对的 LCA ;

即,Tarjan 算法按照深度优先遍历过程,从一个指定节点 uuu 开始遍历,逐个访问 uuu 的子节点 vvv ,递归求解节点对的 LCA ;

当遍历到节点时,

  1. 遍历完节点的子节点,并将节点与其子节点合并;
  2. 将节点标记为已访问;
  3. 遍历与当前节点有查询关系的节点,如果两个节点均已访问,那么节点对的 LCA 就是所在并查集的根结点,记录即可;

注意

由于询问的节点对先后顺序不一定按照算法遍历中结点的先后顺序给出,所以存储节点对时,应当双向存储,保证两节点不受访问先后顺序影响;

3. 代码

int n, m, root, father[MAXN], ans[MAXN];
vector < int > g[MAXN];
vector < int > q[MAXN]; // 存储访问
vector < int > q_id[MAXN]; // 存储访问编号
bool flag[MAXN], colour[MAXN]; // 标记节点
void firstset(int n) {
	for (int i = 1; i <= n; i++) {
		father[i] = i;
	}
	return;
}
int findset(int x) {
	if (x == father[x]) return x;
	return father[x] = findset(father[x]);
}
void push(int x, int y) {
	father[findset(x)] = findset(y);
	return;
}
void tarjan(int i) {
	flag[i] = true;
	for (int t = 0; t < g[i].size(); t++) {
		int v = g[i][t];
		if (!flag[v]) {
			tarjan(v); // DFS 遍历树
			push(v, i); // 合并并查集
		}
	}
	colour[i] = true; // 标记为已访问
	for (int t = 0; t < q[i].size(); t++) { // 与当前节点有关的问题
		int v = q[i][t], id = q_id[i][t];
		if (colour[v]) { // 如果两节点均被访问
			ans[id] = findset(v); // 即为两点所在集合的根结点
		}
	}
	return;
}
int main() {
	scanf("%d %d %d", &n, &m, &root);
	firstset(n);
	for (int i = 1; i < n; i++) {
		int x, y;
		scanf("%d %d", &x, &y);
		g[x].push_back(y);
		g[y].push_back(x);
	}
	for (int i = 1; i <= m; i++) {
		int x, y;
		scanf("%d %d", &x, &y);
		q[x].push_back(y), q_id[x].push_back(i);  // 双向建立节点对
		q[y].push_back(x), q_id[y].push_back(i);
	}
	tarjan(root);
	for (int i = 1; i <= m; i++) {
		printf("%d\n", ans[i]);
	}
	return 0;
}

六、欧拉序RMQ算法

1. 思路

由于欧拉序可以将树形结构转化为线性结构,LCA 求两点深度最大的公共祖先,想到用欧拉序与 RMQ 维护区间最大值计算;

在欧拉序中,遍历的顺序为先遍历一棵子树,再遍历根结点,从而遍历其他的子树;

又因为在欧拉序中节点 xxx 第一次出现的位置到最后一次出现的位置之间都是 xxx 子树上的节点;

所以在 xxx 节点最后一次出现到 yyy 节点第一次出现间的节点即为 xxxyyy 的公共祖先;

但因为 xxx 可能为 yyy 的祖先,或 yyy 可能为 xxx 的祖先;

所以因查找 xxxyyy 第一次出现的区间内的节点;

用 RMQ 计算这个区间内深度最大的节点即可;

2. 过程

先遍历出树的欧拉序;

再用 RMQ 维护欧拉序中节点深度的最大值;

当查询 uuuvvv 的 LCA 时,查询在欧拉序中 uuu 第一次出现位置与 vvv 第一次出现位置的区间内的深度最大值所在的节点即为 uuuvvv 的 LCA;

3. 代码

int n, m, root, dep[MAXN], a[MAXN], len, b[MAXN], fi[MAXN];
int dp[MAXN][MAXX], pre[MAXN][MAXX], log1[MAXN];
vector < int > g[MAXN];
bool flag[MAXN];
void dfs(int i) { // 遍历欧拉序
	flag[i] = true;
	a[++len] = i;
	b[len] = dep[a[len]];
	fi[a[len]] = len;
	for (int t = 0; t < g[i].size(); t++) {
		int v = g[i][t];
		if (!flag[v]) {
			dep[v] = dep[i] + 1;
			dfs(v);
			a[++len] = i;
			b[len] = dep[a[len]];
		}
	}
	return;
}
void init(int len) { // RMQ 维护欧拉序中深度最大值
	for (int i = 1; i <= len; i++) {
		dp[i][0] = b[i]; // dp 存储节点深度
		pre[i][0] = a[i]; // pre 存储节点编号
	}
	for (int j = 1; (1 << j) <= len; j++) {
		for (int i = 1; i + (1 << j) - 1 <= len; i++) {
			if (dp[i][j - 1] < dp[i + (1 << j - 1)][j - 1]) {
				dp[i][j] = dp[i][j - 1];
				pre[i][j] = pre[i][j - 1];
			} else {
				dp[i][j] = dp[i + (1 << j - 1)][j - 1];
				pre[i][j] = pre[i + (1 << j - 1)][j - 1];
			}
		}
	}
	return;
}
void logset() { // 预处理 log
	log1[1] = 0;
	for (int i = 2; i <= MAXN; i++) {
		log1[i] = log1[i / 2] + 1;
	}
	return;
}
void firstset(int root) {
	dfs(root); // 进行欧拉序遍历
	init(len); // RMQ 预处理
	logset(); // log 预处理
	return;
}
int LCA(int l, int r) {
	l = fi[l], r = fi[r]; // l,r 第一次出现位置
	if (l > r) swap(l, r);
	int k = log1[r - l + 1];
	if (dp[l][k] < dp[r - (1 << k) + 1][k]) {
		return pre[l][k];
	} else {
		return pre[r - (1 << k) + 1][k]; // 返回深度最大的节点编号
	}
}
int main() {
	scanf("%d %d %d", &n, &m, &root);
	for (int i = 1; i < n; i++) {
		int x, y;
		scanf("%d %d", &x, &y);
		g[x].push_back(y);
		g[y].push_back(x);
	}
	firstset(root);
	for (int i = 1; i <= m; i++) {
		int x, y;
		scanf("%d %d", &x, &y);
		printf("%d\n", LCA(x, y));
	}
	return 0;
}

七、LCA应用

1. 求两点距离

思路

在树上,两点 (u,v)(u, v)(u,v) 的最短路径则为从 uuu 向上走到 LCA(u,v)LCA(u, v)LCA(u,v) 再向下走到 vvv

证明

若不经过两点的 LCA 继续向上走,由于是树形结构,所以一定会重复走边,则不是最短;

代码
int dis(int u, int v) {
	return dep[u] - dep[v] - 2 * dep[LCA(u, v)];
}

2.树上差分

思路

树上差分可实现快速对两点路径上的节点权值进行修改;

以在 uuuvvv 的路径上节点增加 xxx , iii 节点的权值为 valival_ivali 为例;

则先在 uuuvvv 的权值上加 xxx ,但由于前缀和时为从深度大的节点像深度小的节点加,所以所有从 u,vu, vuv 到根结点的路径上的节点均会 +x+x+x ;

则为了不影响到除了路径以外的点权值,则在 uuuvvv 路径上深度最小的节点 LCA(u,v)LCA(u, v)LCA(u,v) 上使 valLCA(u,v)−=x∗2val_{LCA(u, v)} -= x * 2valLCA(u,v)=x2 即可,由于 uuuvvv 上各增加了 xxx ,则到 LCA 除一共增加了两个 xxx ,所以减去 2∗x2 * x2x

树上差分示意图

则修改方法为 valu+x,valv+x,valLCA(u,v)−=x∗2val_u + x, val_v + x, val_{LCA(u, v)} -= x * 2valu+x,valv+x,valLCA(u,v)=x2

再做一次树上前缀和即可得到节点权值;

每条边遍历的次数即为该边所连接两点中深度较小的节点的权值;

代码
void change(int u, int v, int x) { // u 到 v 路径上增加 x
	c[u] += x;
	c[v] += x;
	c[LCA(u, v)] -= 2 * x;
}
void dfs1(int i) { // 树上前缀和
	flag[i] = true;
	for (int t = 0; t < g[i].size(); t++) {
		int v = g[i][t];
		if (!flag[v]) {
			dfs1(v);
			c[i] += c[v];
		}
	}
	return;
}

3.次小生成树

思路

通过替换最小生成树中的一条边得到次小生成树;

当将一条非树边 (x,y,z)(x, y, z)(x,y,z) 加入最小生成树时,则会与 xxxyyy 路径上的节点一起形成环;

则需替换一条边,设 xxxyyy 的路径上的最大边权为 val1val_1val1 ,严格次大边权为 val2val_2val2 ,则对于次小生成树有两种情况;

  1. z>val1z > val_1z>val1 ,则把 val1val_1val1 对应的边替换为 (x,y,z)(x, y , z)(x,y,z) ,此时严格次小生成树边权和为 sum−val1+zsum - val_1 + zsumval1+z
  2. z==val1z == val_1z==val1 ,则把 val2val_2val2 对应的边替换为 (x,y,z)(x, y , z)(x,y,z) ,此时严格次小生成树边权和为 sum−val2+zsum - val_2 + zsumval2+z

则枚举每条非树边,添加到最小生成树中,按上述条件计算出候选答案,从中选择最小的即可;

则问题转化为如何求 xxxyyy 的路径上的最大边权与严格次大边权;

使用树上倍增法,

dp[i][j]dp[i][j]dp[i][j] 表示节点 iii 的第 2j2^j2j 个祖先;

则转移同倍增 LCA 算法
dp[i][j]=dp[dp[u][j−1]][j−1] dp[i][j] = dp[dp[u][j - 1]][j - 1] dp[i][j]=dp[dp[u][j1]][j1]
dp1[i][k]dp1[i][k]dp1[i][k] 表示从节点 iii 到其的第 2j2^j2j 个祖先的路径上的最大边权;

转移时类似 ST 表的转移,即取 从节点 iii 到其的第 2j−12^{j - 1}2j1 个祖先的路径上的最大边权 与 从节点 iii 的第 2j−12^{j - 1}2j1 个祖先到其的第 2j−12^{j - 1}2j1 个祖先的路径上的最大边权 的最大值即可;
dp1[i][j]=max⁡{dp1[i][j−1],dp1[dp[i][j−1]][j−1]} dp1[i][j] = \max \{ dp1[i][j - 1], dp1[dp[i][j - 1]][j - 1] \} dp1[i][j]=max{dp1[i][j1],dp1[dp[i][j1]][j1]}
dp2[i][k]dp2[i][k]dp2[i][k] 表示从节点 iii 到其的第 2j2^j2j 个祖先的路径上的严格次大边权;

对于转移,有 3 种情况;

  1. dp1[i][j−1]>dp1[dp[i][j−1]][j−1]dp1[i][j - 1] > dp1[dp[i][j - 1]][j - 1]dp1[i][j1]>dp1[dp[i][j1]][j1]

    即最大边权在 节点 iii 到其的第 2j−12^{j - 1}2j1 个祖先的路径 上时,则次大边权不能选取 节点 iii 到其的第 2j−12^{j - 1}2j1 个祖先的路径 上的最大值,则选取 节点 iii 到其的第 2j−12^{j - 1}2j1 个祖先的路径 上的次大值 以及 节点 iii 的第 2j−12^{j - 1}2j1 个祖先到其的第 2j−12^{j - 1}2j1 个祖先的路径 上的最大边权 的最大值即可;

    状态转移方程如下,
    dp2[i][j]=max⁡{dp2[i][j−1],dp1[dp[i][j−1]][j−1]} dp2[i][j] = \max \{ dp2[i][j - 1], dp1[dp[i][j - 1]][j - 1] \} dp2[i][j]=max{dp2[i][j1],dp1[dp[i][j1]][j1]}

  2. dp1[i][j−1]<dp1[dp[i][j−1]][j−1]dp1[i][j - 1] < dp1[dp[i][j - 1]][j - 1]dp1[i][j1]<dp1[dp[i][j1]][j1]

    即最大边权在 节点 iii 的第 2j−12^{j - 1}2j1 个祖先到其的第 2j−12^{j - 1}2j1 个祖先的路径 上时,则次大边权不能选取 节点 iii 的第 2j−12^{j - 1}2j1 个祖先到其的第 2j−12^{j - 1}2j1 个祖先的路径 上的最大值,则选取 节点 iii 的第 2j−12^{j - 1}2j1 个祖先到其的第 2j−12^{j - 1}2j1 个祖先的路径 上的次大值 以及 节点 iii 到其的第 2j−12^{j - 1}2j1 个祖先的路径 上的最大边权 的最大值即可;

    状态转移方程如下,
    dp2[i][j]=max⁡{dp1[i][j−1],dp2[dp[i][j−1]][j−1]} dp2[i][j] = \max \{ dp1[i][j - 1], dp2[dp[i][j - 1]][j - 1] \} dp2[i][j]=max{dp1[i][j1],dp2[dp[i][j1]][j1]}

  3. dp1[dp[i][j−1]][j−1]==dp1[i][j−1]dp1[dp[i][j - 1]][j - 1] == dp1[i][j - 1]dp1[dp[i][j1]][j1]==dp1[i][j1]

    即最大边权在 节点 iii 到其的第 2j−12^{j - 1}2j1 个祖先的路径 与 节点 iii 的第 2j−12^{j - 1}2j1 个祖先到其的第 2j−12^{j - 1}2j1 个祖先的路径 上时,则选取这两条路径上的严格次小边权最大值即可;

    状态转移方程如下,
    dp2[i][j]=max⁡{dp2[i][j−1],dp2[dp[i][j−1]][j−1]} dp2[i][j] = \max \{ dp2[i][j - 1], dp2[dp[i][j - 1]][j - 1] \} dp2[i][j]=max{dp2[i][j1],dp2[dp[i][j1]][j1]}

综上,有
dp2[i][j]={max⁡{dp2[i][j−1],dp1[dp[i][j−1]][j−1]}(dp1[i][j−1]>dp1[dp[i][j−1]][j−1])max⁡{dp1[i][j−1],dp2[dp[i][j−1]][j−1]}(dp1[i][j−1]<dp1[dp[i][j−1]][j−1])max⁡{dp2[i][j−1],dp2[dp[i][j−1]][j−1]}(dp1[dp[i][j−1]][j−1]==dp1[i][j−1]) dp2[i][j] = \left\{ \begin{matrix} \max \{ dp2[i][j - 1], dp1[dp[i][j - 1]][j - 1] \} (dp1[i][j - 1] > dp1[dp[i][j - 1]][j - 1]) \\ \max \{ dp1[i][j - 1], dp2[dp[i][j - 1]][j - 1] \} (dp1[i][j - 1] < dp1[dp[i][j - 1]][j - 1]) \\ \max \{ dp2[i][j - 1], dp2[dp[i][j - 1]][j - 1] \} (dp1[dp[i][j - 1]][j - 1] == dp1[i][j - 1]) \\ \end{matrix} \right. dp2[i][j]=max{dp2[i][j1],dp1[dp[i][j1]][j1]}(dp1[i][j1]>dp1[dp[i][j1]][j1])max{dp1[i][j1],dp2[dp[i][j1]][j1]}(dp1[i][j1]<dp1[dp[i][j1]][j1])max{dp2[i][j1],dp2[dp[i][j1]][j1]}(dp1[dp[i][j1]][j1]==dp1[i][j1])
对于每条非树边 (x,y,z)(x, y, z)(x,y,z) ,使用类似 LCA 的倍增算法,求出 x,yx, yx,yLCA(x,y)LCA(x, y)LCA(x,y) 的路径中的边权最大及次大值;

即用两个变量 tot1tot1tot1tot2tot2tot2 分被存储最大值与次大值;

x,yx, yx,y 节点向上走时,tot1tot1tot1dp1dp1dp1 数组的最大值, tot2tot2tot2 则取 dp2dp2dp2tot1tot1tot1 更新前的权值最大值即可;

代码
#include <cstdio>
#include <vector>
#include <algorithm>
#define MAXN 300005
#define MAXX 32
#define INF 1e16
using namespace std;
long long n, m, dep[MAXN], log1[MAXN], dp[MAXN][MAXX];
long long sum, ans = INF, father[MAXN], tot1, tot2, dp1[MAXN][MAXX], dp2[MAXN][MAXX];
bool flag[MAXN];
struct edge { // 存储边
	int x, y;
    long long z;
    bool f;
	bool operator < (const edge &a) const {
		return z < a.z;
	}
} e[MAXN];
struct edge1 { // 存储最小生成树
    int to;
    long long val;
};
vector <edge1> g[MAXN];
void firstset(int n) {
    for (int i = 1; i <= n; i++) {
        father[i] = i;
    }
    return;
}
int findset(int x) {
    if (father[x] == x) return x;
    return father[x] = findset(father[x]);
}
void push(int x, int y) {
    int a = findset(x), b = findset(y);
    if (a != b) {
        father[a] = b;
    }
    return;
}
void Kru() { // 最小生成树
    sort(1 + e, 1 + e + m);
    firstset(n);
    for (int i = 1; i <= m; i++) {
        int x = findset(e[i].x), y = findset(e[i].y);
        if (x != y) {
            push(e[i].x, e[i].y);
            g[e[i].x].push_back( edge1({e[i].y, e[i].z}) );
            g[e[i].y].push_back( edge1({e[i].x, e[i].z}) );
            sum += e[i].z;
            e[i].f = true; // 标记为树边
        }
    }
    return;
}
void logset() {
	log1[1] = 0;
	for (int i = 2; i <= MAXN; i++) {
        log1[i] = log1[i / 2] + 1;
    }
    return;
}
void dfs(int i) { // 预处理
    flag[i] = true;
    for (int t = 0; t < g[i].size(); t++) {
        int v = g[i][t].to;
        long long tot = g[i][t].val;
        if (!flag[v]) {
            dep[v] = dep[i] + 1;
            dp[v][0] = i, dp1[v][0] = tot, dp2[v][0] = -INF;
            for (int j = 1; j <= log1[dep[v]]; j++) {
                dp[v][j] = dp[dp[v][j - 1]][j - 1];
                dp1[v][j] = max(dp1[v][j - 1], dp1[dp[v][j - 1]][j - 1]);
                if (dp1[v][j - 1] > dp1[dp[v][j - 1]][j - 1]) {
                    dp2[v][j] = max(dp2[v][j - 1], dp1[dp[v][j - 1]][j - 1]);
                } else if (dp1[v][j - 1] < dp1[dp[v][j - 1]][j - 1]) {
                    dp2[v][j] = max(dp1[v][j - 1], dp2[dp[v][j - 1]][j - 1]);
                } else {
                    dp2[v][j] = max(dp2[v][j - 1], dp2[dp[v][j - 1]][j - 1]);
                }
            }
            dfs(v);
        }
    }
    return;
}
void update(long long x) { // 更新 tot1, tot2
    if (x > tot1) {
        tot2 = tot1, tot1 = x;
    } else if (x > tot2 && x != tot1) {
        tot2 = x;
    }
    return;
}
void get_data(int u, int v) {
    tot1 = -INF, tot2 = -INF;
    if (dep[u] < dep[v]) swap(u, v);
	while (dep[u] != dep[v]) {
        update(dp1[u][log1[dep[u] - dep[v]]]);
        update(dp2[u][log1[dep[u] - dep[v]]]);
		u = dp[u][log1[dep[u] - dep[v]]];
	}
	if (u == v) return;
	for (int i = log1[dep[u]]; i >= 0; i--) {
		if (dp[u][i] != dp[v][i]) {
            update(max(dp1[u][i], dp1[v][i]));
            update(max(dp2[u][i], dp2[v][i]));
			u = dp[u][i], v = dp[v][i];
		}
	}
    update(max(dp1[u][0], dp1[v][0]));
    update(max(dp2[u][0], dp2[v][0]));
	return;
}
int main() {
    scanf("%d %d", &n, &m);
    for (int i = 1; i <= m; i++) {
        scanf("%d %d %lld", &e[i].x, &e[i].y, &e[i].z);
        e[i].f = false;
    }
    Kru();
    logset();
    dfs(1);
    for (int i = 1; i <= m; i++) {
        if (!e[i].f) {
            get_data(e[i].x, e[i].y); // 替换树边
            if (tot1 < e[i].z) {
                ans = min(ans, sum - tot1 + e[i].z);
            } else if (tot1 == e[i].z) {
                ans = min(ans, sum - tot2 + e[i].z);
            }
        }
    }
    printf("%lld", ans);
    return 0; 
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值