算法训练营 图的应用(最小生成树)

最小生成树

  • 子图:从原图中选中一些由节点和边组成的图,称之为原图的子图。
  • 生成子图:选中一些由边和所有节点组成的图,称之为原图的生成子图。
  • 生成树:如果生成的子图恰好是一棵树,则称之为生成树。
  • 最小生成树:权值之和最小的生成树,称之为最小生成树。
  • 求解最小生成树的两种求解算法:Prim算法和Kruskal算法。

Prim算法

算法设计

  1. 初始化。令集合U = {u_0},u_0 \in V,并初始化数组closest[]、lowcost[]和s[]。
  2. 在集合V-U中找lowcost值最小的节点t,即lowcost[t] = min{lowcost[j]|j \in V-U},满足该公式的节点t就是集合V-U中连续集合U的最邻近点。
  3. 将节点t加入集合U中。
  4. 如果集合V-U为空,则算法结束,否则转向步骤5
  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算法

算法步骤

  1. 初始化。将所有边都按权值从小到大排序,将每个节点的集合号都初始化为自身编号。
  2. 按排序后的顺序选择权值最小的边(u,v)。
  3. 如果节点u和v属于两个不同的连通分支,则将边(u,v)加入边集TE中,并将两个连通分支合并。
  4. 如果选取的边数小于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:

  1. A和B相互接触或重叠;

  2. A和B通过“走廊”连接;

  3. 有一个单元C,从A到C,且从B到C是可能的(传递)。

需要设计一种配置,看看用走廊连接哪些单元可以使整个空间站连通。建造走量的成本与其长度成正比。因此选择走廊总长度最短的计划。

输入:输入由多个数据集组成。每个数据集的第1行都包含一个整数 n ( 0 < n ≤ 100 ) n(0 < n \leq 100) n(0<n100),表示单元的数量。以下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 3N100),表示村庄的数量;然后是N行,其中第i行包含N个整数,第j个整数表示村庄i和村庄j之间的距离(距离为[1,1000]内的整数);接着是整数Q ( 0 ≤ Q ≤ N × ( N + 1 ) / 2 ) (0 \leq Q \leq N \times (N+1)/2) (0QN×(N+1)/2),表示已建成道路的数量;最后是Q行,每行都包含两个整数a和b ( 1 ≤ a < b ≤ N ) (1 \leq a < b \leq N) (1a<bN),表示村庄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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

羽星_s

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值