You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

163 lines
6.3 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 100010, M = 300010;
const int INF = 0x3f3f3f3f;
typedef pair<int, int> PII;
int f[N][16]; // f[i][k]表示树上的某节点i向上走2^k步到达的节点
PII d[N][16]; // d[i][k]表示树上的某节点i向上走2^k步到达的节点最长距离和次长距离
int depth[N]; // 深度数组
// Kruskal用的结构体
struct Edge {
int a, b, c; // 从a到b边权为c
bool flag; // 是不是最小生成树的树边
const bool operator<(const Edge &ed) const {
return c < ed.c;
}
} edge[M];
// 邻接表
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
// 并查集
int p[N];
int find(int x) {
if (x == p[x]) return x;
return p[x] = find(p[x]);
}
// 树上倍增求任意两点最短距离
int bfs(int root) {
queue<int> q;
q.push(root);
depth[root] = 1;
while (q.size()) {
int u = q.front();
q.pop();
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (!depth[j]) {
q.push(j);
depth[j] = depth[u] + 1; // 记录深度
f[j][0] = u; // 记录2^0->t,描述父节点
// 下面是与普通的倍增不一样的代码,记录两点间最大长度与次大长度
d[j][0] = {w[i], 0}; // j->t 的最大距离与次大距离
for (int k = 1; k <= 15; k++) { // 倍增
int v = f[j][k - 1]; // 设j跳2 ^(k-1)到达的是v点
f[j][k] = f[v][k - 1]; // v点跳 2^(k-1)到达的终点就是j跳2^k的终点
// ①最大边权一定是两个线段中最长边权的最大值
d[j][k].first = max(d[j][k - 1].first, d[v][k - 1].first);
// ②次大边权分情况讨论
// 如果前半段的最大值 小于 后半段的最大值
// 次大值 等于 max(前半段最大值,后半段次大值)
if (d[j][k - 1].first == d[v][k - 1].first)
// 次大值 等于 max(前半段次大值,后半段次大值)
d[j][k].second = max(d[j][k - 1].second, d[v][k - 1].second);
// 如果前半段的最大值 等于 后半段的最大值
else if (d[j][k - 1].first < d[v][k - 1].first)
d[j][k].second = max(d[j][k - 1].first, d[v][k - 1].second);
// 如果前半段的最大值 大于 后半段的最大值
else // 次大值 等于 max(前半段次大值,后半段的最大值)
d[j][k].second = max(d[j][k - 1].second, d[v][k - 1].first);
}
}
}
}
}
// 因为同时需要同步修改最大值和次大值,所以采用了地址符&引用方式定义参数
// m1:最大值m2次大值
void cmp(int &m1, int &m2, PII x) {
if (m1 == x.first)
m2 = max(m2, x.second);
else if (m1 < x.first)
m2 = max(m1, x.second), m1 = x.first;
else
m2 = max(m2, x.first);
}
// 最近公共祖先
// 由a->b的边,边权是w
// 返回值如果加上这条边w去掉最小生成树中的某条边(m1或m2),得到一个待选的次小生成树
// 此时的 w- m1 或者 w-m2的值是多少。
// 具体是-m1,还是-m2要区别对待因为如果w=m1,就是-m2,否则就是-m1
// 利用倍增的思想对bfs已经打好的表 d数组和f数组 进行快速查询
// 找出a->b之间的最大距离和次大距离
int lca(int a, int b, int w) {
if (depth[a] < depth[b]) swap(a, b); // 保证a的深度大于b的深度
int m1 = -1e18, m2 = -1e18; // 最大边,次大边初始化
for (int k = 15; k >= 0; k--) // 由小到大尝试
if (depth[f[a][k]] >= depth[b]) { // 让a向上跳2^k步
cmp(m1, m2, d[a][k]); // a向上跳2^k步时走过的路径中可能存在最大边或次大边
a = f[a][k]; // 标准的lca
}
// 当a与b不是同一个点时此时两者必须是depth一样的情况同时向上查询2^k必然可以找到LCA
if (a != b) {
for (int k = 15; k >= 0; k--)
if (f[a][k] != f[b][k]) {
cmp(m1, m2, d[a][k]); // a向上跳2^k步时走过的路径中可能存在最大边或次大边
cmp(m1, m2, d[b][k]); // b向上跳2^k步时走过的路径中可能存在最大边或次大边
a = f[a][k], b = f[b][k];
}
// 此时a和b到lca下同一层 所以还要各跳1步=跳2^0步
// 联想一下在普通版本LCA中的最终返回值就明白了
// return f[a][0];
cmp(m1, m2, d[a][0]);
cmp(m1, m2, d[b][0]);
}
return w == m1 ? w - m2 : w - m1;
}
int main() {
int n, m, a, b, c;
scanf("%d %d", &n, &m);
// 并查集初始化
for (int i = 1; i <= n; i++) p[i] = i;
// 邻接表初始化
memset(h, -1, sizeof h);
// Kruskal
for (int i = 0; i < m; i++) {
scanf("%d %d %d", &a, &b, &c);
edge[i] = {a, b, c, false};
}
// 按边权排序+最小生成树
sort(edge, edge + m);
LL sum = 0, ans = 1e18;
for (int i = 0; i < m; i++) {
a = find(edge[i].a), b = find(edge[i].b), c = edge[i].c;
if (a != b) {
p[a] = b;
sum += c; // 最小生成树的边权总和
edge[i].flag = true; // 标识为最小生成树中的边
// 将最小生成树中的树边单独构建一个图出来
add(edge[i].a, edge[i].b, c), add(edge[i].b, edge[i].a, c);
}
}
// 倍增预处理,记录任意点向上2^k步的最大值次大值深度等信息后面lca会用到
bfs(1);
// 用非树边去尝试替换最小生成树中的边然后取min
// lca查表
for (int i = 0; i < m; i++)
if (!edge[i].flag) { // 枚举非树边
a = edge[i].a, b = edge[i].b, c = edge[i].c;
ans = min(ans, sum + lca(a, b, c));
}
// 输出
printf("%lld\n", ans);
return 0;
}