本题来源于Luogu,传送门
前言
本题难度:普及/提高-
涉及算法:图论、并查集、生成树
涉及语法:sort()函数(结构体)比较器
本题算是作为自己做的第一个最小生成树相关的题目,特对本题自己了解掌握学习到的相关语法算法进行汇总整理,由于基本的并查集模块比较熟练,本博客将主要聚焦于其他的部分。
一、并查集
主要思想:用一个代表元素作为一个集合的特征,对该元素修改实现合并集合的操作merge(),对该元素的访问比较实现查询两个对象是否在同一集合getFather()。
具体并查集的知识不在此赘述。感兴趣的读者可以查阅相关CSDN博客。
二、图论——生成树
在CSDN发现了一篇优质的图论——生成树合集,作者Alex_McAvoy,传送门
对一个具有 n 个点的连通图进行遍历,对于遍历后的子图,其包含原图中所有的点且保持图连通,最后的结构一定是一个具有 n-1 条边的树,通常称为生成树。
在生成树问题中,最常见的问题就是最小生成树问题,所谓最小生成树,就是对于一个有 n 个点的无向连通图的生成树,其包含原图中的所有点,且保持图连通的边权总和最少的边。
简单来说,对于一个有 n 个点的图,边一定是大于等于 n-1 条的,最小生成树,就是在这些边中选择 n-1 条出来连接所有的 n 个点,且这 n-1 条边的边权之和是所有方案中最小的。
最小生成树具有以下两条性质:
切割性质:连接点 x、y 的边权最小的边必定被生成树包含
回路性质:任意回路/环上的边权最大的边必不被生成树包含
求最小生成树一般有 Prim 算法与 Kruskal 算法,其中,Prim 算法时间复杂度为 O(V*V),与图中边数无关,适合稠密图;Kruskal 算法时间复杂度 为O(ElogE),需要对图的边进行访问,适合稀疏图。————————————————
版权声明:本文为CSDN博主「Alex_McAvoy」的原创文章,遵循CC 4.0 BY-SA版权协议,
原文链接:https://blog.csdn.net/u011815404/article/details/88625346
对于最小生成树 ,一般有Prim算法与Kruskal算法,在浅浅看了两个算法之后,惊奇地发现Kruskal算法如此清晰易懂——排序(贪心)+并查集即可解决,上手无难度。
算法大意:
Kruskal 算法基本思想是并查集思想,将所有边升序排序,并认为每一个点都是孤立的,分属 n 个独立的集合。
按顺序枚举每一条边,如果这条边连接的两个点分属两个不同的集合,那么就将这条边加入最小生成树,这两个不同的集合合并为一个集合;如果这条边连接的两个点属于同一集合,那么就跳过。直到选取 n-1条边为止(只剩一个集合)。
其时间复杂度为:O(E*logE),E代表边数。
三、sort()比较器
sort()函数存放在<algorithm>头文件中,关于sort()的用法,大致有以下两种
//对 [first, last) 区域内的元素做默认的升序排序
void sort (RandomAccessIterator first, RandomAccessIterator last);
//按照指定的 comp 排序规则,对 [first, last) 区域内的元素进行排序
void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp);
自定义排序,我们需要第二种,并且通过自定义比较函数comp来实现排序的自定义,而我们只关心怎样才返回逻辑上的升序降序。
comp函数的定义需要两个参数,参数a和参数b,当满足一定的条件的时候返回ture或false,只需要记住一条规则:
1.当返回值为true时,第一个参数放在前面,第二个参数放在后面; false则反之
2.当a = b时,必须要返回fasle
记住这两条规则,就可以自定义排序函数实现自定义排序的目的。
四、P2330 [SCOI2005]繁忙的都市
题目描述
城市 C 是一个非常繁忙的大都市,城市中的道路十分的拥挤,于是市长决定对其中的道路进行改造。城市 C 的道路是这样分布的:城市中有 n 个交叉路口,有些交叉路口之间有道路相连,两个交叉路口之间最多有一条道路相连接。这些道路是双向的,且把所有的交叉路口直接或间接的连接起来了。每条道路都有一个分值,分值越小表示这个道路越繁忙,越需要进行改造。但是市政府的资金有限,市长希望进行改造的道路越少越好,于是他提出下面的要求:
- 改造的那些道路能够把所有的交叉路口直接或间接的连通起来。
- 在满足要求 1 的情况下,改造的道路尽量少。
- 在满足要求 1、2 的情况下,改造的那些道路中分值最大的道路分值尽量小。
任务:作为市规划局的你,应当作出最佳的决策,选择哪些道路应当被修建。
输入格式
第一行有两个整数 n,m
n,m 表示城市有 nn 个交叉路口,mm 条道路。
接下来 m 行是对每条道路的描述,u, v, c
u,v,c 表示交叉路口 uu 和 vv 之间有道路相连,分值为 cc。
输出格式
两个整数 s, max,表示你选出了几条道路,分值最大的那条道路的分值是多少。
说明/提示
数据范围及约定
对于全部数据,满足 1\le n\le 3001≤n≤300,1\le c\le 10^41≤c≤104,1 \le m \le 10^51≤m≤105。
输入样例#1:
4 5
1 2 3
1 4 5
2 4 7
2 3 6
3 4 8
输出样例#1:
3 6
五、题目分析
· 将所有的路口联通——连通块——并查集问题。
· 道路具有分值——边权——图论。
· “分值最大的道路分值尽量小”+图论——最小生成树。
本题算是一道最小生成树的裸题,思路十分明确:
1、以分值为关键字升序排序 2、并查集筛选
由于每一条路涉及到路两端的十字路口、 路的“分值”,所以我选择采用结构体的方式存数据。
struct crossroads{
int a,b;
int value;
}roads_data[100005];
那么对结构体数组的排序,如若图方便不自写排序,用头文件<algorithm>的sort()函数,就需要编写相应的比较函数。根据相应的分值升序排列。
bool cmpMod(crossroads a,crossroads b)
{
return a.value<b.value? true:false;
//return a.value<b.value;
}
至于排完序,这就是一道裸裸的并查集题目,并没有其他的陷阱。
int road[305];
int getFather(int x)
{
if(road[x]==x)return x;
return road[x]=getFather(road[x]);
}
void merge(int x,int y)
{
road[getFather(x)]=getFather(y);
}
六、过关代码
至此,所有的源代码如下。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<iomanip>
#include<limits.h>
using namespace std;
struct crossroads{
int a,b;
int value;
}roads_data[100005];
bool cmpMod(crossroads a,crossroads b)
{
return a.value<b.value? true:false;
//return a.value<b.value;
}
int n,m;
int road[305];
int getFather(int x)
{
if(road[x]==x)return x;
return road[x]=getFather(road[x]);
}
void merge(int x,int y)
{
road[getFather(x)]=getFather(y);
}
int times=0;
int maxx;
int main()
{
cin>>n>>m;
for(int i=0;i<m;++i)
{
cin>>roads_data[i].a>>roads_data[i].b>>roads_data[i].value;
}
sort(roads_data,roads_data+m,cmpMod);
for(int i=0;i<n;++i)
{
road[i]=i;
}
for(int i=0;i<m;++i)
{
if(getFather(roads_data[i].a) != getFather(roads_data[i].b))
{
merge(roads_data[i].a,roads_data[i].b);
times++;
maxx=roads_data[i].value;
}
}
cout<<times<<" "<<maxx;
return 0;
}