## [$AcWing$ $1148$ 秘密的牛奶运输](https://www.acwing.com/problem/content/description/1150/) ### 一、题目描述 农夫约翰要把他的牛奶运输到各个销售点。 运输过程中,可以先把牛奶运输到一些销售点,再由这些销售点分别运输到其他销售点。 **运输的总距离越小,运输的成本也就越低。** 低成本的运输是农夫约翰所希望的。 不过,他并不想让他的竞争对手知道他具体的运输方案,所以他希望采用 **费用第二小** 的运输方案 **而不是最小的**。 现在请你帮忙找到该运输方案。 **注意:** * 如果两个方案至少有一条边不同,则我们认为是不同方案; * 费用第二小的方案在数值上一定要严格大于费用最小的方案; * 答案保证一定有解; **输入格式** 第一行是两个整数 $N,M$,表示销售点数和交通线路数; 接下来 $M$ 行每行 $3$ 个整数 $x,y,z$,表示销售点 $x$ 和销售点 $y$ 之间存在线路,长度为 $z$。 **输出格式** 输出费用第二小的运输方案的运输总距离。 **数据范围** $1≤N≤500$, $1≤M≤10^4$, $1≤z≤10^9$, 数据中可能包含重边。 **输入样例**: ```cpp {.line-numbers} 4 4 1 2 100 2 4 200 2 3 250 3 4 100 ``` **输出样例**: ```cpp {.line-numbers} 450 ``` ### 二、解题思路 本题求 **严格次小生成树**。我们只需要 **将最小生成树中某一条边替换为另一条较大的边** 即可,可以尝试加上每一条非树边,然后去掉多余的边,最后在所有方案中 **求权值最小** 的那个就是答案了。 ![](https://img-blog.csdnimg.cn/20200823224149811.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMwMjc3MjM5,size_16,color_FFFFFF,t_70) 如图所示,我们求出了图的一个最小生成树,然后尝试连接$u$到$v$(**非最小生成树中的边**),从而在生成树中$u$到$v$的路径加上$u$到$v$的这条边就构成了一个环,我们可以删掉 **生成树中** $u$到$v$的路径中的 **任意一条边**,就可以得到新的生成树。 由最小生成树的性质知,$u\sim v$之间的边权 **一定是这个环上边权最大的一个** ,否则当初就不如走它了,那也就不是最小生成树了。为了生成一棵 **次小生成树** ,需要在环中删除那个小于$u\sim v$边权的边中的 **最大值**。 设原最小生成树的边权之和为$sum$,$u\sim v$的边权为$w$,待删除的树边的边权是$d$,则生成的新的生成树的边权之和为$sum + w - d$,$sum$和$w$是固定的,为了边权之和尽可能的小,则待删去的边权$d$要尽可能的大,这就解释了为什么要删去环中除$w$外 **边权最大的边**。 因为题目要求的是 **严格意义上的次小生成树**,要求新生成树的权值和 **一定要比最小生成树大**,所以在上图的环中如果删除了和$w$一样大的树边,得到的还是最小生成树,既然不能确定生成树中$u$到$v$经过的边的边权都不大于$w$,那么只好求出$u$到$v$的路径中边权的 **最大值**和 **次大值** 了,即使最大值等于$w$,次大值也会小于$w$。 > **解释**: > - **次小生成树** :次小生成树的边长和 **大于等于** 最小生成树的边长和 > - **严格次小生成树** :次小生成树的边长和 **大于** 最小生成树的边长和 > > **分情况讨论** > - 如果$w==d_{zd}$,则替换掉$d_{cd}$,即$w->d_{cd}$ > - 如果$w>d_{zd}$,则替换掉$d_{zd}$,即$w->d_{zd}$ 下面的问题就是如何在一棵树中 **求任意两个节点间路径中最大的边权和次大的边权** 了,可以用$dfs$+**换根** 来实现。 #### 时间复杂度 $O(N^2)$ ### 四、实现代码 ```cpp {.line-numbers} #include using namespace std; typedef long long LL; const int N = 510, M = 10010; int n, m; // 结构体 struct Edge { int a, b, w; bool flag; // 是不是最小生成树中的边 bool const operator<(const Edge &t) const { return w < t.w; } } edge[M]; // 因为本题需要用链式前向星建图,所以避开了使用e做为边的数组名称 int d1[N][N]; // 从i出发,到达j最短距离 int d2[N][N]; // 从i出发,到达j次短距离 LL sum; // 最小生成树的边权和 // 邻接表 int h[N], e[M], w[M], ne[M], idx; void add(int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; } // 并查集 int p[N]; int find(int x) { if (x == p[x]) return x; return p[x] = find(p[x]); } /* 假设根为s,求出树中任意两点间的最长距离和严格次长距离。 需要配合换根进行枚举操作才会有效果。 s:出发点 u:到达了u点 fa:u的前序节点,防止走回头路 m1:这条路径上已经获取到的最长路径 m2:这条路径上已经获取到的次长路径 */ void dfs(int s, int u, int fa, int m1, int m2) { for (int i = h[u]; ~i; i = ne[i]) { // 枚举u的每一条出边 int v = e[i]; // v为u的对边节点 if (v == fa) continue; // 不走回头路 int t1 = m1, t2 = m2; // 必须要复制出来td1和td2,原因是此轮要分发多个子任务,此m1,m2是多个子任务共享的父亲传递过来的最大和次大值 if (w[i] > t1) t2 = t1, t1 = w[i]; // 更新最大值、次大值 else if (w[i] < t1 && w[i] > t2) t2 = w[i]; // 更新严格次大值 // 记录从s出发点,到v节点,一路上的最长路径和严格次长路径 d1[s][v] = t1, d2[s][v] = t2; // 生命不息,探索不止 dfs(s, v, u, t1, t2); } } int main() { scanf("%d %d", &n, &m); // 初始化邻接表 memset(h, -1, sizeof h); // Kruskal + 建图 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求最小生成树 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; // ②最小生成树建图,无向图,为求最小生成树中任意两点间的路径中最大距离、次大距离做准备 add(a, b, w), add(b, a, w); // ③标识此边为最小生成树中的边,后面需要枚举每条不在最小生成树中的边 edge[i].flag = 1; } } // d1[i][j]和d2[i][j] // 换根,以每个点为根,进行dfs,可以理解为枚举了每一种情况,肯定可以获取到任意两点间的最长路径和严格次长路径 for (int i = 1; i <= n; i++) dfs(i, i, 0, 0, 0); LL res = 1e18; // 预求最小,先设最大 // 枚举所有不在最小生成树中的边,尝试加入a->b的这条直边 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 > d1[a][b]) // 最小生成树外的一条边,(a-b),如果比最小生成树中a-b的最长边长,就有机会参加评选次小生成树。 // 最终的选举结果取决于它增加的长度是不是最少的 res = min(res, sum + w - d1[a][b]); // 替换最大边 else if (w > d2[a][b]) // 替换严格次大边 res = min(res, sum + w - d2[a][b]); // 严格次小生成树的边权和 } // 输出 printf("%lld\n", res); return 0; } ```