最近公共祖先
公共祖先: 在一棵有根树上,若节点 \(F\) 是节点 \(x\) 的祖先,也是节点 \(y\) 的祖先,那么称 \(F\) 是 \(x\) 和 \(y\) 的公共祖先。
最近公共祖先(LCA): 在 \(x\) 和 \(y\) 的所有公共祖先中,深度最大的称为最近公共祖先,记为 \(LCA(x,y)\)。

LCA 显然有以下性质。
- 在所有公共祖先中,\(LCA(x,y)\) 到 \(x\) 和 \(y\) 的距离都是最短的。例如,在 \(e\) 和 \(g\) 的所有祖先中,\(c\) 距离更短。
- \(x\) 与 \(y\) 之间最短的路径经过 \(LCA(x,y)\)。例如,从 \(e\) 到 \(g\) 的最短路径经过 \(c\)。
- \(x\) 和 \(y\) 本身也可以是它们自己的公共祖先。若 \(y\) 是 \(x\) 的祖先,则有 \(LCA(x,y)=y\),如图中 \(d=lca(d,h)\)。
如何求 LCA?根据 LCA 的定义,很容易想到一个简单直接的方法:分别从 \(x\) 和 \(y\) 出发,一直向根节点走,第一次相遇的节点就是 \(LCA(x,y)\)。具体实现时,可以用标记法:首先从 \(x\) 出发一直向根节点走,沿路标记所有经过的祖先节点;把 \(x\) 的祖先标记完之后,然后再从 \(y\) 出发向根节点走,走到第一个被 \(x\) 标记的节点,就是 \(LCA(x,y)\)。标记法的时间复杂度较高,在有 \(n\) 个节点的树上求一次 \(LCA(x,y)\) 的时间复杂度为 \(O(n)\)。若有 \(m\) 次查询,总的时间复杂度为 \(O(mn)\),效率太低。
倍增法求 LCA
可以把标记法换一种方式实现,分为以下两个步骤。
- 先把 \(x\) 和 \(y\) 提到相同的深度。例如,\(x\) 比 \(y\) 深,就把 \(x\) 提到 \(y\) 的高度(既让 \(x\) 走到 \(y\) 的同一高度),如果发现 \(x\) 直接就跳到 \(y\) 的位置上了,那么就停止查找,否则继续下一步。
- 让 \(x\) 和 \(y\) 继续同步向上走,每走一步就判断是否相遇,相遇点就是 \(LCA(x,y)\) 停止。
上面的两个步骤,如果 \(x\) 和 \(y\) 都一步一步向上走,时间复杂度为 \(O(n)\)。如何改进?如果不是一步步走,而是跳着走,就能加快速度。如何跳?可以按 \(2\) 的倍数向上跳,即跳 \(1,2,4,8,\cdots\) 步,这就是倍增法,倍增法用“跳”的方法加快了上述两个步骤。
步骤 1
把 \(x\) 和 \(y\) 提到相同的深度。具体任务是:给定两个节点 \(x\) 和 \(y\),设 \(x\) 比 \(y\) 深,让 \(x\) “跳”到与 \(y\) 相同的深度。
因为已知条件是只知道每个节点的父节点,所以如果没有其他辅助条件,\(x\) 只能一步步向上走,没法“跳”。要实现“跳”的动作,必须提前计算出一些 \(x\) 的祖先节点,作为 \(x\) 的“跳板”。然而,应该提前计算出哪些祖先节点呢?如何通过这些预计算出的节点准确且高效地跳到一个任意给定的 \(y\) 的深度?这就是倍增法的精妙之处:预计算出每个节点的第 \(1,2,4,8,16,\cdots\) 个祖先,即按 \(2\) 倍增的那些祖先。
有了预计算出的这些祖先做跳板,能从 \(x\) 快速跳到任何一个给定的目标深度。以从 \(x\) 跳到它的第 \(27\) 个祖先为例:
- 从 \(x\) 跳 \(16\) 步,到达 \(x\) 的第 \(16\) 个祖先 \(fa_1\);
- 从 \(fa_1\) 跳 \(8\) 步,到达 \(fa_1\) 的第 \(8\) 个祖先 \(fa_2\);
- 从 \(fa_2\) 跳 \(2\) 步到达祖先 \(fa_3\);
- 从 \(fa_3\) 跳 \(1\) 步到达祖先 \(fa_4\)。
共跳了 \(16+8+2+1=27\) 步,这个方法利用了二进制的特征:任何一个数都可以由 \(2\) 的倍数相加得到。\(27\) 的二进制是 \(11011\),其中的 \(4\) 个 \(1\) 的权值就是 \(16,8,2,1\)。
显然,用倍增法从 \(x\) 跳到某个 \(y\) 的时间复杂度为 \(O(\log n)\)。
剩下的问题是如何快速预计算每个节点的“倍增”的祖先。定义 \(fa_{x,i}\) 为 \(x\) 的第 \(2^i\) 个祖先,有非常巧妙的递推关系:\(fa_{x,i}=fa_{fa_{x,i-1},i-1}\)。分两步理解:\(fa_{x,i-1}\) 从 \(x\) 起跳,先跳 \(2^{i-1}\) 步,记这个点为 \(z\);再从 \(z\) 跳 \(2^{i-1}\) 步,一共跳了 \(2^{i-1}+2^{i-1}=2^i\) 步。
特别地,\(fa_{x,0}\) 是 \(x\) 的第 \(2^0=1\) 个祖先,就是 \(x\) 的父节点。\(fa_{x,0}\) 是递推式的初始条件,从它开始递推出了所有的 \(fa_{x,i}\)。递推的计算量有多大?从任意节点 \(x\) 到根节点,最多只有 \(\log n\) 个祖先,所以只需要递推 \(O(\log n)\) 次。所以整个 \(fa\) 的计算时间复杂度为 \(O(n \log n)\)。
步骤 2
经过上一个步骤,\(x\) 和 \(y\) 现在位于同一深度,让它们同步向上跳,就能找到它们的公共祖先。\(x\) 和 \(y\) 的公共祖先有很多,LCA(x, y) 是距离 \(x\) 和 \(y\) 最近的那个,其他祖先都更远。
从一个节点跳到根节点,最多跳 \(\log n\) 次。现在从 \(x,y\) 出发,从最大的 \(i \approx \log n\) 开始,跳 \(2^i\) 步,分别跳到 \(fa_{x,i},fa_{y,i}\),它们位于非常靠近根节点的位置(\(2^i \approx n\)),有以下两种情况:
- \(fa_{x,i}=fa_{y,i}\),这是一个公共祖先,它的深度小于或等于 LCA(x, y),这说明跳过头了,退回去换一个小的 \(i-1\) 重新跳一次。
- \(fa_{x,i} \ne fa_{y,i}\),说明还没跳到公共祖先,那么更新 \(x \leftarrow fa_{x,i}, y \leftarrow fa_{y,i}\),从新的起点 \(x,y\) 继续开始跳。由于新的 \(x,y\) 的深度比原来位置的深度减少超过一半,再跳时就不用跳 \(2^i\) 步,跳 \(2^{i-1}\) 步就够了。
以上两种情况,分别是比 LCA(x, y) 浅和深的两种位置。用 \(i\) 循环判断以上两种情况,就是从深浅两侧逐渐逼近 LCA(x, y)。每循环一次,\(i\) 减一,当 \(i\) 减为 \(0\) 时,\(x\) 和 \(y\) 正好位于 LCA(x,y) 的下一层,则 \(fa_{x,0}\) 就是 LCA(x,y)。
查询一次 LCA 的时间复杂度是多少?这里的 \(i\) 会从 \(\log n\) 递减到 \(0\),循环 \(O(\log n)\) 次。
倍增法的总计算量包括预计算 \(fa\) 和查询 \(m\) 次 LCA,总时间复杂度为 \(O(n \log n + m \log n)\)。
例题:P3379 【模板】最近公共祖先(LCA)
参考代码
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 500005;
const int LOG = 19;
vector<int> tree[N];
int depth[N], fa[N][LOG];
void dfs(int cur, int pre) {
depth[cur] = depth[pre] + 1; // 深度比父节点深度多1
for (int nxt : tree[cur]) { // 遍历所有邻居节点
if (nxt != pre) { // 除了父节点以外都是子节点
fa[nxt][0] = cur; // 记录父节点fa[][0]
dfs(nxt, cur);
}
}
}
int lca(int x, int y) {
if (depth[x] < depth[y]) swap(x, y); // 保证x深度更大
// 将x和y提到相同高度
int delta = depth[x] - depth[y];
for (int i = LOG - 1; i >= 0; i--)
if (delta & (1 << i)) x = fa[x][i];
if (x == y) return x; // 如果提到相同深度后已经重合,则直接返回
// x和y同步往上跳
for (int i = LOG - 1; i >= 0; i--)
if (fa[x][i] != fa[y][i]) { // 如果祖先相等,说明跳过头了,换一个小的i继续尝试
x = fa[x][i]; y = fa[y][i]; // 如果祖先不相等,就更新x和y继续跳
}
// 最终x和y位于LCA的下一层,此时x或y的父节点就是LCA
return fa[x][0];
}
int main()
{
int n, m, s; scanf("%d%d%d", &n, &m, &s);
for (int i = 1; i < n; i++) {
int x, y; scanf("%d%d", &x, &y);
// 建树
tree[x].push_back(y); tree[y].push_back(x);
}
dfs(s, 0); // 预处理深度等信息
for (int i = 1; i < LOG; i++)
for (int j = 1; j <= n; j++)
fa[j][i] = fa[fa[j][i - 1]][i - 1]; // 从fa[][0]开始递推
while (m--) {
int a, b; scanf("%d%d", &a, &b); printf("%d\n", lca(a, b));
}
return 0;
}
常见的应用思想:
- 对于 \((x,y)\) 间的路径,拆成 \(x\) 到 \(LCA\) 和 \(y\) 到 \(LCA\) 分别倍增。
- 对于 \((x,y)\) 间的路径,拆成 \(root\) 到 \(x\) 和 \(root\) 到 \(y\),去掉 \(root\) 到 \(LCA\)。
例题:P1967 [NOIP2013 提高组] 货车运输
分析:假设询问的点是 \(u\) 和 \(v\)。
如果只有一次询问,可以按边权从大到小加边,直至 \((u,v)\) 连通,那么拓展到多次询问,每次的路径一定在原图的最大生成树上。如果两个点在不同的树上就是无解,这个可以用并查集判断。
现在问题就放到树上了。如果把路径拆成 \((root, u)\) 和 \((root, v)\) 再去掉 \((root, lca)\) 会发现,对于取最小值的操作无法做去掉的那一部分计算,因此就只能把路径拆成 \((u, lca)\) 和 \((v, lca)\) 了。
在倍增求祖先时同时维护这段路径上的边权最小值,进行询问的计算时,在找 \(LCA\) 的过程中找到目前边权的最小值,别忘了最后还要考虑 \((x, fa_{x,0})\) 和 \((y, fa_{y,0})\) 这两条边的边权。
参考代码
#include <cstdio>
#include <utility>
#include <algorithm>
#include <vector>
using std::sort;
using std::min;
using std::swap;
using std::pair;
using std::vector;
using g_edge = pair<int, pair<int, int>>; // (边权,(x,y))
using t_edge = pair<int, int>; // (点,边权)
const int N = 10005;
const int M = 50005;
const int LOG = 14;
const int INF = 100000;
g_edge e[M];
vector<t_edge> tree[N];
bool vis[N];
int d[N], f[N][LOG], minw[N][LOG];
struct DSU {
int fa[N];
void init(int n) {
for (int i = 1; i <= n; i++) fa[i] = i;
}
int query(int x) {
return fa[x] == x ? x : fa[x] = query(fa[x]);
}
bool merge(int x, int y) {
int qx = query(x), qy = query(y);
if (qx != qy) fa[qx] = qy;
return qx != qy;
}
};
DSU dsu;
void dfs(int u, int fa) {
vis[u] = true;
for (t_edge e : tree[u]) {
int v = e.first, w = e.second;
if (v == fa) continue;
d[v] = d[u] + 1; f[v][0] = u; minw[v][0] = w;
dfs(v, u);
}
}
int lca(int x, int y) { // 这个lca函数求的是路径上的最小边权
if (d[x] < d[y]) swap(x, y);
int delta = d[x] - d[y];
int res = INF;
for (int i = LOG - 1; i >= 0; i--)
if (delta & (1 << i)) {
res = min(res, minw[x][i]);
x = f[x][i];
}
if (x == y) return res;
for (int i = LOG - 1; i >= 0; i--) {
if (f[x][i] != f[y][i]) {
res = min(res, min(minw[x][i], minw[y][i]));
x = f[x][i]; y = f[y][i];
}
}
// 别忘了最后两条边
res = min(res, min(minw[x][0], minw[y][0]));
return res;
}
int main()
{
int n, m; scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int x, y, z; scanf("%d%d%d", &x, &y, &z);
e[i] = {z, {x, y}};
}
// 构建最大生成树
sort(e + 1, e + m + 1);
dsu.init(n);
for (int i = m; i >= 1; i--) {
int w = e[i].first;
int x = e[i].second.first, y = e[i].second.second;
if (dsu.merge(x, y)) {
tree[x].push_back({y, w});
tree[y].push_back({x, w});
}
}
// 倍增预处理
for (int i = 1; i <= n; i++) if (!vis[i]) dfs(i, 0);
for (int j = 1; j < LOG; j++) {
for (int i = 1; i <= n; i++) {
f[i][j] = f[f[i][j - 1]][j - 1];
minw[i][j] = min(minw[i][j - 1], minw[f[i][j - 1]][j - 1]);
}
}
int q; scanf("%d", &q);
for (int i = 1; i <= q; i++) {
int x, y; scanf("%d%d", &x, &y);
printf("%d\n", dsu.query(x) != dsu.query(y) ? -1 : lca(x, y));
}
return 0;
}
习题:P12393 「RiOI-6」flos
解题思路
以下称远离根为向下,接近根为向上,记 \(f_u\) 表示 \(u\) 的父节点,\(d_u\) 为 \(u\) 的深度。
当查询的 \((x,t)\) 中 \(x\) 为根节点或 \(t=0\) 时,答案是显然的。
设 \(h_u\) 表示从点 \(u\) 向下走能走到的最远距离,\(g_u\) 表示从 \(f_u\) 开始向下不经过 \(u\) 能走到的最远距离,则对于每个查询 \((x,t)\),问题相当于求 \(\max \limits_{u \in \text{anc}(x)} \{ d_x - d_{f_u} + \min(t,g_u) \}\),其中 \(\text{anc}(x)\) 指的是 \(x\) 的所有祖先节点。
\(g_u\) 怎么计算呢?由于 \(f_u\) 要避开 \(u\) 所在的那棵子树,可以将 \(h_u\) 拆分为最长路径 \(h_{u,1}\) 和(非严格)次长路径 \(h_{u,2}\),这样当 \(h_{f_u, 1}\) 正好等于 \(h_u + 1\) 时(最长路径正好是 \(u\) 的方向),\(g_u\) 即为 \(h_{f_u, 2}\)(即次长路径),否则为 \(h_{f_u, 1}\)。
如何加速 \(\max \limits_{u \in \text{anc}(x)} \{ d_x - d_{f_u} + \min(t,g_u) \}\) 这个式子的计算?注意到里面的 \(\min(t, g_u)\),考虑分类讨论:
- 若 \(g_u \lt t\),此时括号内的式子变成 \(d_x - d_{f_u} + g_u\),后两项只和 \(u\) 有关,可以预处理根节点到 \(u\) 的路径上的 \(g - d\) 的最大值。
- 若 \(g_u \ge t\),此时 \(d_{f_u}\) 越小越好,相当于找深度最小的 \(u\) 满足 \(g_u \ge t\),这可以根据 \(g_u\) 预处理出 \(\text{maxg}_u\) 表示从根节点到 \(u\) 的路径上最大的 \(g_u\) 值,利用其单调性结合树上倍增可以在 \(O(\log n)\) 时间复杂度内定位。并且出现这种情况时,定位到的这个深度最小的 \(u\) 的上方都是 \(g \lt t\) 的,同时考虑上一条中提到的 \(g-d\) 最大值的情况。
预处理加上查询的整体时间复杂度为 \(O((q+n)\log n)\)。
参考代码
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 2e5 + 5;
const int LOG = 18;
const int INF = 1e9;
// --- 全局变量 ---
vector<int> tr[N]; // 邻接表
int fa[N][LOG]; // fa[i][j] 表示节点 i 的第 2^j 个祖先
int dep[N]; // 节点深度
int h1[N], h2[N]; // h1[u], h2[u] 分别表示从 u 出发向其子树能走的最长和次长路径长度
int g[N]; // g[v] 表示从 v 的父节点出发,不经过 v 的最长路径长度
int maxg[N]; // maxg[v] 表示从根到 v 的路径上,所有祖先的 g 值的最大值
int val[N]; // val[v] 表示从根到 v 的路径上,所有祖先的 g-dep 的最大值
/**
* @brief 第一次DFS,自底向上计算深度、父节点、最长/次长向下路径
*/
void dfs1(int u, int from, int depth) {
dep[u] = depth;
for (int v : tr[u]) {
if (v == from) continue;
dfs1(v, u, depth + 1);
fa[v][0] = u;
// 更新最长和次长向下路径
if (h1[v] + 1 > h1[u]) {
h2[u] = h1[u];
h1[u] = h1[v] + 1;
} else if (h1[v] + 1 > h2[u]) {
h2[u] = h1[v] + 1;
}
}
}
/**
* @brief 第二次DFS,自顶向下计算 g, maxg, val 数组,传递“向上”的路径信息
*/
void dfs2(int u, int from) {
for (int v : tr[u]) {
if (v == from) continue;
// 计算 g[v]: 从父节点u出发,不走v方向的最长路径
if (h1[v] + 1 == h1[u]) { // 如果v在u的最长路径上
g[v] = h2[u]; // 则取u的次长路径
} else {
g[v] = h1[u]; // 否则取u的最长路径
}
// maxg[v] = 从根到v的所有祖先的g值的最大值
maxg[v] = max(maxg[u], g[v]);
// val[v] = 从根到v的所有祖先的 g-dep 的最大值
val[v] = max(val[u], g[v] - dep[u]);
dfs2(v, u);
}
}
int main()
{
int n, q, d;
scanf("%d%d%d", &n, &q, &d);
for (int i = 1; i <= n; i++) {
val[i] = -INF; // 初始化为极小值
}
for (int i = 1; i < n; i++) {
int u, v;
scanf("%d%d", &u, &v);
tr[u].push_back(v);
tr[v].push_back(u);
}
// --- 预处理 ---
dfs1(1, 0, 1);
// 建立倍增数组
for (int j = 1; j < LOG; j++) {
for (int i = 1; i <= n; i++) {
fa[i][j] = fa[fa[i][j - 1]][j - 1];
}
}
dfs2(1, 0);
int ans = 0;
while (q--) {
int x, t;
scanf("%d%d", &x, &t);
if (d > 0) { // 在线查询
x ^= ans; t ^= ans;
}
if (x == 1) { // 特判:起点为根
ans = min(h1[1], t);
} else if (t == 0) { // 特判:时间为0
ans = dep[x] - 1;
} else {
// 基础答案:考虑向下走,或直接走到根
ans = max(min(h1[x], t), dep[x] - 1);
// 情况A: 时间t充裕,大于任何祖先分岔路径长度
if (maxg[x] < t) {
// 答案为 max(向下走, 向上走且时间不受限)
ans = max(ans, dep[x] + val[x]);
}
// 情况B: 时间t是限制因素
else {
// 考虑从父节点分岔的情况
ans = max(ans, 1 + min(t, g[x]));
// 用倍增找到最高的祖先y,其分岔路径长度>=t
int u = x;
for (int i = LOG - 1; i >= 0; i--) {
if (maxg[fa[u][i]] >= t) {
u = fa[u][i];
}
}
int y = fa[u][0]; // y就是分界点
// 综合考虑走到y之上(时间不受限)和走到y(时间受限)的情况
ans = max(ans, dep[x] + max(val[y], t - dep[y]));
}
}
printf("%d\n", ans);
}
return 0;
}
例题:P4180 [BJWC2010] 严格次小生成树
设一张图的最小生成树边权之和为 \(S\),则该图的严格次小生成树定义为该图所有边权之和大于 \(S\) 的生成树中边权之和最小者(可能不存在,也可能存在多棵)。
现给出一张 \(n\) 个点,\(m\) 条边的无向图,边权为 \(w_i\),求出该图的严格次小生成树边权之和。数据保证原图存在严格次小生成树。
数据范围:\(n \le 10^5, m \le 3 \times 10^5, 0 \le w_i \le 10^9\)。
分析:一种简单的思路是尝试找到原图的所有生成树,然后通过比较得出答案。但由于生成树数量过多,这样的算法显然效率很低。
由于严格次小生成树的边权和仅大于最小生成树边权和,因此可以猜测,严格次小生成树很可能就是在最小生成树上替换一条或几条边得到。事实上,可以证明,一定存在一棵严格次小生成树,使得它与某棵最小生成树仅有一条边的差距。
考虑一条不在原来的最小生成树上的边,如果把它加入最小生成树后会形成一个环,显然这个环上其他边的边权都小于等于刚加的这条边的边权(不然一开始的最小生成树就不成立了)。更进一步,这里可以把等于的情况去掉,因为如果存在等于的情况,说明是另一棵边权和相等但树的形态不同的最小生成树。所以如果严格次小生成树和最小生成树之间有两条以上的边不同,那么我们可以把这些不同的边中的其中一条改为在最小生成树上的边,剩下的不变,则此时得到的生成树边权和变小了,但还是比最小生成树的边权和要大。由此得知,最多只选 \(1\) 条边做替换。
有了这个性质,就可以考虑在建完最小生成树之后寻找那条不属于最小生成树,但属于严格次小生成树的边。枚举每一条非树边,在加入这条边之后,生成树上出现了一个环,再断掉环中其他边(环中其他的边实际上就是这条非树边的两点在最小生成树中的路径)里面边权最大的边(若该边边权与环内其他边权最大者相等,则断掉边权中严格次大的,注意有可能不存在这样的严格次大边),那么就得到了包含这条边的生成树中权值最小的。将所有这样的生成树权值和取 \(\min\) 后,就可以得到最终的答案。
可以采用树上倍增的方法,定义 \(1\) 为根,并存储每个点向上 \(2^i\) 条边的最大值与严格次大值。在寻找时,通过倍增取出这些最大值与严格次大值并依次进行更新,就可以得到需要断的边的权值。注意在存储和寻找严格次大值时的分类讨论条件。
整个算法的时间复杂度为 \(O(n \log n + m \log n)\)。
#include <cstdio>
#include <algorithm>
#include <vector>
using std::sort;
using std::swap;
using std::min;
using std::max;
using std::vector;
typedef long long LL;
const int N = 1e5 + 5;
const int M = 3e5 + 5;
const int LOG = 17;
const LL INF = 1e15;
struct Edge {
int x, y, z;
};
Edge edges[M];
bool mst[M]; // 记录每条边是否是最小生成树上的边
vector<Edge> tree[N];
// root用于并查集
// depth存储节点深度
// fa/w1/w2[u][i]代表节点u向上2的i次方层祖先/边权最大值/边权次大值
int root[N], depth[N], fa[N][LOG], w1[N][LOG], w2[N][LOG];
int query(int x) {
return root[x] == x ? x : root[x] = query(root[x]);
}
// update函数实现对两组最大、次大值合并得到新的最大、次大值
void update(int& mx1, int& mx2, int a1, int a2, int b1, int b2) {
mx1 = max(a1, b1);
mx2 = a1 == b1 ? max(a2, b2) : max(min(a1, b1), max(a2, b2));
}
void dfs(int u, int pre) {
depth[u] = depth[pre] + 1;
fa[u][0] = pre;
for (Edge e : tree[u]) {
int v = e.y, w = e.z;
if (v == pre) continue;
w1[v][0] = w;
dfs(v, u);
}
}
LL lca(int x, int y, int w, LL sum) {
// 在倍增法求lca的过程中实现最大边权和次大边权的计算
if (depth[x] < depth[y]) swap(x, y);
int delta = depth[x] - depth[y];
int res1 = 0, res2 = 0; // 最大、严格次大
for (int i = LOG - 1; i >= 0; i--)
if (delta & (1 << i)) {
update(res1, res2, res1, res2, w1[x][i], w2[x][i]);
x = fa[x][i];
}
if (x == y) {
// 有可能加的非树边与环内其他最长边相等
// 也有可能环内不存在次长边
if (res1 == w) return res2 == 0 ? INF : sum + w - res2;
else return res1 == 0 ? INF : sum + w - res1;
}
int tmp1 = 0, tmp2 = 0;
for (int i = LOG - 1; i >= 0; i--)
if (fa[x][i] != fa[y][i]) {
update(tmp1, tmp2, w1[x][i], w2[x][i], w1[y][i], w2[y][i]);
update(res1, res2, res1, res2, tmp1, tmp2);
x = fa[x][i]; y = fa[y][i];
}
update(tmp1, tmp2, w1[x][0], w2[x][0], w1[y][0], w2[y][0]);
update(res1, res2, res1, res2, tmp1, tmp2);
// 有可能加的非树边与环内其他最长边相等
// 也有可能环内不存在次长边
if (res1 == w) return res2 == 0 ? INF : sum + w - res2;
else return res1 == 0 ? INF : sum + w - res1;
}
int main()
{
int n, m; scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) root[i] = i;
for (int i = 1; i <= m; i++) {
scanf("%d%d%d", &edges[i].x, &edges[i].y, &edges[i].z);
}
// 先求一棵最小生成树
sort(edges + 1, edges + m + 1, [](Edge& e1, Edge& e2) {
return e1.z < e2.z;
});
LL sum = 0;
for (int i = 1; i <= m; i++) {
int x = edges[i].x, y = edges[i].y, z = edges[i].z;
int qx = query(x), qy = query(y);
if (qx != qy) {
root[qx] = qy; sum += z; mst[i] = true;
tree[x].push_back({x, y, z});
tree[y].push_back({y, x, z});
}
}
dfs(1, 0);
for (int i = 1; i < LOG; i++)
for (int j = 1; j <= n; j++) {
// 预处理倍增表
fa[j][i] = fa[fa[j][i - 1]][i - 1];
int a1 = w1[j][i - 1], a2 = w2[j][i - 1];
int b1 = w1[fa[j][i - 1]][i - 1], b2 = w2[fa[j][i - 1]][i - 1];
update(w1[j][i], w2[j][i], a1, a2, b1, b2);
}
LL ans = INF;
for (int i = 1; i <= m; i++) {
int x = edges[i].x, y = edges[i].y, z = edges[i].z;
if (x == y) continue;
if (!mst[i]) ans = min(ans, lca(x, y, z, sum));
}
printf("%lld\n", ans);
return 0;
}

浙公网安备 33010602011771号