求最小生成树相关例题题解

本文讲述了如何使用Kruskal算法解决Luogu竞赛中涉及无线通讯网络规划和地毯拆卸的最小生成树问题,以及如何在买礼物场景中优化成本的示例。
摘要由CSDN通过智能技术生成

一.前言

求最小生成树常用的两种算法:prim算法和kruskal算法,之前我已经总结过prim算法的相关知识和使用方法(详细可以看这篇博客),而kruskal算法比较简单,更好操作(主要通过并查集和排序),所以在这里我就不总结了。如果有需要可以自行到网上学习(可以看看这篇博客)。

接下来我会总结最近写的几道求最小生成树的相关例题,(其中大多数用到kruskal算法,就当弥补一下),如果有需要以后会总结kruskal算法相关知识。


二.P1991 无线通讯网

https://www.luogu.com.cn/problem/P1991

题目描述

国防部计划用无线网络连接若干个边防哨所。2 种不同的通讯技术用来搭建无线网络;

每个边防哨所都要配备无线电收发器;有一些哨所还可以增配卫星电话。

任意两个配备了一条卫星电话线路的哨所(两边都有卫星电话)均可以通话,无论他们相距多远。而只通过无线电收发器通话的哨所之间的距离不能超过 D,这是受收发器的功率限制。收发器的功率越高,通话距离 D 会更远,但同时价格也会更贵。

收发器需要统一购买和安装,所以全部哨所只能选择安装一种型号的收发器。换句话说,每一对哨所之间的通话距离都是同一个 D。你的任务是确定收发器必须的最小通话距离 D,使得每一对哨所之间至少有一条通话路径(直接的或者间接的)。

输入格式

第一行,22 个整数 S 和 P,S 表示可安装的卫星电话的哨所数,P 表示边防哨所的数量。

接下里 P 行,每行两个整数 x,y 描述一个哨所的平面坐标 (x,y),以 km 为单位。

输出格式

第一行,11 个实数 D,表示无线电收发器的最小传输距离,精确到小数点后两位。

输入输出样例

输入 #1复制

2 4
0 100
0 300
0 600
150 750

输出 #1复制

212.13

说明/提示

数据范围及约定

  • 对于 20% 的数据:P=2,S=1;
  • 对于另外 20% 的数据:P=4,S=2;
  • 对于100% 的数据保证:1≤S≤100,S<P≤500,0≤x,y≤10000。

 这道题就是最小生成树的类型题,这里只要注意一下两点点间距离,将它们表示出来,作为边的权值就可以了,所以通过该函数实现

double length(int x1, int y1,int x2, int y2)
{
	return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}

完整代码

#include<bits/stdc++.h>
using namespace std;
#define N 1000005
int s, p, n, num;
double ans;
int a[N], b[N], pre[N];      //pre数组实现并查集
struct node {
	int x, y;
	double dis;
}f[N];                     //结构体数组存储各个点的信息以及距离

bool cmp(node x, node y)
{
	return x.dis < y.dis;
}                           //sort内部比较函数,根据dis升序排列

double length(int x1, int y1,int x2, int y2)      //计算两点间距离
{
	return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}

int find(int x)                 //并查集,查操作
{
	if (pre[x] == x)
		return x;
	return pre[x] = find(pre[x]);
}

void k()                         //kruskal核心代码段
{
	for (int i = 1; i <= n; i++) {
		int w = find(f[i].x);
		int v = find(f[i].y);
		if (w != v)             //如果不联通
		{
			pre[w] = v;         //将两个集合联通
			ans = f[i].dis;     //边权之和
			num++;             
			if (num >= p - s)   //退出条件,找到最小生成树
			{
				printf("%.2lf\n", ans);
				return ;
			}
		}

	}
}
int main()
{
	cin >> s >> p;
	for (int i = 1; i <= p; i++) {     //pre数组初始化为自己
		pre[i] = i;
	}
	for (int i = 1; i <= p; i++) {  //任意两点将距离,存图
		cin >> a[i] >> b[i];
		for (int j = 1; j < i; j++) {
			f[++n].x = i;
			f[n].y = j;
			f[n].dis = length(a[i], b[i], a[j], b[j]);
		}
	}
	sort(f + 1,f + 1 + n, cmp);      //根据dis降序排列
	k();
	return 0;
}


  

