题目地址:
https://www.acwing.com/problem/content/1150/
农夫约翰要把他的牛奶运输到各个销售点。运输过程中,可以先把牛奶运输到一些销售点,再由这些销售点分别运输到其他销售点。运输的总距离越小,运输的成本也就越低。低成本的运输是农夫约翰所希望的。不过,他并不想让他的竞争对手知道他具体的运输方案,所以他希望采用费用第二小的运输方案而不是最小的。现在请你帮忙找到该运输方案。注意:如果两个方案至少有一条边不同,则我们认为是不同方案;费用第二小的方案在数值上一定要严格小于费用最小的方案;答案保证一定有解。
输入格式:
第一行是两个整数
N
,
M
N,M
N,M,表示销售点数和交通线路数;接下来
M
M
M行每行
3
3
3个整数
x
,
y
,
z
x,y,z
x,y,z,表示销售点
x
x
x和销售点
y
y
y之间存在线路,长度为
z
z
z。
输出格式:
输出费用第二小的运输方案的运输总距离。
数据范围:
1
≤
N
≤
500
1≤N≤500
1≤N≤500
1
≤
M
≤
1
0
4
1≤M≤10^4
1≤M≤104
1
≤
z
≤
1
0
9
1≤z≤10^9
1≤z≤109
数据中可能包含重边。
即求严格次小生成树。有一个定理,任一次小生成树(无论是严格的还是不严格的)与某个最小生成树只差一条边(换句话说,最小生成树将某条边换成另一条非树边就可能成为次小生成树)。所以算法可以这样设计,我们先用Kruskal算法求出最小生成树,并标记哪些边是树边;接着求出任意两点之间的树路径的最长边和严格次长边的长度是多少(这个路径是唯一的,因为是在最小生成树上求。但是严格次长边确实有可能不存在,也许两点之间的路径上只有一条边,也许该路径上所有边长度都相同,但是由于题目保证有解,所以这不影响后面的算法,可以直接令次长边边长为 0 0 0即可),这可以用DFS暴力枚举求出;然后枚举每个非树边(这个边加入最小生成树一定能产生环),看看将其替换树上最长边能得到的树总边权是多少,以此更新答案(如果树上最长边和当前枚举的非树边长度一样,则替换路径上的次长边),由于题目保证有解,所以这样枚举肯定能枚举到那个解。代码如下:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, M = 1e4 + 10;
int n, m;
struct Edge {
int a, b, w;
// 标记当前边是否是树边
bool flag;
bool operator<(const Edge &e) const {
return w < e.w;
}
} edge[M];
int h[N], e[N * 2], ne[N * 2], w[N * 2], idx;
int dist1[N][N], dist2[N][N];
int p[N];
void add(int a, int b, int c) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int find(int x) {
if (x != p[x]) p[x] = find(p[x]);
return p[x];
}
// 若v是第一次dfs的时候的u,那么dfs是求出树上每个顶点到v之间的路径上的最大边和次大边边权
void dfs(int u, int fa, int max1, int max2, int d1[], int d2[]) {
d1[u] = max1;
d2[u] = max2;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
// 不走回头路
if (j == fa) continue;
int t1 = max1, t2 = max2;
if (w[i] > t1) t2 = t1, t1 = w[i];
else if (w[i] < t1 && w[i] > t2) t2 = w[i];
dfs(j, u, t1, t2, d1, d2);
}
}
int main() {
memset(h, -1, sizeof h);
scanf("%d%d", &n, &m);
for (int i = 0; i < m; i++)
scanf("%d%d%d", &edge[i].a, &edge[i].b, &edge[i].w);
sort(edge, edge + m);
for (int i = 1; i <= n; i++) p[i] = i;
// Kruskal算法求最小生成树,求出树的总边权,并且标记树边
long sum = 0;
for (int i = 0; i < m; i++) {
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
int pa = find(a), pb = find(b);
if (pa != pb) {
p[pa] = pb;
sum += w;
edge[i].flag = true;
add(a, b, w), add(b, a, w);
}
}
// 求出树上两个点之间的路径上的最长边和次长边的边权
for (int i = 1; i <= n; i++) dfs(i, -1, 0, 0, dist1[i], dist2[i]);
long res = 1e18;
// 枚举非树边,将其替换最长树边或者次长树边,看能得到的次小生成树总边权是多少,用其更新答案
for (int i = 0; i < m; i++)
if (!edge[i].flag) {
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
if (w > dist1[a][b]) res = min(res, sum + w - dist1[a][b]);
else if (w > dist2[a][b]) res = min(res, sum + w - dist2[a][b]);
}
printf("%ld\n", res);
return 0;
}
时间复杂度 O ( n 2 + m log m ) O(n^2+m\log m) O(n2+mlogm),空间 O ( n ) O(n) O(n), n n n是图的点数, m m m是边数。