You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

7.9 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

AcWing 1148 秘密的牛奶运输

一、题目描述

农夫约翰要把他的牛奶运输到各个销售点。

运输过程中,可以先把牛奶运输到一些销售点,再由这些销售点分别运输到其他销售点。

运输的总距离越小,运输的成本也就越低。

低成本的运输是农夫约翰所希望的。

不过,他并不想让他的竞争对手知道他具体的运输方案,所以他希望采用 费用第二小 的运输方案 而不是最小的

现在请你帮忙找到该运输方案。

注意:

  • 如果两个方案至少有一条边不同,则我们认为是不同方案;
  • 费用第二小的方案在数值上一定要严格大于费用最小的方案;
  • 答案保证一定有解;

输入格式 第一行是两个整数 N,M,表示销售点数和交通线路数;

接下来 M 行每行 3 个整数 x,y,z,表示销售点 x 和销售点 y 之间存在线路,长度为 z

输出格式 输出费用第二小的运输方案的运输总距离。

数据范围 1≤N≤500, 1≤M≤10^4, 1≤z≤10^9, 数据中可能包含重边。

输入样例

4 4
1 2 100
2 4 200
2 3 250
3 4 100

输出样例

450

二、解题思路

本题求 严格次小生成树。我们只需要 将最小生成树中某一条边替换为另一条较大的边 即可,可以尝试加上每一条非树边,然后去掉多余的边,最后在所有方案中 求权值最小 的那个就是答案了。

如图所示,我们求出了图的一个最小生成树,然后尝试连接uv(非最小生成树中的边),从而在生成树中uv的路径加上uv的这条边就构成了一个环,我们可以删掉 生成树中 uv的路径中的 任意一条边,就可以得到新的生成树。

由最小生成树的性质知,u\sim v之间的边权 一定是这个环上边权最大的一个 ,否则当初就不如走它了,那也就不是最小生成树了。为了生成一棵 次小生成树 ,需要在环中删除那个小于u\sim v边权的边中的 最大值

设原最小生成树的边权之和为sumu\sim v的边权为w,待删除的树边的边权是d,则生成的新的生成树的边权之和为sum + w - dsumw是固定的,为了边权之和尽可能的小,则待删去的边权d要尽可能的大,这就解释了为什么要删去环中除w边权最大的边

因为题目要求的是 严格意义上的次小生成树,要求新生成树的权值和 一定要比最小生成树大,所以在上图的环中如果删除了和w一样大的树边,得到的还是最小生成树,既然不能确定生成树中uv经过的边的边权都不大于w,那么只好求出uv的路径中边权的 最大值次大值 了,即使最大值等于w,次大值也会小于w

解释

  • 次小生成树 :次小生成树的边长和 大于等于 最小生成树的边长和
  • 严格次小生成树 :次小生成树的边长和 大于 最小生成树的边长和

分情况讨论

  • 如果w==d_{zd},则替换掉d_{cd},即w->d_{cd}
  • 如果w>d_{zd},则替换掉d_{zd},即w->d_{zd}

下面的问题就是如何在一棵树中 求任意两个节点间路径中最大的边权和次大的边权 了,可以用dfs+换根 来实现。

时间复杂度

O(N^2)

四、实现代码

#include <bits/stdc++.h>
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;
}