[week6]掌握魔法の东东 I —— 最小成本生成树(Kruskal)

题意

东东在老家农村无聊,想种田。农田有 n 块,编号从 1~n。种田要灌水
众所周知东东是一个魔法师,他可以消耗一定的 MP 在一块田上施展魔法,使得黄河之水天上来。他也可以消耗一定的 MP 在两块田的渠上建立传送门,使得这块田引用那块有水的田的水。 (1<=n<=3e2)
黄河之水天上来的消耗是 Wi,i 是农田编号 (1<=Wi<=1e5)
建立传送门的消耗是 Pij,i、j 是农田编号 (1<= Pij <=1e5, Pij = Pji, Pii =0)
东东为所有的田灌水的最小消耗

Input

第1行:一个数n
第2行到第n+1行:数wi
第n+2行到第2n+1行:矩阵即pij矩阵

Output

东东最小消耗的MP值

输入样例

4
5
4
4
3
0 2 2 2
2 0 3 3
2 3 0 4
2 3 4 0

输出样例

9

提示


分析

这道题需要利用Kruskal算法得到答案。


  • Kruskal算法

Kruskal算法实际上是一种解决最小成本生成树的贪心算法。贪心算法解决最小成本生成树问题的方法除此之外还有两种:Prim算法、Sollin算法。

在这里只介绍Kruskal算法。

首先要明确什么是最小成本生成树,以及它的性质:

  • 最小成本生成树

    • 最小成本生成树是指在一棵各相连边权重都不相等的树上找到一棵关联所有节点的、边权重之和最小的生成树。也就是在一个无向加权图中找到包含所有点集且边权之和最小的边集。

    • 生成树一定满足只有n-1条边(树中共n个节点)

  • Kruskal算法

    这种算法的本质就是每一次都从边集中选边权最小且与已选边不构成环路的边,若非法则删除,直到选满n-1条边或是没有边可选。


  • Kruskal的代码实现

算法本身并不复杂,因此实现代码也比较简单。

1. 边集

首先,我们需要实现边集。

如果按图或树的存储方式进行存储👉[week6]氪金带东——树的直径(图和树性质的应用),同样也可以保存所有边。

但是在算法中,我们需要每次都从边集中选出最小权重的边,而图和树的存储方式无法进行排序。并且,在这种存储方式中,由于是无向图,因此每条边都被存储了两次,而算法的重点在于挑选边,而与点集没有太大关系,所以一定程度上浪费了空间。

因此我们如果单独将所有边都存储在数组中,既可以节省空间,又方便排序。

因为每次都需要挑选最小成本边,所以可以选择==最小优先级队列(小根堆)==来存储,每次只需要取出队首元素并弹出即可。

2. 如何判断待选边是否在已选边集中构成环路?

构成环路也就是说新边的两点已经是我们已选边集中。

判断两个元素是否在同一个集合中,这就自然想起并查集👉[week6]戴好口罩!—— 并查集的基本应用

对当前选中最小边的两点进行判定,若它们在同一集合中,则该边不合法,否则该边符合要求,将两个点所在集合合并。


  • 题目分析

这道题目如果没有从天上引水这一条件,那么就是基础的最小成本生成树问题。但是题目中提到东东需要从天上进行引水,才能使田地被浇灌。

在这里需要明确,建立传送门就能共享水源。那么只要至少有一个田地被引水,将剩余的田地用传送门连接之后,所有的田地都能有水。

因此要想求最小成本生成树,还需要包含边权(传送门消耗)之外的引水消耗。

那不妨将引水作为一个节点添加到树中,其与原树中所有节点相连的边即为每个田地引水的消耗。

这样再对新的树求最小成本生成树,所得到的答案一定包含了至少一处引水。


  • 代码实现

这道题的代码实现没有太大难度,在这里记录一下两个需要小小注意的地方。

  • 输入边并存储

输入数据中给出的是一个完整的邻接矩阵,但是算法实际上只需要边集。由于数据结构本质上是一个无向图,也就是说邻接矩阵中的元素一定关于对角线对称,对角线上全为0。因此我们只需要将上三角形或下三角形的点新建边并存入集合中就可以。

【小tip:由于是无向图,因此每条边的两个点对应都要存储该边。也就是边e1(a,b)与e2(b,a)是等同的。而在邻接矩阵中,行和列的索引就分别代表边的两点,因此在(a,b)与(b,a)
处存储的是同一条边的权重,所以这是一个对称矩阵。而图中不可能存在自己与自己相连的边,因此对角线全为0】

  • 权重数组的二次利用

由于输入数据单独输入引水的消耗,因此在代码中我先用一个数组对其进行存储,在输入矩阵时,建立每个节点对应的边前先建立每个节点对应表示引水消耗的边。而在使用完之后,由于对应点集还需要建立并查集数组,则可以直接将存储权重的数组二次利用。


总结

  1. 小小的变通就可以使问题变得很简单🤓

代码

//
//  main.cpp
//  lab3
//
//

#include <iostream>
#include <vector>
#include <queue>
#include <numeric>
using namespace std;

struct value
{
    int a,b,weight;
};

struct cmp
{
    bool operator () ( value& a, value& b)
    {
        return a.weight > b.weight;
    }
};

vector<int> water(400,0);
vector<int> numbers(400,1);
vector<int> ans(400);
priority_queue<value, vector<value>, cmp> door;

int finds(int n)            //查找祖先
{
    if( water[n] == n )
        return n;
    else
        return water[n] = finds(water[n]);
}

void unite(int a,int b)                 //合并集合
{
    int a1 = finds(a);              //查找a和b的祖先
    int b1 = finds(b);
    
    if( a1 == b1 )          //若为同一个集合,则不需要合并
        return;
   
    if( numbers[b1] > numbers[a1] )         //始终保持a集合中元素更多
        swap(b1, a1);
        
    water[b1] = a1;                     //小集合挂在大集合上
    numbers[a1] += numbers[b1];             //a集合的元素树为合并之后的和
}

int main()
{
    ios::sync_with_stdio(false);
    
    int n = 0,w = 0;
    cin>>n;
    
    for( int i = 1 ; i <= n ; i++ )     //输入传水消耗
    {
        cin>>w;
        water[i] = w;
    }
    
    for( int i = 1 ; i <= n ; i++ )         //建立边集
    {
        door.push({0,i,water[i]});     //建立每个门引水消耗边
        
        for( int j = 1 ; j <= n ; j++ )
        {
            cin>>w;
            if( j > i )                     //由于是无向图,因此一定对称,只存储列述大鱼当前行数的值
                door.push({i,j,w});        //将当前行数和列数所代表的边及其建门消耗存入边集中
        }
    }
    
    water.clear();
    
    for( int i = 1 ; i <= n ; i++ )     //将water数组用来记录并查集,检查当前所选边是否连通
        water[i] = i;

    
    while( n > 0 )                  //一共n+1个点(加上了引水),则一共会选择n条边
    {
        value now = door.top();     //取当前最小边
        door.pop();
        
        int a = finds(now.a);       //查找当前边连接的两点的祖先
        int b = finds(now.b);
        
        if( a != b )                //若不连通,符合要求
        {
            ans.push_back(now.weight);      //将该边的权重放入结果数组中
            unite(a, b);                    //将两个点合并
            n--;                            //待选边数-1
        }
    }
    
    cout<<accumulate(ans.begin(), ans.end(),0)<<endl;       //输出所选权重和
    
    return 0;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

天翊藉君

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

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

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

打赏作者

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

抵扣说明:

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

余额充值