问题引入
解题思想
对于一张无向图,如果存在最小生成树和次小生成树,那么对于任何一棵最小生成树,都存在一棵(严格)次小生成树,是恶的这两棵树仅有一条边不同
这一条边,就是由某条非树边,在其构成的环路中,替换其中边权最大(或次大)的一条边(替换边权最大使得树的边权和变化最小)
按照这种规则能够获得一些树,次小生成树一定是其中某一个,通过变量维护最小值即可
上述流程中的难点在于如何快速找到任意两点间最大或次大的边权。针对这个问题,有以下两种解决方案
深度优先搜索
特点
仅适用于数据较小的情况
流程
从每一个点出发开始进行dfs,找到与其它点的路径之间的最大(次大)边权
代码实现
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
using LL = long long;
const int N = 510, M = 1e4 + 10;
struct Edge {
int a, b, w;
bool f;
bool operator<(const Edge &t) const
{
return w < t.w;
}
}edges[M];
int n, m;
int p[N];
int dis1[N][N], dis2[N][N]; // 任意两点间距离的最大值和次大值
int h[N], e[N * 2], ne[N * 2], w[N * 2], idx;
inline void add(int a, int b, int c)
{
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx ++;
}
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
/**
* 我觉得获得任意两点间最大边权是代码实现中最难的一部分
* x:函数调用时的点i已经走到了x点
* fa:此前走过的点,避免再次折返回去,否则会陷入无限调用
* maxd1:走到x点时维护的最大边权值
* maxd2:走到x点时维护的次大边权值
* d1[]:函数调用时的点i到达各个点的最大边权值
* d2[]:函数调用时的点i到达各个点的次大边权值
*/
void dfs(int x, int fa, int maxd1, int maxd2, int d1[], int d2[])
{
d1[x] = maxd1; d2[x] = maxd2;
for (int i = h[x]; ~i; i = ne[i])
{
int j = e[i];
if (j != fa)
{
int td1 = maxd1, td2 = maxd2;
if (w[i] > maxd1)
{
td2 = maxd1;
td1 = w[i];
}
else if (w[i] < maxd1 && w[i] > maxd2) td2 = w[i];
dfs(j, x, td1, td2, d1, d2); // 往后继续走,继续更新x点能够走到的点的距离
}
}
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h); // 只要是用邻接表存储一定要记得这个
for (int i = 0; i < m; ++ i)
{
int a, b, w;
cin >> a >> b >> w;
edges[i] = {a, b, w};
}
// 构建最小生成树
sort(edges, edges + m);
for (int i = 1; i <= n; ++ i) p[i] = i;
LL sum = 0;
for (int i = 0; i < m; ++ i)
{
// 如果写a=find(edges[i].a),a就变成了最上层的父节点,与add使用的a,b,w意义不符了
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
int pa = find(a), pb = find(b);
if (pa !=pb)
{
p[pa] = pb;
sum += w;
edges[i].f = true;
add(a, b, w);
add(b, a, w);
}
}
// 通过dfs获取任意两点间经过边的最大和次大边权值
for (int i = 1; i <= n; ++ i) dfs(i, -1, -1e9, -1e9, dis1[i], dis2[i]);
// 对于每一条非树边,替换对应环路中边权的最大或次大获取对应树的边权和同时维护一个最小值
LL res = 1e15; // 由于是一棵树的边权和,最大就是500个点,499条边,每条边的长度都是1e9
for (int i = 0; i < m; ++ i)
if (!edges[i].f)
{
LL t;
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
if (w > dis1[a][b]) t = sum + w - dis1[a][b];
else if (w > dis2[a][b]) t = sum - dis2[a][b] + w;
res = min(res, t);
}
cout << res << endl;
return 0;
}
倍增LCA
流程
在预处理原有数据的前提下,借鉴(f[i][k])的思想,额外维护出(dis1[i][k])和(dis2[i][k]),分别表示从点(i)出发,走过(2^k)步长度的路径中的最大和次大边权(同多源最短路倍增LCA解法1相同)
代码实现
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <vector>
using namespace std;
using LL = long long;
const int N = 510, M = 1e4 + 10, INF = 0x3f3f3f3f;
struct Edge {
int a, b, w;
bool used;
bool operator< (const Edge &t) const
{
return w < t.w;
}
}edges[M];
int n, m;
int h[N], e[N * 2], ne[N * 2], w[N * 2], idx;
int p[N];
int depth[N], f[N][9], dis1[N][9], dis2[N][9];
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
void add(int a, int b, int c)
{
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
LL kruskal()
{
LL sum = 0;
sort(edges, edges + m);
for (int i = 1; i <= n; ++ i) p[i] = i;
for (int i = 0; i < m; ++ i)
{
int a = find(edges[i].a), b = find(edges[i].b), w = edges[i].w;
if (a != b)
{
p[a] = b;
sum += w;
edges[i].used = true;
}
}
return sum;
}
void build()
{
memset(h, -1, sizeof h);
for (int i = 0; i < m; ++ i)
if (edges[i].used)
{
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
add(a, b, w), add(b, a, w);
}
}
void bfs()
{
queue<int> q;
q.push(1);
// 先memset再赋值
memset(depth, 0x3f, sizeof depth);
depth[0] = 0, depth[1] = 1;
while (q.size())
{
int t = q.front();
q.pop();
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (depth[j] == INF)
{
q.push(j);
f[j][0] = t;
depth[j] = depth[t] + 1;
dis1[j][0] = w[i], dis2[j][0] = -INF;
for (int k = 1; k <= 8; ++ k)
{
f[j][k] = f[f[j][k - 1]][k - 1];
dis1[j][k] = dis2[j][k] = -INF;
int dis[] = {dis1[j][k - 1], dis1[f[j][k - 1]][k - 1], dis2[j][k - 1], dis2[f[j][k - 1]][k - 1]};
for (int u = 0; u < 4; ++ u)
{
if (dis[u] > dis1[j][k]) dis2[j][k] = dis1[j][k], dis1[j][k] = dis[u];
else if (dis[u] != dis1[j][k] && dis[u] > dis2[j][k]) dis2[j][k] = dis[u];
}
}
}
}
}
}
int lca(int a, int b, int w)
{
int cnt = 0;
int dis[N * 34];
if (depth[a] < depth[b]) swap(a, b);
for (int k = 8; ~k; -- k)
if (depth[f[a][k]] >= depth[b])
{
dis[cnt ++] = dis1[a][k];
dis[cnt ++] = dis2[a][k];
a = f[a][k];
}
if (a != b)
{
for (int k = 8; ~k; -- k)
{
if (f[a][k] != f[b][k])
{
dis[cnt ++] = dis1[a][k];
dis[cnt ++] = dis2[a][k];
dis[cnt ++] = dis1[b][k];
dis[cnt ++] = dis2[b][k];
a = f[a][k];
b = f[b][k];
}
}
dis[cnt ++] = dis1[a][0];
dis[cnt ++] = dis1[b][0];
}
int d1 = -INF, d2 = -INF;
for (int i = 0; i < cnt; ++ i)
if (dis[i] > d1) d2 = d1, d1 = dis[i];
else if (dis[i] != d1 && dis[i] > d2) d2 = dis[i];
if (w > d1) return d1;
if (w > d2) return d2;
return INF;
}
int main()
{
cin >> n >> m;
for (int i = 0; i < m; ++ i)
{
int a, b, w;
cin >> a >> b >> w;
edges[i] = {a, b, w};
}
LL sum = kruskal();
build();
bfs();
LL res = 1e18;
for (int i = 0; i < m; ++ i)
if (!edges[i].used)
{
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
res = min(res, sum + w - lca(a, b, w));
}
cout << res << endl;
return 0;
}