这里是利用kruskal算法来解题,以此来加深对kruskal算法的理解。
回顾
kruskal算法的基本步骤:
- 排序边: 将图中的所有边按照权重从小到大进行排序。
- 初始化: 将图中的每个顶点视为一个独立的树(树只有一个顶点)。
- 按权重顺序加入边: 从排序后的边中依次选择最小权重的边。
- 检查边的两端顶点:1.如果这条边连接的两个顶点不在同一棵树中,说明加入这条边不会形成环,则将这条边加入最小生成树。2.如果两个顶点在同一棵树中,说明加入这条边会形成环,应该跳过这条边。
-
合并树: 如果加入了一条边,则将连接的两个顶点所在的两棵树合并为一棵。
-
重复步骤3至5: 重复执行步骤3至5,直到最小生成树包含图中所有的顶点。
请注意,Kruskal算法要求图是连通的,即任意两个顶点之间都存在路径。如果图不是连通的,Kruskal算法将无法构建最小生成树。
题目1:
P1991 无线通讯网 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题目描述
国防部计划用无线网络连接若干个边防哨所。2 种不同的通讯技术用来搭建无线网络;
每个边防哨所都要配备无线电收发器;有一些哨所还可以增配卫星电话。
任意两个配备了一条卫星电话线路的哨所(两边都有卫星电话)均可以通话,无论他们相距多远。而只通过无线电收发器通话的哨所之间的距离不能超过 D,这是受收发器的功率限制。收发器的功率越高,通话距离 D 会更远,但同时价格也会更贵。
收发器需要统一购买和安装,所以全部哨所只能选择安装一种型号的收发器。换句话说,每一对哨所之间的通话距离都是同一个 D。你的任务是确定收发器必须的最小通话距离 D,使得每一对哨所之间至少有一条通话路径(直接的或者间接的)。
输入格式
第一行,2 个整数 S 和 P,S 表示可安装的卫星电话的哨所数,P 表示边防哨所的数量。
接下里 P 行,每行两个整数 x,y 描述一个哨所的平面坐标 (x, y),以 km 为单位。
输出格式
第一行,1 个实数 D,表示无线电收发器的最小传输距离,精确到小数点后两位。
样例输入
2 4
0 100
0 300
0 600
150 750样例输出
212.13
思路:
- 读取哨所的坐标,边读入边计算两个点之间的距离;
- 初始化一个数组
fa
,表示哨所所属的集合; - 按照两个哨所(哨所对)之间的距离进行快速排序;
- 通过并查集对哨所对进行处理
代码:
#include<stdio.h>
#include<math.h>
#define M 1001000
int n,m;
int fa[M];
int arr[M],brr[M]; // 存储点的 x和 y坐标
struct Node{ // 定义一个结构体 Node,用于存储两个点的信息
int x; // 点对的第一个点的编号
int y; // 点对的第二个点的编号
double s; // 两点之间的距离
}node[M];
double dis(int i,int j){ // 计算两点之间的距离
return sqrt(pow(arr[i]-arr[j],2)+pow(brr[i]-brr[j],2));
}
// 快速排序函数,用于对点对按照距离进行排序
void quicksort(int l,int r){
int i,j;
struct Node t; // 用于交换的临时变量
if(l>r) return ; // 如果l>r,则直接返回
i=l;
j=r;
while(i!=j){ // 当 i不等于 j时,进行循环
while(node[j].s>=node[l].s&&i<j) // 从右向左找第一个小于 node[l].s的点对
j--;
while(node[i].s<=node[l].s&&i<j) // 从左向右找第一个大于 node[l].s的点对
i++;
if(i<j){ // 如果 i<j,则交换 node[i]和 node[j]
t=node[i];
node[i]=node[j];
node[j]=t;
}
}
t=node[l]; // 将 node[l]与 node[i]交换,使得 node[l]左边的点都小于它,右边的点都大于它
node[l]=node[i];
node[i]=t;
quicksort(l,i-1); // 对左边的部分进行递归排序
quicksort(i+1,r); // 对右边的部分进行递归排序
return ;
}
int find(int x){
if(fa[x]!=x){ // 如果 x不是自己的父节点,说明 x不在同一个集合中
fa[x]=find(fa[x]); // 继续向上查找 x的根节点,并进行路径压缩
}
return fa[x]; // 返回 x的根节点
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d",&arr[i],&brr[i]);
}
int k=1;
for(int i=1;i<=m;i++){
for(int j=i+1;j<=m;j++){
node[k].x=i;
node[k].y=j;
node[k++].s=dis(i,j); // 计算 i和 j之间的距离,并存储在 s中,然后 k自增
}
}
for(int i=1;i<=k;i++) // 初始化,每个点的父节点都是自己
fa[i]=i;
quicksort(1,k); // 对所有点对按照距离进行快速排序
int t=0; // 初始化计数器 t为 0,用于记录不需要安装无线电收发器的哨所数量
for(int i=1;i<=k;i++){
int tx=find(node[i].x);
int ty=find(node[i].y);
if(tx==ty) continue; // 如果x和y已经在同一个集合中,跳过此次循环
fa[tx]=ty; // 否则,将x点的根节点设置为y点的根节点,合并两个集合
t++;
if(t==m-n){ //当 t达到总哨所数减去安装卫星电话的哨所数量时,则打印结果
printf("%.2lf",node[i].s);
break;
}
}
return 0;
}
题目2:
P2121 拆地毯 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题目背景
还记得 NOIP 2011 提高组 Day1 中的铺地毯吗?时光飞逝,光阴荏苒,三年过去了。组织者精心准备的颁奖典礼早已结束,留下的则是被人们踩过的地毯。请你来解决类似于铺地毯的另一个问题。
题目描述
会场上有 n 个关键区域,不同的关键区域由 m 条无向地毯彼此连接。每条地毯可由三个整数 u、v、w 表示,其中 u 和 v 为地毯连接的两个关键区域编号,w 为这条地毯的美丽度。
由于颁奖典礼已经结束,铺过的地毯不得不拆除。为了贯彻勤俭节约的原则,组织者被要求只能保留至多 K 条地毯,且保留的地毯构成的图中,任意可互相到达的两点间只能有一种方式互相到达。换言之,组织者要求新图中不能有环。现在组织者求助你,想请你帮忙算出这至多 K 条地毯的美丽度之和最大为多少。
输入格式
第一行包含三个正整数 n、m、K。
接下来 m 行中每行包含三个正整数 u、v、w。
输出格式
只包含一个正整数,表示这 K 条地毯的美丽度之和的最大值。
样例输入
5 4 3
1 2 10
1 3 9
2 3 7
4 5 3样例输出
22提示
选择第 1、2、4 条地毯,美丽度之和为 10 + 9 + 3 = 22。
若选择第 1、2、3 条地毯,虽然美丽度之和可以达到 10 + 9 + 7 = 26,但这将导致关键区域 1、2、3 构成一个环,这是题目中不允许的。
思路:
- 利用kruskal算法解题;
- 使用快速排序对边进行排序;
- 遍历排序后的边,使用并查集来判断每条边连接的两个地毯是否属于同一个连通图(如果不属于同一个连通图,则合并这两个集合,并累加权值);
- 如果成功选择了k条边,则打印这k条边的权值和
代码:
#include<stdio.h>
#include<stdbool.h>
#include<stdlib.h>
#define M 100001
int n,m,k;
struct Node{ // 记录两块连接的地毯的编号和边权值
int u; // 其中一块的编号
int v; //另一块的编号
int w; //边权值
}node[M];
int fa[M]; // 用于存储并查集的父节点
void quicksort(int l,int r){ // 快速排序函数,对边按照权值进行排序
int i,j;
struct Node t;
if(l>r) return ; // 如果左指针大于右指针,则递归结束
i=l;
j=r;
while(i!=j){
while(node[j].w<=node[l].w&&i<j) // 从右向左找到第一个大于node[l].w的节点
j--;
while(node[i].w>=node[l].w&&i<j) // 从左向右找到第一个小于node[l].w的节点
i++;
if(i<j){
t=node[i];
node[i]=node[j];
node[j]=t;
}
}
t=node[l];
node[l]=node[i];
node[i]=t;
quicksort(l,i-1); // 对左半部分递归排序
quicksort(i+1,r); // 对右半部分递归排序
return ;
}
int find(int x){ // 查找函数,用于找到节点x的根节点
if(fa[x]!=x){
fa[x]=find(fa[x]); // 路径压缩,直接将x的父节点指向根节点
}
return fa[x];
}
int main(){
int sum=0,cnt=0,flag=0; // sum记录权值和 cnt记录已选的边数 flag标志是否已经选够 k条边
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=m;i++)
scanf("%d%d%d",&node[i].u,&node[i].v,&node[i].w);
for(int i=1;i<=n;i++) // 初始化,每个节点的父节点都是自己
fa[i]=i;
quicksort(1,m);
for(int i=1;i<=m;i++){
int tu=find(node[i].u); // 找到一块地毯所在的集合的根节点
int tv=find(node[i].v); // 找到另一块地毯所在的集合的根节点
if(tu!=tv){ // 如果两个节点不在同一个集合中
fa[tu]=tv; // 将 tu所在集合合并到 tv所在集合
cnt++; // 已选边数加一
sum+=node[i].w; // 权值和相加
}
if(cnt==k){ // 如果已经选择了k条边
flag=1; // 设置标志位,1为选够 k条边
break;
}
}
if(flag==1) // 如果 flag为 1,则打印结果
printf("%d\n",sum);
return 0;
}
题目3:
P1194 买礼物 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题目描述
又到了一年一度的明明生日了,明明想要买 B 样东西,巧的是,这 B 样东西价格都是 A 元。
但是,商店老板说最近有促销活动,也就是:
如果你买了第 I 样东西,再买第 J 样,那么就可以只花 K_{I,J} 元,更巧的是,K_{I,J} 竟然等于 K_{J,I}。
现在明明想知道,他最少要花多少钱。
输入格式
第一行两个整数,A,B。
接下来 B 行,每行 B 个数,第 I 行第 J 个为 K_{I,J}。
我们保证 K_{I,J}=K_{J,I} 并且 K_{I,I}=0。
特别的,如果 K_{I,J}=0,那么表示这两样东西之间不会导致优惠。
注意 K_{I,J} 可能大于 A。
输出格式
一个整数,为最小要花的钱数。
样例 #1
样例输入
1 1
0样例输出
1样例 #2
样例输入
3 3
0 2 4
2 0 2
4 2 0样例输出
7提示
样例解释 2。
先买第 2 样东西,花费 3 元,接下来因为优惠,买 1,3 样都只要 2 元,共 7 元。
(同时满足多个“优惠”的时候,聪明的明明当然不会选择用 4 元买剩下那件,而选择用 2 元。)
思路:
- 通过两重循环,代码读取每对物品之间的优惠关系;
- 使用快速排序算法对node数组进行排序,按照优惠后的价格从小到大排列;
- 代码遍历排序后的优惠关系数组;
- 最后还需判断所以物品是否在同一集合
代码:
#include<stdio.h>
#define M 510
int n,m;
struct Node{
int x,y,c;
}node[M*M];
int fa[M];
int find(int x){
if(fa[x]!=x) fa[x]=find(fa[x]);
return fa[x];
}
void quicksort(int l,int r){
int i,j;
struct Node t;
if(l>r) return ;
i=l;
j=r;
while(i!=j){
while(node[j].c>=node[l].c&&i<j)
j--;
while(node[i].c<=node[l].c&&i<j)
i++;
if(i<j){
t=node[i];
node[i]=node[j];
node[j]=t;
}
}
t=node[l];
node[l]=node[i];
node[i]=t;
quicksort(l,i-1);
quicksort(i+1,r);
return ;
}
int main(){
int k=0,z,t=0,sum=0,cnt=0;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
fa[i]=i;
for(int i=1;i<=m;i++) //利用两重循环来记录购买第 i件物品后第 j件物品的价格(有优惠的情况)
for(int j=1;j<=m;j++){
scanf("%d",&z);
if(z!=0){
node[++k].x=i;
node[k].y=j;
node[k].c=z;
}
if(node[k].c>n) cnt++; // 记录没有优惠或优惠后价格仍大于 n的物品数
}
quicksort(1,k); //排序的时候注意要全部一起比
for(int i=1;i<=k-cnt;i++){ //遍历优惠后价格低于 n的物品
int tx=find(node[i].x); //查找两件物品是否在同一集合
int ty=find(node[i].y);
if(tx!=ty){ //不在同一集合则合并集合,并且合并集合的次数自增
fa[tx]=ty;
t++;
sum+=node[i].c; //权值相加
}
}
// 判断所有的点是否在同一集合
if(t==m-1) printf("%d\n",sum+n); //如果在,则打印权值和加 n
else printf("%d\n",sum+(m-t)*n); //如果不在,则打印权值和加 剩余的物品数与 n的乘积
return 0;
}
题目4:
P1396 营救 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题目背景
“咚咚咚……”“查水表!”原来是查水表来了,现在哪里找这么热心上门的查表员啊!小明感动得热泪盈眶,开起了门……
题目描述
妈妈下班回家,街坊邻居说小明被一群陌生人强行押上了警车!妈妈丰富的经验告诉她小明被带到了 t 区,而自己在 s 区。
该市有 m 条大道连接 n 个区,一条大道将两个区相连接,每个大道有一个拥挤度。小明的妈妈虽然很着急,但是不愿意拥挤的人潮冲乱了她优雅的步伐。所以请你帮她规划一条从 s 至 t 的路线,使得经过道路的拥挤度最大值最小。
输入格式
第一行有四个用空格隔开的 n,m,s,t,其含义见【题目描述】。
接下来 m 行,每行三个整数 u, v, w,表示有一条大道连接区 u 和区 v,且拥挤度为 w。
两个区之间可能存在多条大道。
输出格式
输出一行一个整数,代表最大的拥挤度。
样例输入
3 3 1 3
1 2 2
2 3 1
1 3 3样例输出
2提示
样例输入输出解释
小明的妈妈要从 1 号点去 3 号点,最优路线为 1->2->3。
思路:
- 通过快速排序算法对边数组按照权值进行排序。
- 使用并查集数据结构来维护节点的连通性。
- 遍历排序后的边数组,对于每一条边,如果其两个端点不在同一个连通分量中,并且其中一个连通分量还未被访问过,则合并这两个连通分量,并更新最大权值。
代码:
#include<stdio.h>
#include<stdbool.h>
#define M 100000
bool st[M]; // 用于标记已访问的节点
int n,m,s,t;
struct Node{
int u;
int v;
int w;
}node[M];
int fa[M];
void quicksort(int l,int r){
int i,j;
struct Node t;
if(l>r) return ;
i=l;
j=r;
while(i!=j){
while(node[j].w>=node[l].w&&i<j)
j--;
while(node[i].w<=node[l].w&&i<j)
i++;
if(i<j){
t=node[i];
node[i]=node[j];
node[j]=t;
}
}
t=node[l];
node[l]=node[i];
node[i]=t;
quicksort(l,i-1);
quicksort(i+1,r);
return ;
}
int find(int x){
if(fa[x]!=x) fa[x]=find(fa[x]);
return fa[x];
}
int main(){
int max=-1; // 用于记录最大权值,即最大拥挤度
scanf("%d%d%d%d",&n,&m,&s,&t);
for(int i=1;i<=n;i++) // 初始化
fa[i]=i;
for(int i=1;i<=m;i++)
scanf("%d%d%d",&node[i].u,&node[i].v,&node[i].w);
quicksort(1,m); // 对边数组进行快速排序
for(int i=1;i<=m;i++){
int tu=find(node[i].u); // 查找大道一端(道的起点)所在集合的根节点
int tv=find(node[i].v); // 查找大道另一端(道的终点)所在集合的根节点
if(tu!=tv&&!st[tu]){ // 如果两个节点不在同一个集合且起点所在集合未被访问过
fa[tu]=tv; // 合并两个集合
st[tu]=true; // 标记起点所在集合已被访问
if(max<node[i].w){ // 更新最大权值(最大拥挤度)
max=node[i].w;
}
if(find(s)==find(t)){ // 如果起始点和目标点现在属于同一个集合,则跳出循环
break;
}
}
}
printf("%d\n",max);
return 0;
}
小结:
以上四道题基本上都是模板题,先输入并存储边的信息,接着初始化父节点数组,然后排序(从小到大或从大到小) ,再接着遍历边,根据题目信息和要求对数据进行处理,最后输出结果。