算法日记:最小生成树

最小生成树


一、什么是最小生成树?

给定一个带权无向图G(V,E)n=|V|,m=|E|。由V中全部n个顶点还有En-1条边构成的无向连通子图称为G的一颗生成树。边的权值之和最小的生成树被称为无向图G的最小生成树


二、最小生成树求解方法

1.Kruskal算法

定理

任意一颗最小生成树一定包含这个无向图中权值最小的边

证明:

假设无向图G存在一颗最小生成树不包含无线图中权值最小的边。设e(x,y,z)是无向图中权值最小的边,那么把这条边加入假设的最小生成树一定会构成一个环,而这个环中的任意一条边权值都大于e(x,y,z)这条边的权值,所以用这条边替换这个环中任意一条边会形成一个权值更小的生成树,与假设矛盾。所以无向图G存在一棵最小生成树一定包含这个无向图权值最小的边。

算法流程
Kruskal算法是通过不断选择有效的边构成最小生成树,在任意时刻Kruskal算法从剩余的边中选出权值最小的边,且这条边的两个节点不连通,把该边加入答案集,图中端点的连通情况可以用并查集维护。

1.建立并查集,每个节点各自一个集合
2.将所有边按照权值大小排序
3.扫描所有的边**(x,y,z)如果xy在同一个连通块中就忽略这条边,继续扫描下一条边直到扫描到xy不在同一个连通块中
4.找到这样的边之后合并
x,y所在的集合
5.所有的边扫描完成后,第四部中处理过的边就构成最小生成树
算法复杂度
O(m logm)**

代码如下(示例):

#include<bits/stdc++.h>
using namespace std;
int fa[100005];//并查集维护数组
int find(int x) {//查找x所在的树的根节点
	return x==fa[x]?x:(fa[x]=find(fa[x]));
}
struct node {
	int x,y,z;
} edge[100005];//边集合
bool operator<(node a,node b) {//用于排序
	return a.z<b.z;
}
int main() {
	int n,m;//n为点的数量,m为边的数量
	int ans=0;
	cin>>n>>m;
	for(int i=1; i<=m; i++) {//输入所有边
		int x,y,z;
		cin>>x>>y>>z;
		edge[i].x=x;
		edge[i].y=y;
		edge[i].z=z;
	}
	sort(edge+1,edge+1+m);//将所有边按从小到大打排序
	for(int i=1; i<=n; i++)fa[i]=i;//所有节点一开始的根节点都是自己
	for(int i=1; i<=m; i++) {//Kruskal算法核心
		int x=find(edge[i].x);
		int y=find(edge[i].y);
		if(x==y)//判断x==y则表示点x与点连通
			continue;
		//不连通则将e(edge[i].x,edge[i].y,edge[i].z)加入最小生成树
		fa[x]=y;
		ans+=edge[i].z;
	}
	cout<<ans<<endl;//ans为最小生成树权值和
	return 0;
}

2.Prim算法

算法流程
Prim算法通过不断加入点来构成最小生成树,最初只有1号节点属于最小生成树,在任意时刻确定已经属于最小生成树的点的集合为T,部署与最小生成树的点的集合为SPrim算法找到Minx∈T,y∈S{z},也就是两个端点分别属于集合TS的权值最小的边,然后把y点从集合S中删除加入集合T,并把z累积到答案中。

具体来说可以维护数组d:如果x∈S那么d[x]表示为x到集合T中节点权值最小的边的权值。如果x∈Td[x]表示x点被加入最小生成树时所选的边的权值大小。

可以类比Dijkstra算法,用一个数组标记节点是否属于集合T。一开始因为T集合中只有1号节点,所有的d[x]均表示为x点与1点的一条边的距离,没有边则表示为无穷大。每次从未标记的节点且d值最小的节点加入T集合,再用新加入的点去更新所有节点的d值,也就是Dijkstra里面松弛最短路的操作,假设新加入的点为p,对于松弛操作就是对于任意一个节点i如果 d[i] > w [i] [p] 那么 d[i] = w [i] [p]
算法复杂度为O(n^2)
代码如下(示例):