三.P2121 拆地毯

 https://www.luogu.com.cn/problem/P2121

题目背景

还记得 NOIP 2011 提高组 Day1 中的铺地毯吗?时光飞逝,光阴荏苒,三年过去了。组织者精心准备的颁奖典礼早已结束,留下的则是被人们踩过的地毯。请你来解决类似于铺地毯的另一个问题。

题目描述

会场上有 n 个关键区域,不同的关键区域由 m 条无向地毯彼此连接。每条地毯可由三个整数 u、v、w 表示,其中 u 和 v 为地毯连接的两个关键区域编号,w 为这条地毯的美丽度。

由于颁奖典礼已经结束,铺过的地毯不得不拆除。为了贯彻勤俭节约的原则,组织者被要求只能保留至多 K 条地毯,且保留的地毯构成的图中,任意可互相到达的两点间只能有一种方式互相到达。换言之,组织者要求新图中不能有环。现在组织者求助你,想请你帮忙算出这至多 K 条地毯的美丽度之和最大为多少。

输入格式

第一行包含三个正整数 n、m、K。

接下来 m 行中每行包含三个正整数 u、v、w。

输出格式

只包含一个正整数,表示这 K 条地毯的美丽度之和的最大值。

输入输出样例

输入 #1复制

5 4 3
1 2 10
1 3 9
2 3 7
4 5 3

输出 #1复制

22

说明/提示

选择第 1、2、4 条地毯,美丽度之和为 10 + 9 + 3 = 22。

若选择第 1、2、3 条地毯,虽然美丽度之和可以达到 10 + 9 + 7 = 26,但这将导致关键区域 1、2、3 构成一个环,这是题目中不允许的。

1<=n,m,k<=100000

 这道题也是典型的生成树问题,但这里不是最小生成树,而是求最大生成树,其实解决方法是一样的,只不过在排序的时候用降序排列就可以了

#include<bits/stdc++.h>
using namespace std;
#define N 1000005
int n, m, k, ans, sum;
int pre[N];               //pre数组,并查集
struct node {
	int u, v, w;
}f[N];                 //结构体数组存图

bool cmp(node x, node y)
{
	return x.w > y.w;
}                        //根据边的降序排列
 
int find(int x)          //并查集,查操作
{
	if (pre[x] == x)
		return x;
	return pre[x] = find(pre[x]);
}

void kru()                 //kruskal核心代码段,一样的操作不多赘述
{
	for (int i = 1; i <= m; i++) {
		int x = find(f[i].u);
		int y = find(f[i].v);
		if (x != y)
		{
			pre[x] = y;
			sum += f[i].w;
			ans++;
		}
		if (ans == k)            //退出条件,这里格外要注意退出条件,经常会出错
		{
			cout << sum << endl;
			return;
		}
	}
}
int main()
{
	cin >> n >> m >> k;
	for (int i = 1; i <= m; i++) {
		cin >> f[i].u >> f[i].v >> f[i].w;
	}
	for (int i = 1; i <= n; i++)
		pre[i] = i;                 //pre数组初始化
	sort(f + 1, f + m + 1, cmp);    //根据边降序排列
	kru();
	return 0;
}


四. P1194 买礼物

 https://www.luogu.com.cn/problem/P1194

题目描述

又到了一年一度的明明生日了,明明想要买 B 样东西,巧的是,这 B 样东西价格都是 A 元。

但是,商店老板说最近有促销活动,也就是:

