题目链接
秘密的牛奶运输
题目描述
农夫约翰要把他的牛奶运输到各个销售点。
运输过程中,可以先把牛奶运输到一些销售点,再由这些销售点分别运输到其他销售点。
运输的总距离越小,运输的成本也就越低。
低成本的运输是农夫约翰所希望的。
不过,他并不想让他的竞争对手知道他具体的运输方案,所以他希望采用费用第二小的运输方案而不是最小的。
现在请你帮忙找到该运输方案。
注意:
- 如果两个方案至少有一条边不同,则我们认为是不同方案;
- 费用第二小的方案在数值上一定要严格大于费用最小的方案;
- 答案保证一定有解;
输入格式
第一行是两个整数 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
样例输入 #1
4 4
1 2 100
2 4 200
2 3 250
3 4 100
样例输出 #1
450
提示
【数据范围】
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算法求图中的最小生成树,边权之和 s u m sum sum;并标记每条边是否在最小生成树中;同时构建出最小生成树。
- 预处理最小生成树中任意两点之间代价最大的边的边权 d 1 [ a , b ] d1[a,b] d1[a,b]和代价次大的边的边权 d 2 [ a , b ] d2[a, b] d2[a,b],便于将来用非树边去替换。
- 依次枚举所有不在最小生成树中的边 a ↔ b a \leftrightarrow b a↔b,边权为 c c c。尝试用该边替换节点 a a a到节点 b b b的路径中的一条边,显然要选代价最大或者次大的那条边。
- 如果 c > d 1 [ a , b ] c > d1[a,b] c>d1[a,b],可以用该边替换 a a a到 b b b路径中代价最大的那条边,替换之后的总权值为 s u m − d 1 [ a , b ] + c sum-d1[a,b]+c sum−d1[a,b]+c
- 否则如果 c < d 1 [ a , b ] c<d1[a,b] c<d1[a,b]并且 c > d 2 [ a , b ] c >d2[a,b] c>d2[a,b],可以用该边替换 a a a到 b b b路径中代价次大的那条边,替换后的总价值为 s u m − d 2 [ a , b ] + c sum-d2[a,b]+c sum−d2[a,b]+c
- 求所有替换之后总权值的最小值,就是次小生成树。
时间复杂度
Kruskal算法的时间复杂度为 O ( m l o g m ) O(mlogm) O(mlogm),预处理最小生成树中任意两点之间代价最大和次大的边的时间复杂度为 O ( n 2 ) O(n^2) O(n2),
代码实现
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 505, M = 1e4 + 5;
struct E {int a, b, c;bool f; //表示是否在最小生成树中bool operator < (const E &e) const { return c < e.c; }
}edge[M];
int n, m;
int p[N], d1[N][N], d2[N][N];
int h[N], e[M], w[M], ne[M], idx;
int find(int x)
{if(x != p[x]) p[x] = find(p[x]);return p[x];
}
void add(int a, int b, int c) // 添加一条边a->b,边权为c
{e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}//预处理生成树中u点到其它点之间所有边中边权最大值d1[u][v]和次大值d2[u][v]
void dfs(int u, int fa, int maxd1, int maxd2, int d1[], int d2[])
{d1[u] = maxd1, d2[u] = maxd2;for(int i = h[u]; ~ i; i = ne[i]){int v = e[i];if(v != fa) //避免往回搜索 {int t1 = maxd1, t2 = maxd2;if(w[i] > t1) t2 = t1, t1 = w[i];else if(w[i] < t1 && w[i] > t2) t2 = w[i];dfs(v, u, t1, t2, d1, d2);}}
}
int main()
{cin >> n >> m;for(int i = 0; i < m; i ++) cin >> edge[i].a >> edge[i].b >> edge[i].c;memset(h, -1, sizeof h);//最小生成树sort(edge, edge + m);for(int i = 1; i <= n; i ++) p[i] = i;LL sum = 0;for(int i = 0; i < m; i ++){int a = edge[i].a, b = edge[i].b, c = edge[i].c;int pa = find(a), pb = find(b);if(pa != pb){p[pa] = pb;sum += c;add(a, b, c), add(b, a, c); //构建最小生成树edge[i].f = true;}}//处理最小生成树种任意两点间所有边中边权的最大值和次大值,注意最大值和次大值初始化尽可能小for(int i = 1; i <= n; i ++) dfs(i, -1, -1e9, -1e9, d1[i], d2[i]);LL ans = 1e18;//枚举所有不在最小生成树中的边for(int i = 0; i < m; i ++){if(!edge[i].f) {int a = edge[i].a, b = edge[i].b, c = edge[i].c;//尝试边i替换a-b的路径中最大的一条边LL t = 1e18;if(c > d1[a][b]) t = sum - d1[a][b] + c;else if(c > d2[a][b]) t = sum - d2[a][b] + c;ans = min(ans, t);}}cout << ans << endl;return 0;
}