农夫约翰要把他的牛奶运输到各个销售点。
运输过程中,可以先把牛奶运输到一些销售点,再由这些销售点分别运输到其他销售点。
运输的总距离越小,运输的成本也就越低。
低成本的运输是农夫约翰所希望的。
不过,他并不想让他的竞争对手知道他具体的运输方案,所以他希望采用费用第二小的运输方案而不是最小的。
现在请你帮忙找到该运输方案。
注意::
- 如果两个方案至少有一条边不同,则我们认为是不同方案;
- 费用第二小的方案在数值上一定要严格大于费用最小的方案;
- 答案保证一定有解;
输入格式
第一行是两个整数 N,M,表示销售点数和交通线路数;
接下来 MM 行每行 33 个整数 x,y,z,表示销售点 x 和销售点 y 之间存在线路,长度为 z。
输出格式
输出费用第二小的运输方案的运输总距离。
数据范围
1≤N≤500,
1≤M≤104,
1≤z≤109,
数据中可能包含重边。输入样例:
4 4 1 2 100 2 4 200 2 3 250 3 4 100
输出样例:
450
次小生成树定义:在图中的所有生成树中,权值之和第二小的树:
非严格次小生成数:权值第一小的生成树和第二小的生成树权值相同
严格次小生成数:权值第二小的生成树严格大于权值第一小的生成树
次小生成树的求法:
方法一:先求最小生成树,再枚举删除最小生成树的边,然后加上非最小生成树的边:
此方法只能求非严格次小生成树
方法二:先求最小生成树,然后依次枚举非树边,将非树边加入树中,同时树中去掉一条边,使得最终的图仍是一颗树:
此方法不仅可以求非严格次小生成树,也可以求严格次小生成树
所以我们着重介绍方法二
方法二解法:
设T为图G的一颗生成树,对于非树边a和树边b,插入边a和删除边b的操作记为( + a, - b).
如果T + a - b之后,仍是一颗生成树称(+ a , - b)是T的一个可行交换。
称由T进行一次可行变换所得到的新的生成树集合称为T的邻集。
定理:此小生成树一定在邻集中
解决步骤:
1:假设树中的权值总和为res则需要求(res + w - 树边的最大值/次大值)使得他最小,其中w表示非树边的值
2:为何要求最大与次大??因为怕树中的最大值与非树边相同,但是次大值肯定小于最大值
求最小生成树。
3:记录其中的树边,然后求出最小生成树中任意两个点a, b之间的路径的最大值与次大值,然后依次枚举每条非树边用于替换树边,找出替换后的大于最小生成树的权值的最小值即为严格次小生成树
方法二代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; typedef long long LL; const int N = 510, M = 10010; int n, m; int p[N]; int dist1[N][N], dist2[N][N];//dist1记录最小值,dist2记录次小值 int h[N], e[M], ne[M], w[M], idx;//邻接表记录的是树边 struct Edge { int a, b, w; bool is_tree; bool operator < (const Edge &W) const { return w < W.w; } }edges[M]; void add(int a, int b, int c)//邻接表 { e[idx] = b; w[idx] = c; ne[idx] = h[a]; h[a] = idx; idx ++ ; } int find(int x)//并查集 { if (p[x] != x) p[x] = find(p[x]); return p[x]; } 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 != -1; i = ne[i]) { int j = e[i]; if (j != fa) { int r1 = max1, r2 = max2; if (w[i] > r1)//若当前枚举的权值大于最大值 { r2 = r1;//将最大值置为次大值 r1 = w[i];//将w[i]置为最大值 } else if (w[i] > r2 && w[i] < r1) r2 = w[i];//若当前枚举的权值大于次大值 dfs(j, u, r1, r2, d1, d2);//枚举下一个点 } } } int main() { cin >> n >> m; for (int i = 1; i <= n; i ++ ) p[i] = i; memset(h, -1, sizeof h); for (int i = 0; i < m; i ++ ) { int a, b, c; scanf("%d %d %d", &a, &b, &c); edges[i] = {a, b, c}; } sort(edges, edges + m); LL sum = 0; for (int i = 0; i < m; i ++ )//求出最小生成树 { 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].is_tree = true;//将i加入树边 add(a, b, w), add(b, a, w); } } for (int i = 1; i <= n; i ++ ) dfs(i, -1, -1e9, -1e9, dist1[i], dist2[i]); //找出树边中从点i走到各个点的路径的最大值 LL res = 1e18; for (int i = 0; i < m; i ++ ) if (!edges[i].is_tree)//枚举非树边 { int a = edges[i].a, b = edges[i].b, w = edges[i].w; LL max1 = dist1[a][b], max2 = dist2[a][b];//记录树中最大值与次大值 if (w > max1) res = min(res, sum + w - max1);//若最大值可以被更新 else if (w > max2) res = min(res, sum + w - max2);//若最大值不可以更新但次大值可以更新 } cout << res << endl; return 0; }
方法三:时间复杂度O(m)
步骤:
1:先求出最小生成树,并记录树中的总权值sum。
2:预处理三个数组记录的是树边,fa[i][j], d1[i][j], d2[i][j]。fa[i][j]表示从点 i 跳2^j 步之后所跳到达的点为fa[i][j]。d1[i][j], d2[i][j]分别为从 i 点开始跳2 ^ j步之后所到达的点fa[i][j]中路径的最大值与次大值
1:先求出最小生成树。
3:枚举每条非树边a, b,权值为w。在最小生成树中找出 a 到 b 的最大的权值假设为d,找出sum + w - d他的值为大于sum的最小值即为严格最小生成树
方法三代码如下:
#include <queue> #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; typedef long long LL; const int N = 100010, M = 300010, INF = 0x3f3f3f3f; int n, m; int p[N]; int depth[N]; int fa[N][17]; int d1[N][17], d2[N][17]; int h[N], e[M], ne[M], w[M], idx; struct Edge { int a, b, w; bool used; bool operator < (const Edge &W) const { return w < W.w; } }edge[M]; void add(int a, int b, int c)//邻接表 { e[idx] = b; w[idx] = c; ne[idx] = h[a]; h[a] = idx; idx ++ ; } int find(int x)//并查集 { if (p[x] != x) p[x] = find(p[x]); return p[x]; } void bfs() { queue<int> q; memset(depth, 0x3f, sizeof depth); depth[0] = 0, depth[1] = 1;//设置边界,若不了解看推荐的博客 q.push(1);//这里以任意点为根节点都可以,我这里假设的是以1为根节点 while (q.size()) { int t = q.front(); q.pop(); for (int i = h[t]; i != -1; i = ne[i]) { int j = e[i]; if (depth[j] > depth[t] + 1)//若j还未被更新 { depth[j] = depth[t] + 1; fa[j][0] = t;//j往上跳2^0为点t q.push(j); d1[j][0] = w[i], d2[j][0] = -INF;//最大值为w[i],要求次大值没有初始化为-INF. for (int k = 1; k <= 16; k ++ ) { int anc = fa[j][k - 1]; fa[j][k] = fa[anc][k - 1]; int distance[4] = {d1[j][k - 1], d2[j][k - 1], d1[anc][k - 1], d2[anc][k - 1]}; //[j, j + 2 ^ k]步直接的最大值必在以上四个值中 d1[j][k] = -INF, d2[j][k] = -INF;//要求的是最大值所以初始化为负无穷 for (int u = 0; u < 4; u ++ ) { int d = distance[u]; if (d > d1[j][k])//若可以更新最大值 { d2[j][k] = d1[j][k]; d1[j][k] = d; } else if (d2[j][k] < d && d < d1[j][k]) d2[j][k] = d;//若可以更新次大值,注意要严格小于最大值 } } } } } } LL lca(int a, int b, int w) { if (a == b) return INF; if (depth[a] < depth[b]) swap(a, b);//保证a所在的位置比b深好进行下面操作 int cnt = 0; static int distance[N * 2];//要记录次大值与最大值所以大小为2 * N;用static是为了节省空间 for (int k = 16; k >= 0; k -- )//将a节点跳到与b同一层的深度 if (depth[fa[a][k]] >= depth[b])//若a跳2^k的深度大于b的深度则将a跳过去 { distance[cnt ++ ] = d1[a][k];//记录[a, a + 2 ^ k]路径之间的值 distance[cnt ++ ] = d2[a][k];//要先纪律顺序不能反否则a会被更新 a = fa[a][k];//将a跳到fa[a][k] } if (a != b)//说明他们跳到同一深度后还不是同一节点 { for (int k = 16; k >= 0; k -- )//将a, b跳到a, b的最近公共祖先的下一层 if (fa[a][k] != fa[b][k]) { distance[cnt ++ ] = d1[a][k];//记录a, b的最大与次大值 distance[cnt ++ ] = d2[a][k]; distance[cnt ++ ] = d1[b][k]; distance[cnt ++ ] = d2[b][k]; a = fa[a][k]; b = fa[b][k]; } distance[cnt ++ ] = d1[a][0]; distance[cnt ++ ] = d2[a][0]; distance[cnt ++ ] = d1[b][0]; distance[cnt ++ ] = d2[b][0]; } LL max1 = -INF, max2 = -INF;//找出最大值与次大值 for (int i = 0; i < cnt; i ++ ) { if (distance[i] > max1)//若最大值可被更新 { max2 = max1; max1 = distance[i]; } else if (max2 < distance[i] && distance[i] < max1) max2 = distance[i];//次大值可被更新 } if (w > max1) return w - max1;//找出符合条件的w - max1的最小值然后加上main函数里的sum即为 if (w > max2) return w - max2;//最小生成树 return INF;//说明不存在可以被更新次小生成树。 } LL kruscal() { LL res = 0; sort(edge, edge + m); memset(h, -1, sizeof h); for (int i = 1; i <= n; i ++ ) p[i] = i; for (int i = 0; i < m; i ++ ) { int a = edge[i].a, b = edge[i].b, w = edge[i].w; int fa = find(a), fb = find(b); if (fa != fb) { p[fa] = fb; res += w; edge[i].used = true; add(a, b, w), add(b, a, w); } } return res; } int main() { cin >> n >> m; for (int i = 0; i < m; i ++ ) { int a, b, c; scanf("%d %d %d", &a, &b, &c); edge[i] = {a, b, c}; } LL sum = kruscal();//求最小生成树 bfs();//预处理fa, d1, d2数组 LL res = 1e18; for (int i = 0; i < m; i ++ ) if (!edge[i].used)//枚举分数边 { int a = edge[i].a, b = edge[i].b, w = edge[i].w; res = min(res, sum + lca(a, b, w));//找出大于sum的最小值即次小生成树 } cout << res << endl; return 0; }