AcWing 1148 秘密的牛奶运输

AcWing 1148 秘密的牛奶运输

一、题目描述

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

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

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

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

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

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

注意:

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

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

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

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

数据范围
1N500,
$1≤M≤104,
1z109,
数据中可能包含重边。

输入样例

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

输出样例

450

二、解题思路

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

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

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

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

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

解释

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

分情况讨论

  • 如果w==dzd,则替换掉dcd,即w>dcd
  • 如果w>dzd,则替换掉dzd,即w>dzd

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

时间复杂度

O(N2)

四、实现代码

#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;
}
posted @   糖豆爸爸  阅读(236)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
历史上的今天:
2020-03-25 Zabbix Agent for Linux部署
2018-03-25 对于ElasticSearch与Hadoop是如何互相调用的?
2013-03-25 我们在运营前还需要准备的技术储备
Live2D
点击右上角即可分享
微信分享提示