次小生成树三种算法详解 + 模板题 :《信息学奥赛一本通》 , USACO 秘密的牛奶运输 《算法竞赛进阶指南》次小生成树

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

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

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

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

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

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

注意:

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

输入格式

第一行是两个整数 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;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

啥也不会hh

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

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

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

打赏作者

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

抵扣说明:

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

余额充值