最小生成树(一些题解 kruskal)

这里是利用kruskal算法来解题,以此来加深对kruskal算法的理解。

回顾

kruskal算法的基本步骤:

  1. 排序边: 将图中的所有边按照权重从小到大进行排序。
  2. 初始化: 将图中的每个顶点视为一个独立的树(树只有一个顶点)。
  3. 按权重顺序加入边: 从排序后的边中依次选择最小权重的边。
  4. 检查边的两端顶点:1.如果这条边连接的两个顶点不在同一棵树中,说明加入这条边不会形成环,则将这条边加入最小生成树。2.如果两个顶点在同一棵树中,说明加入这条边会形成环,应该跳过这条边。
  5. 合并树: 如果加入了一条边,则将连接的两个顶点所在的两棵树合并为一棵。

  6. 重复步骤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;
}

小结:

以上四道题基本上都是模板题,先输入并存储边的信息,接着初始化父节点数组,然后排序(从小到大或从大到小) ,再接着遍历边,根据题目信息和要求对数据进行处理,最后输出结果。

  • 17
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值