#include<bits/stdc++.h>
using namespace std;
int a[105][105];//邻接矩阵存图
int d[105];//d[x]如果x不在T集合中表示x点到T集合中节点权值最小的一条边的权值
bool vis[105];//vis[i]=1表示节点在当前最小生成树集合T中,为0则表示在集合S中
int n,m;//节点数n边数m
void Prim() {
	memset(vis,0,sizeof(vis));
	memset(d,0x3f,sizeof(d));
	d[1]=0;
	for(int i=1; i<n; i++) { //第一次循环确定1号节点在T中,接下来每次循环确定一个节点,最后一次循环确定一个节点后剩下的那个节点自动也被确定下来,所以n-1次循环即可
		int x=0;//记录当前找到的与T集合中的节点距离最近的节点
		for(int j=1; j<=n; j++) //更新x
			if(!vis[j]&&(x==0||d[j]<d[x]))x=j;
		vis[x]=1;
		for(int y=1; y<=n; y++) //用找到的节点更新其他节点最短路
			if(!vis[y])d[y]=min(d[y],a[y][x]);
	}
}
int main() {
	int ans=0;
	memset(a,0x3f,sizeof(a));

	cin>>n>>m;
	for(int i=1; i<=m; i++) {
		int x,y,z;
		cin>>x>>y>>z;
		a[x][y]=a[y][x]=min(a[x][y],z);
	}
	Prim();
	for(int i=1; i<=n; i++)ans+=d[i];
	cout<<ans<<endl;//最小生成树边权和
	return 0;
}

三、例题

P2820 局域网

题目背景

某个局域网内有n(n<=100)台计算机,由于搭建局域网时工作人员的疏忽,现在局域网内的连接形成了回路,我们知道如果局域网形成回路那么数据将不停的在回路内传输,造成网络卡的现象。因为连接计算机的网线本身不同,所以有一些连线不是很畅通,我们用f(i,j)表示i,j之间连接的畅通程度,f(i,j)值越小表示i,j之间连接越通畅,f(i,j)为0表示i,j之间无网线连接。

题目描述
需要解决回路问题,我们将除去一些连线,使得网络中没有回路,并且被除去网线的**Σf(i,j)**最大,请求出这个最大值。

输入格式
第一行两个正整数n, k

接下来的k行每行三个正整数i ,j, m表示i,j两台计算机之间有网线联通,通畅程度为m

输出格式
一个正整数,Σf(i,j)的最大值

输入输出样例
输入 #1

5 5
1 2 8
1 3 1
1 5 3
2 4 5
3 4 2

输出 #1
8

说明/提示
f(i,j)<=1000

参考代码

//Kruskal
#include<bits/stdc++.h>
using namespace std;
int fa[105];
int find(int x) {
	return x==fa[x]?x:(fa[x]=find(fa[x]));
}
struct node {
	int x,y,z;
} edge[100005];
bool operator<(node a,node b) {
	return a.z<b.z;
}
int main() {
	int n,m;
	int sum=0;
	int ans=0;
	cin>>n>>m;
	for(int i=1; i<=m; i++) {
		int x,y,z;
		cin>>x>>y>>z;
		edge[i].x=x;
		edge[i].y=y;
		edge[i].z=z;
		sum+=z;
	}
	sort(edge+1,edge+1+m);
	for(int i=1; i<=n; i++)fa[i]=i;
	for(int i=1; i<=m; i++) {
		int x=find(edge[i].x);
		int y=find(edge[i].y);
		if(x==y)
			continue;
		fa[x]=y;
		ans+=edge[i].z;
	}
	cout<<sum-ans<<endl;
	return 0;
}**
//Prim
#include<bits/stdc++.h>
using namespace std;
int a[105][105];
int d[105];
bool vis[105];
int n,m;
void Prim() {
	memset(vis,0,sizeof(vis));
	memset(d,0x3f,sizeof(d));
	d[1]=0;
	for(int i=1; i<n; i++) {
		int x=0;
		for(int j=1; j<=n; j++)
			if(!vis[j]&&(x==0||d[j]<d[x]))x=j;
		vis[x]=1;
		for(int y=1; y<=n; y++)
			if(!vis[y])d[y]=min(d[y],a[y][x]);
	}
}
int main() {
	int sum=0;
	int ans=0;
	memset(a,0x3f,sizeof(a));

	cin>>n>>m;
	for(int i=1; i<=m; i++) {
		int x,y,z;
		cin>>x>>y>>z;
		a[x][y]=a[y][x]=min(a[x][y],z);
		sum+=z;
	}
	Prim();
	for(int i=1; i<=n; i++)ans+=d[i];
	cout<<sum-ans<<endl;
	return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值