如果你买了第 I 样东西,再买第 J 样,那么就可以只花KI,J​ 元,更巧的是,KI,J​ 竟然等于 KJ,I​。

现在明明想知道,他最少要花多少钱。

输入格式

第一行两个整数,A,B。

接下来 B 行,每行 B 个数,第 I 行第 J 个为 KI,J​。

我们保证 KI,J​=KJ,I​ 并且 KI,I​=0。

特别的,如果KI,J​=0,那么表示这两样东西之间不会导致优惠。

注意 KI,J​ 可能大于 A。

输出格式

一个整数,为最小要花的钱数。

输入输出样例

输入 #1复制

1 1
0

输出 #1复制

1

输入 #2复制

3 3
0 2 4
2 0 2
4 2 0

输出 #2复制

7

说明/提示

样例解释 2。

先买第 2 样东西,花费 3 元,接下来因为优惠,买 1,3 样都只要 2 元,共 7 元。

(同时满足多个“优惠”的时候,聪明的明明当然不会选择用 4 元买剩下那件,而选择用 22 元。)

数据规模

对于 30% 的数据,1≤B≤10。

对于 100% 的数据,01≤B≤500,0≤A,KI,J​≤1000。

2018.7.25新添数据一组

这道题和前面题目的不同之处在于,这里题目没有给我们直接边让我们存图,他这里是类似邻接矩阵的形式,我们要做的就是转换成我们熟悉的边存图,这里我们就通过两重循环将点和边存入结构体数组中,同时完了方便使用,我们从0出发,将初始价格a存入0到n的边中。

#include<bits/stdc++.h>
using namespace std;
#define N 1000005
int n, m, k, ans, sum, s;
int pre[N];           //一样的pre数组
struct node {
	int u, v, w;
}f[N];               //一样的结构体数组存图

bool cmp(node x, node y)
{
	return x.w < y.w;
}                    //一样的比较函数

int find(int x)      //一样的并查集,操作
{
	if (pre[x] == x)
		return x;
	return pre[x] = find(pre[x]);
}

void kru()             //一样的kruskal核心代码段
{
	for (int i = 1; i <= ans; i++)
	{
		int x = find(f[i].u);
		int y = find(f[i].v);
		if (x != y)
		{
			pre[x] = y;
			sum += f[i].w;
			s++;
		}
		if (s == m) {        //一样的退出条件
			cout << sum << endl;
			return;
		}
	}
}
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= m; i++) {       //重点来了,将边一一存入
		for (int j = 1; j <= m; j++) {  
			int a;
			cin >> a;
			if (a != 0) {
				ans++;               //这里就不根据题目来说了,就根据图来理解
				f[ans].u = i;        //起始点
				f[ans].v = j;        //终止点
				f[ans].w = a;        //边
			}
		}
	}
	for (int i = 1; i <= m; i++) {   //从0开始,存入最开始按原价买东西的情况
		ans++;
		f[ans].u = 0;
		f[ans].v = i;
		f[ans].w = n;
	}
	for (int i = 1; i <= m; i++)
		pre[i] = i;
	sort(f+1, f +1+ ans, cmp);      //根据w升序排列
	kru();
	return 0;
}


五.P2872 [USACO07DEC] Building Roads S

https://www.luogu.com.cn/problem/P2872

题目描述

Farmer John had just acquired several new farms! He wants to connect the farms with roads so that he can travel from any farm to any other farm via a sequence of roads; roads already connect some of the farms.

Each of the N (1 ≤ N ≤ 1,000) farms (conveniently numbered 1..N) is represented by a position (Xi, Yi) on the plane (0 ≤ Xi ≤ 1,000,000; 0 ≤ Yi ≤ 1,000,000). Given the preexisting M roads (1 ≤ M ≤ 1,000) as pairs of connected farms, help Farmer John determine the smallest length of additional roads he must build to connect all his farms.

