每周算法:次小生成树

题目链接

秘密的牛奶运输

题目描述

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

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

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

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

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

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

注意:

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

输入格式

第一行是两个整数 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 1N500,
1 ≤ M ≤ 1 0 4 1≤M≤10^4 1M104,
1 ≤ z ≤ 1 0 9 1≤z≤10^9 1z109,
数据中可能包含重边。

算法思想

根据题目描述,求的是一棵严格次小生成树。所谓严格指的是该次小生成树的总边权严格大于最小生成树的边权之和。如果次小生成树的总边权大于等于最小生成树的边权之和,那么可以称为非严格次小生成树

要求次小生成树,可以先求最小生成树,然后枚举非树边(不在最小生成树中的边),尝试将该边加入树中,同时从树中去掉一条边,保证最终仍然是一棵树。统计所有这些树的边权之和的最小值就是次小生成树。如下图所示:在这里插入图片描述
该算法的基本思想如下:

  • 使用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 ab,边权为 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 sumd1[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 sumd2[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;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

少儿编程乔老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值