最小生成树
- 子图:从原图中选中一些由节点和边组成的图,称之为原图的子图。
- 生成子图:选中一些由边和所有节点组成的图,称之为原图的生成子图。
- 生成树:如果生成的子图恰好是一棵树,则称之为生成树。
- 最小生成树:权值之和最小的生成树,称之为最小生成树。
- 求解最小生成树的两种求解算法:Prim算法和Kruskal算法。
Prim算法
算法设计
- 初始化。令集合U = {u_0},u_0 \in V,并初始化数组closest[]、lowcost[]和s[]。
- 在集合V-U中找lowcost值最小的节点t,即lowcost[t] = min{lowcost[j]|j \in V-U},满足该公式的节点t就是集合V-U中连续集合U的最邻近点。
- 将节点t加入集合U中。
- 如果集合V-U为空,则算法结束,否则转向步骤5
- 对集合V-U中所有节点j都更新其lowcost[]和closest[]。更新if(C[t][j]<lowcost[j]){lowcost[j] = C[t][j];closest[j] = t;},转向步骤2。
算法实现
#include<iostream>
using namespace std;
const int INF=0x3f3f3f3f;
const int N=100;
bool s[N];//如果s[i]=true,说明顶点i已加入U
int c[N][N],closest[N],lowcost[N];
void Prim(int n); //Prim算法构建最小生成树
int main(){
int n,m,u,v,w;
cin>>n>>m;
int sumcost=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
c[i][j]=INF;
for(int i=1;i<=m;i++){
cin>>u>>v>>w;
c[u][v]=c[v][u]=w;
}
Prim(n);
cout<<"数组lowcost:"<<endl;
for(int i=1;i<=n;i++)
cout<<lowcost[i]<<" ";
cout<<endl;
for(int i=1;i<=n;i++)
sumcost+=lowcost[i];
cout<<"最小的花费:"<<sumcost<<endl;
return 0;
}
void Prim(int n){
s[1]=true; //初始时,集合中U只有一个元素,即顶点1
for(int i=1;i<=n;i++){
if(i!=1){
lowcost[i]=c[1][i];
closest[i]=1;
s[i]=false;
}
else
lowcost[i]=0;
}
for(int i=1;i<n;i++){
int temp=INF;
int t=1;
for(int j=1;j<=n;j++){//在集合中V-u中寻找距离集合U最近的顶点t
if(!s[j]&&lowcost[j]<temp){
t=j;
temp=lowcost[j];
}
}
if(t==1)
break;//找不到t,跳出循环
s[t]=true;//否则,t加入集合U
for(int j=1;j<=n;j++){ //更新lowcost和closest
if(!s[j]&&c[t][j]<lowcost[j]){
lowcost[j]=c[t][j];
closest[j]=t;
}
}
}
}
输入:
7 12
1 2 23
1 6 28
1 7 36
2 3 20
2 7 1
3 4 15
3 7 4
4 5 3
4 7 9
5 6 17
5 7 16
6 7 25
输出:
数组lowcost:
0 23 4 9 3 17 1
最小的花费:57
Kruskal算法
算法步骤
- 初始化。将所有边都按权值从小到大排序,将每个节点的集合号都初始化为自身编号。
- 按排序后的顺序选择权值最小的边(u,v)。
- 如果节点u和v属于两个不同的连通分支,则将边(u,v)加入边集TE中,并将两个连通分支合并。
- 如果选取的边数小于n-1,则转向步骤2,否则算法结束。
算法实现
#include<iostream>
#include<algorithm>
using namespace std;
const int N=100;
int fa[N];
int n,m;
struct Edge{
int u,v,w;
}e[N*N];
bool cmp(Edge x, Edge y); //比较函数
void Init(int n); //初始化集合号为自身
int Merge(int a,int b); //合并
int Kruskal(int n); //求最小生成树
int main(){
cin>>n>>m;
Init(n);
for(int i=1;i<=m;i++)
cin>>e[i].u>>e[i].v>>e[i].w;
cout<<"最小的花费是:"<<Kruskal(n)<<endl;
return 0;
}
bool cmp(Edge x, Edge y){
return x.w<y.w;
}
void Init(int n){//初始化集合号为自身
for(int i=1;i<=n;i++)
fa[i]=i;
}
int Merge(int a,int b){//合并
int p=fa[a];
int q=fa[b];
if(p==q) return 0;
for(int i=1;i<=n;i++){//检查所有结点,把集合号是q的改为p
if(fa[i]==q)
fa[i]=p;//a的集合号赋值给b集合号
}
return 1;
}
int Kruskal(int n){//求最小生成树
int ans=0;
sort(e,e+m,cmp);
for(int i=0;i<m;i++)
if(Merge(e[i].u,e[i].v)){
ans+=e[i].w;
n--;
if(n==1)//n-1次合并算法结束
return ans;
}
return 0;
}
输入:
7 12
1 2 23
1 6 28
1 7 36
2 3 20
2 7 1
3 4 15
3 7 4
4 5 3
4 7 9
5 6 17
5 7 16
6 7 25
输出:
最小的花费是:57
算法优化
- 如果使用并查集优化合并操作,则每次合并的时间复杂度都为O(logn)。
#include<iostream>
#include<algorithm>
using namespace std;
const int N=100;
int fa[N];
int n,m;
struct Edge{
int u,v,w;
}e[N*N];
void Init(int n); //初始化集合号为自身
int Find(int x); //找祖宗
bool Merge(int a,int b); //合并
int Kruskal(int n); //最小生成树
int main(){
cin>>n>>m;
Init(n);
for(int i=1;i<=m;i++)
cin>>e[i].u>>e[i].v>>e[i].w;
cout<<"最小的花费:"<<Kruskal(n)<<endl;
return 0;
}
bool cmp(Edge x,Edge y){
return x.w<y.w;
}
void Init(int n){
for(int i=1;i<=n;i++)
fa[i]=i;
}
int Find(int x){
if(x!=fa[x])
fa[x]=Find(fa[x]);
return fa[x];
}
bool Merge(int a,int b){
int p=Find(a);
int q=Find(b);
if(p==q) return 0;
fa[q]=p;
return 1;
}
int Kruskal(int n){
int ans=0;
sort(e,e+m,cmp);
for(int i=0;i<m;i++)
if(Merge(e[i].u,e[i].v)){
ans+=e[i].w;
n--;
if(n==1)
return ans;
}
return 0;
}
输入:
7 12
1 2 23
1 6 28
1 7 36
2 3 20
2 7 1
3 4 15
3 7 4
4 5 3
4 7 9
5 6 17
5 7 16
6 7 25
输出:
最小的花费是:57
训练1:丛林之路
题目描述
丛林道路网络的维护费用太高,理事会必须选择停止维护一些道路。如下图所示,在地图中,村庄被标记为A~I。左边的地图显示了现在所有道路及每月的维护费用,每月可以用最少的费用维护一些道路,保证所有村庄都是连通的。右边的地图显示了最便宜的道路维护方案,每月的维护总费用为216元。
输入:输入由1~100个数据集组成,最后一行只包含0.每个数据集的第1行都为数字n(1<n<27),表示村庄的数量,对村庄使用字母表的前n个大写字母标记。每个数据集都有n-1行描述,这些行的村庄标签按字母顺序排序。最后一个村庄没有道路。村庄的每条道路都以村庄标签开头,后面跟着一个从这个村庄到后面村庄的道路数k。如果k>0,则改行后面包含k条道路的数据。每条道路的水机都是道路另一端的村庄标签,后面是道路的每月维护成本。维护费用是小于100 的正整数,道路是用户量不会超过75条,每个村庄通往其他村庄的道路都不超过15条
输出:对于每个数据集,都单行输出每月维护连接所有村庄的道路的最低费用。
算法设计
- 使用Prim或Kruskal算法求最小生成树。需要注意的是,在数据的输入格式方面,A 2 B 12 I 25表示A关联两条边,包括A-B的边(边权为12)及A-I的边(边权为25)。
Prim方法
#include<iostream>
#include<cstring>
using namespace std;
int m[30][30],dis[30];
bool vis[30];
int n;
int prim(int s); //Prim算法求最小生成树
int main(){
while(cin>>n&&n){
int num,w;
char c;
memset(m,0x3f,sizeof(m)); //初始化权值数组
for(int i=1;i<n;i++){
cin>>c>>num;
int u=c-'A'; //将村庄标记转换为数值
while(num--){
cin>>c>>w; //记录村庄子节点
int v=c-'A';
if(w<m[u][v])
m[u][v]=m[v][u]=w;
}
}
cout<<prim(0)<<endl;
}
return 0;
}
int prim(int s){
for(int i=0;i<n;i++)
dis[i]=m[s][i];//初始化权值数组
memset(vis,false,sizeof(vis));
vis[s]=1;//标记已查找
int sum=0;
int t;
for(int i=1;i<n;i++){
int min=0x3f3f3f3f;
for(int j=0;j<n;j++){//找最小
if(!vis[j]&&dis[j]<min){
min=dis[j];
t=j;
}
}
sum+=min;
vis[t]=1;
for(int j=0;j<n;j++){//更新
if(!vis[j]&&dis[j]>m[t][j])
dis[j]=m[t][j];
}
}
return sum;
}
输入:
9
A 2 B 12 I 25
B 3 C 10 H 40 I 8
C 2 D 18 G 55
D 1 E 44
E 2 F 60 G 38
F 0
G 1 H 35
H 1 I 35
3
A 2 B 10 C 40
B 1 C 20
0
输出:
216
30
Kruskal方法
#include<iostream>
#include<algorithm>
using namespace std;
const int N=100;
int fa[N];
int n,m = 0;
struct Edge{
int u,v,w;
}e[N*N];
void Init(int n); //初始化集合号为自身
int Find(int x); //找祖宗
bool Merge(int a,int b); //合并
int Kruskal(int n); //最小生成树
int main() {
while (cin >> n && n) {
Init(n);
int num, w;
char c;
for (int i = 1; i < n; i++) {
cin >> c >> num;
int u = c - 'A'; //将村庄标记转换为数值
m = m + num;
while (num--) {
cin >> c >> w; //记录村庄子节点
int v = c - 'A';
e[i].u = u;
e[i].v = v;
e[i].w = w;
}
}
cout << Kruskal(n) << endl;
}
return 0;
}
bool cmp(Edge x,Edge y){
return x.w<y.w;
}
void Init(int n){
for(int i=1;i<=n;i++)
fa[i]=i;
}
int Find(int x){
if(x!=fa[x])
fa[x]=Find(fa[x]);
return fa[x];
}
bool Merge(int a,int b){
int p=Find(a);
int q=Find(b);
if(p==q) return 0;
fa[q]=p;
return 1;
}
int Kruskal(int n){
int ans=0;
sort(e,e+m,cmp);
for(int i=0;i<m;i++)
if(Merge(e[i].u,e[i].v)){
ans+=e[i].w;
n--;
if(n==1)
return ans;
}
return 0;
}
输入:
9
A 2 B 12 I 25
B 3 C 10 H 40 I 8
C 2 D 18 G 55
D 1 E 44
E 2 F 60 G 38
F 0
G 1 H 35
H 1 I 35
3
A 2 B 10 C 40
B 1 C 20
0
输出:
216
30
训练2:联网
题目描述
已知该区域中的一组点,以及两点之间每条路线所需的电缆长度。请注意,在两个给定点之间可能存在许多路线。假设给定的可能路线(直接或间接)连接该区域中的每两个点,请设计网络,使每两个点之间都存在连接(直接或间接),并且使用的电缆总长度最小。
输入:输入由多个数据集组成,每个数据集都描述一个网络。数据集的第1行包含两个整数:第1个整数表示点数P(P ≤ \leq ≤ 50),节点标号为1~P;第2个整数表示点之间的路线数R。以下R行为点之间的路线,每条路线都包括3个整数:前两个正整数为点标号,第3个整数为路线长度L(L ≤ \leq ≤ 100)。数据集之间以空行分隔,输入仅有一个数字P(P = 0)的数据集,表示输入结束。
输出:对于每个数据集,都单行输出所涉及网络的电缆的最小总长度。
算法实现
#include<iostream>
#include<algorithm>
using namespace std;
int fa[55],n,m,cnt;
struct node{
int u,v,cost;
}edge[3000];
bool cmp(node x,node y){//定义排序优先级
return x.cost<y.cost;//按权值升序
}
void add(int a,int b,int c){
edge[cnt].u=a;
edge[cnt].v=b;
edge[cnt++].cost=c;
}
int find(int x){//并查集找祖宗
return fa[x]==x?x:fa[x]=find(fa[x]);
}
bool merge(int a,int b){//集合合并
int x=find(a);
int y=find(b);
if(x==y) return 0;
fa[y]=x;
return 1;
}
int kruskal(){
int sum=0;
sort(edge,edge+m,cmp);
for(int i=0;i<m;i++){
if(merge(edge[i].u,edge[i].v)){
sum+=edge[i].cost;
if(--n==1)
return sum;
}
}
return 0;
}
int main(){
int x,y,z;
while(cin>>n&&n){
cnt=0;
cin>>m;
for(int i=1;i<=n;i++)
fa[i]=i;
for(int i=0;i<m;i++){
cin>>x>>y>>z;
add(x,y,z);
}
cout<<kruskal()<<endl;
}
return 0;
}
输入:
1 0
2 3
1 2 37
2 1 17
1 2 68
3 7
1 2 19
2 3 11
3 1 7
1 3 5
2 3 89
3 1 91
1 2 32
5 7
1 2 5
2 3 7
2 4 8
4 5 11
3 5 10
1 5 6
4 2 12
0
输出:
0
17
16
26
训练3:空间站
题目描述
空间站由许多单元组成,所有单元都是球形的,在该站成功进入其轨道后不久,每个单元都固定在其预定的位置。两个单元可能彼此接触,甚至重叠。在极端情况下,一单元可能完全包围另一个单元。所有单元都必须连接,因为机组成员应该能够从任何单元走到任何其他单元。如果存在下面三种情况,则可以从单元A走到另一个单元B:
-
A和B相互接触或重叠;
-
A和B通过“走廊”连接;
-
有一个单元C,从A到C,且从B到C是可能的(传递)。
需要设计一种配置,看看用走廊连接哪些单元可以使整个空间站连通。建造走量的成本与其长度成正比。因此选择走廊总长度最短的计划。
输入:输入由多个数据集组成。每个数据集的第1行都包含一个整数 n ( 0 < n ≤ 100 ) n(0 < n \leq 100) n(0<n≤100),表示单元的数量。以下n行是对单元的描述,其中每一行都包含4个值,表示球体的中心坐标x、y和z,以及球体的半径r,每个值都为小数(小数点后3位)。x、y、z和r均为正数且小于100.0。输入的结尾由包含0的行表示。
输出:对于每个数据集,都单行输出建造走廊的最短总长度(小数点后3位)
注意:如果不需要建造走廊,则走廊的最短总长度为0.000。
算法设计
- 计算任意两个单元之间的距离,如果两个单元有接触或重叠,则距离为0.000
- 采用Prim算法求解最小生成树
- 输出最小生成树的权值之和
算法实现
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
const int maxn=105;
const double inf=0x3f3f3f3f;//类型double
double m[maxn][maxn],low[maxn];
bool vis[maxn];
int n;
struct cell{
double x,y,z,r;//球形单元的圆心,半径
}c[maxn];
double clu(cell c1,cell c2);//计算两个球单元的距离
double prim(int s);//最小生成树
int main(){
while(cin>>n&&n){
//memset(m,0x3f,sizeof(m));//不可以对浮点数赋值
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
if(i=j)
m[i][j]=0;
else
m[i][j]=inf;
for(int i=0;i<n;i++)
cin>>c[i].x>>c[i].y>>c[i].z>>c[i].r;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
if(i!=j)
m[i][j]=m[j][i]=clu(c[i],c[j]);
printf("%.3lf\n",prim(0));
}
return 0;
}
double clu(cell c1,cell c2){//计算两个球单元的距离
double x=(c1.x-c2.x)*(c1.x-c2.x);
double y=(c1.y-c2.y)*(c1.y-c2.y);
double z=(c1.z-c2.z)*(c1.z-c2.z);
double d=sqrt(x+y+z);
if(d-c1.r-c2.r<=0)
return 0.000;
else
return d-c1.r-c2.r;
}
double prim(int s){//返回值类型double
for(int i=0;i<n;i++)
low[i]=m[s][i];
memset(vis,false,sizeof(vis));
vis[s]=1;
double sum=0.000;
int t;
for(int i=1;i<n;i++){//执行n-1次
double min=inf;
for(int j=0;j<n;j++){//找最小
if(!vis[j]&&low[j]<min){
min=low[j];
t=j;
}
}
sum+=min;
vis[t]=1;
for(int j=0;j<n;j++){//更新
if(!vis[j]&&low[j]>m[t][j])
low[j]=m[t][j];
}
}
return sum;
}
输入:
3
10.000 10.000 50.000 10.000
40.000 10.000 50.000 10.000
40.000 40.000 50.000 10.000
2
30.000 30.000 30.000 20.000
40.000 40.000 40.000 20.000
5
5.729 15.143 3.996 25.837
6.013 14.372 4.818 10.671
80.115 63.292 84.477 15.120
64.095 80.924 70.029 14.881
39.472 85.116 71.369 5.553
0
输出:
30.000
0.000
73.834
训练4:道路建设
题目描述
有N个村庄,编号为1~N,需要建造一些道路,使每两个村庄之间都可以相互连接。两个村庄A和B是相连的,当且仅当A和B之间有一条道路,或者存在一个村庄C,A和C相连且C和B相连。已知一些村庄之间已经有一些道路,你的工作是修键一些道路,使所有村庄都连通起来,所有道路的长度之和最小。
输入:第1行是整数N( 3 ≤ N ≤ 100 3 \leq N \leq 100 3≤N≤100),表示村庄的数量;然后是N行,其中第i行包含N个整数,第j个整数表示村庄i和村庄j之间的距离(距离为[1,1000]内的整数);接着是整数Q ( 0 ≤ Q ≤ N × ( N + 1 ) / 2 ) (0 \leq Q \leq N \times (N+1)/2) (0≤Q≤N×(N+1)/2),表示已建成道路的数量;最后是Q行,每行都包含两个整数a和b ( 1 ≤ a < b ≤ N ) (1 \leq a < b \leq N) (1≤a<b≤N),表示村庄a和村庄b之间的道路已经建成。
输出:单行输出需要构建的所有道路的最小长度。
算法设计
- 采用Prim算法求最小生成树。
算法实现
#include<iostream>
#include<cstring>
using namespace std;
const int maxn=105;
const int inf=0x3f3f3f3f;
double m[maxn][maxn],low[maxn];
bool vis[maxn];
int n;
int prim(int s); //prim算法求最小生成树
int main(){
int q,a,b;
while(cin>>n){
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
cin>>m[i][j];
cin>>q;
while(q--){
cin>>a>>b;
m[a][b]=m[b][a]=0;
}
cout<<prim(1)<<endl;
}
return 0;
}
int prim(int s){
memset(vis,false,sizeof(vis));
for(int i=1;i<=n;i++)
low[i]=m[s][i];
vis[s]=1;
int sum=0;
int t;
for(int i=1;i<n;i++){//执行n-1次
int min=inf;
for(int j=1;j<=n;j++){//找最小
if(!vis[j]&&low[j]<min){
min=low[j];
t=j;
}
}
sum+=min;
vis[t]=1;
for(int j=1;j<=n;j++){//更新
if(!vis[j]&&low[j]>m[t][j])
low[j]=m[t][j];
}
}
return sum;
}
输出:
3
0 990 692
990 0 179
692 179 0
1
1 2
输入:
179