给定 n 个点的坐标,第 i 个点的坐标为 (xi​,yi​),这 n 个点编号为 1 到 n。给定 m 条边,第 i 条边连接第 ui​ 个点和第 vi​ 个点。现在要求你添加一些边,并且能使得任意一点都可以连通其他所有点。求添加的边的总长度的最小值。

输入格式

* Line 1: Two space-separated integers: N and M

* Lines 2..N+1: Two space-separated integers: Xi and Yi

* Lines N+2..N+M+2: Two space-separated integers: i and j, indicating that there is already a road connecting the farm i and farm j.

第一行两个整数 n,m 代表点数与边数。
接下来 n 行每行两个整数 xi​,yi​ 代表第 i 个点的坐标。
接下来 m 行每行两个整数 ui​,vi​ 代表第 i 条边连接第 ui​ 个点和第 vi​ 个点。

输出格式

* Line 1: Smallest length of additional roads required to connect all farms, printed without rounding to two decimal places. Be sure to calculate distances as 64-bit floating point numbers.

一行一个实数代表添加的边的最小长度,要求保留两位小数,为了避免误差, 请用 6464 位实型变量进行计算。

输入输出样例

输入 #1复制

4 1
1 1
3 1
2 3
4 3
1 4

输出 #1复制

4.00

说明/提示

数据规模与约定

对于 100% 的整数,1≤n,m≤1000,1≤xi​,yi​≤106,1≤ui​,vi​≤n。

说明

Translated by 一只书虫仔。

 这题和第一题很像,其实代码也差不多一样,到这里我们发现其实大体操作都是一致的,所以这里我就直接给代码了。

#include <cmath>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
#define N 5000100
int pre[N], xx[N], yy[N], n, m, cnt, sum;
double ans;
struct node {                    //重复的地方我就不写了
	int x, y;
	double dis;
}f[N];

void add(int x, int y, double z)    //通过函数存边,都一样
{
	f[++cnt].x = x;
	f[cnt].y = y;
	f[cnt].dis = z;
}

double d(int x, int y)          //求两点间距离,即边
{
	return (double)(sqrt((double)(xx[x] - xx[y]) * (xx[x] - xx[y]) + (double)(yy[x] - yy[y]) * (yy[x] - yy[y])));
}

bool cmp(node x, node y)
{
	if (x.dis == y.dis)
		return x.x < y.x;
	return x.dis < y.dis;
}

int find(int x)
{
	if (pre[x] == x)
		return x;
	return pre[x] = find(pre[x]);
}

void kruskal()
{
	for (int i = 1; i <= cnt; i++) {
		int dx = find(f[i].x);
		int dy = find(f[i].y);
		if (dx != dy)
		{
			pre[dx] = dy;
			sum++;
			ans += f[i].dis;
		}
	}
}
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> xx[i] >> yy[i];
	}
	for (int i = 1; i <= n; i++) {
		pre[i] = i;
	}
	for (int i = 1; i <= n; i++) {         
		for (int j = i + 1; j <= n; j++) {
			double k = d(i, j);
			add(i, j, k);
		}
	}
	for (int i = 1; i <= m; i++) {     //已经连接的两点,其边相当于0
		int c, d;
		cin >> c >> d;
		add(c, d, 0.0);
	}
	sort(f + 1, f + cnt + 1, cmp);
	kruskal();
	printf("%.2lf\n", ans);
	return 0;
}


通过以上几个使用kruskal算法求最小生成树的例题分析,我们不难发现,通过kruskal算法不仅代码固定,而且比较简单,容易理解。

我们也可以通过prim算法来完成以上的例题,如果有需要我也可以用prim算法重新写一遍。

 OK,本次关于求最小生成树例题分析就结束了,如果对于本篇总结有疑问的欢迎讨论,同时如果有错误或者待修改完善的地方,也希望能够指出,我一定会及时改正,~QVQ~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值