Prim算法的思路
编写Prim函数
这个代码是在图的邻接矩阵(无项、有权)的代码的基础上,添加了Prim最小生成树的函数,并且修改测试用例和主函数代码,图的邻接矩阵(无项、有权)的代码具体请查看 【C语言\数据结构】图之邻接矩阵(无向、有权)代码简单实现,这里就不过多赘述。
方法一
void prim2(graph g,int x){
int judgment[MAX]={0};
int path[MAX];
memset(path,-1,sizeof(path));
judgment[x]=1;
int sum=0;
int n=g.vexnum-1;
while(n--){
int min=INT_MAX,start,end;
for(int i=1;i<=g.vexnum;i++){
if(judgment[i]==1){
for(int j=1;j<=g.vexnum;j++){
if(i!=j&&judgment[j]==0&&g.arcs[i][j]<min){
min=g.arcs[i][j];
start=i;
end=j;
}
}
}
}
path[end]=start;
judgment[end]=1;
sum+=g.arcs[start][end];
}
printf("最小生成树的权值和:\n%d\n",sum);
printf("最小生成树连接情况:\n");
for(int i=1;i<=g.vexnum;i++){
if(path[i]!=-1){
printf("%d---%d\n",path[i],i);
}
}
}
int judgment[MAX]={0};
int path[MAX];
memset(path,-1,sizeof(path));
创建judgment数组,所有元素置0,judgment[i]=0表示顶点i在非树集合中,judgment[i]=1表示顶点i在树集合中。
创建path数组,所有元素置-1,path[i]=n表示顶点i的前驱顶点是n。
memset是计算机中C/C++语言初始化函数。作用是将某一块内存中的内容全部设置为指定的值, 这个函数通常为新申请的内存做初始化工作。
void *memset(void *s, int ch, size_t n);
函数解释:将s中当前位置后面的n个字节 用 ch 替换并返回 s 。
memset函数返回值是s指针,函数的调用不一定需要接收返回值,无返回值接收,只让memset初始化数据。
memset(path,-1,sizeof(path));
将path数组首地址后面字节大小为sizeof(path)的数据用-1替换。
第一个元素传入需要初始化的数组首地址,第二个元素传入需要替换进去的数据,第三个元素传入该数组的字节大小。
judgment[x]=1;
int sum=0;
prim函数中传入的x是一个顶点的下标,表示最开始的树,从它开始创建。
把顶点x添加进树的集合中,judgment[x]的值维护成1。
sum是这颗最小生成树的权值和,初始化为0。
int n=g.vexnum-1;
while(n--){
int min=INT_MAX, start, end;
for(int i=1;i<=g.vexnum;i++){
if(judgment[i]==1){
for(int j=1;j<=g.vexnum;j++){
if(i!=j&&judgment[j]==0&&g.arcs[i][j]<min){
min=g.arcs[i][j];
start=i;
end=j;
}
}
}
}
path[end]=start;
judgment[end]=1;
sum+=g.arcs[start][end];
}
while每循环一次,一个非树集合的元素就添加到树集合中,所以while需要循环 (顶点数-1)次。
每一次循环,寻找非树集合元素与树集合元素相连的权值最小的边。
定义min存储最小的权值,start和end表示最小权值边的起点和终点。
for(int i=1;i<=g.vexnum;i++){
if(judgment[i]==1){
for(int j=1;j<=g.vexnum;j++){
if(i!=j&&judgment[j]==0&&g.arcs[i][j]<min){
min=g.arcs[i][j];
start=i;
end=j;
}
}
}
}
遍历每一个顶点,如果这个顶点是树集合中的元素,就找与之相连的非树集合元素的最小权值边。
注意,start=i,end=j不能交换顺序,start必须是树集合中的元素,end必须是非树集合中的元素。
path[end]=start;
judgment[end]=1;
sum+=g.arcs[start][end];
不能交换顺序的原因是,建树的过程是从单个节点开始,一步一步添加节点,每添加一个节点,就记录该节点的前驱结点,而后面代码是path[end]=start,前后代码必须统一,且用start表示树集合元素,end表示非树集合元素,更加直观。
printf("最小生成树的权值和:\n%d\n",sum);
printf("最小生成树连接情况:\n");
for(int i=1;i<=g.vexnum;i++){
if(path[i]!=-1){
printf("%d---%d\n",path[i],i);
}
}
最后输出最小生成树的权值和sum,并打印生成树的连接情况。
遍历每一个顶点,如果该顶点有前驱顶点,就打印它的前驱顶点和该顶点。
注意,path数组只能存储前序,而不能存储后继,因为一棵树的节点,前驱是唯一的,后继可能不唯一。
方法二
void prim1(graph g,int x){
int dist[MAX],judgment[MAX];
int sum=0;
for(int i=1;i<=g.vexnum;i++){
judgment[i]=0;
}
for(int i=1;i<=g.vexnum;i++){
dist[i]=g.arcs[x][i];
}
judgment[x]=1;
for(int i=2;i<=g.vexnum;i++){
int min=INT_MAX;
int min_index=0;
for(int j=1;j<=g.vexnum;j++){
if(dist[j]<min&&judgment[j]==0){
min=dist[j];
min_index=j;
}
}
judgment[min_index]=1;
for(int j=1;j<=g.vexnum;j++){
if(dist[j]>g.arcs[min_index][j]){
dist[j]=g.arcs[min_index][j];
}
}
sum+=min;
}
printf("最小生成树的权值和:%d\n",sum);
}
int dist[MAX],judgment[MAX];
int sum=0;
定义dist数组,dist[i]表示非树集合中的顶点i与树集合中的最小交叉边长度。
顶点i与树集合中一部分元素都有交叉边,这些交叉边构成一个集合,而这些集合中最小交叉边长度是dist[i],我们称作集合的最小交叉边。
注意,顶点i表示非树集合的元素,dist[i]表示这个顶点与树集合中某一顶点的最小交叉边的长度,我们简称为 i集合的最小交叉边。
定义judgment数组,用来分隔非树集合与树集合。
定义sum用来存储最小生成树的权值和。
for(int i=1;i<=g.vexnum;i++){
judgment[i]=0;
}
for(int i=1;i<=g.vexnum;i++){
dist[i]=g.arcs[x][i];
}
judgment[x]=1;
把judgment中所有元素置0,接着把顶点x添加到树集合中,修正所有非树集合元素与树集合的最小交叉边的长度。
接着修正树集合中顶点x的judgment。
for(int i=2;i<=g.vexnum;i++){
int min=INT_MAX;
int min_index=0;
for(int j=1;j<=g.vexnum;j++){
if(dist[j]<min&&judgment[j]==0){
min=dist[j];
min_index=j;
}
}
judgment[min_index]=1;
for(int j=1;j<=g.vexnum;j++){
if(dist[j]>g.arcs[min_index][j]){
dist[j]=g.arcs[min_index][j];
}
}
sum+=min;
}
最外层的for循环只控制循环的次数,每一次循环把非树集合中的元素添加到树集合中。
min和min_index分别表示最小的交叉边长度和这条边非树集合中的顶点下标。
for(int j=1;j<=g.vexnum;j++){
if(dist[j]<min&&judgment[j]==0){
min=dist[j];
min_index=j;
}
}
遍历所有的非树集合元素,如果它对应的集合的最小交叉边是所有集合的最小交叉边最小的,那他就是最小交叉边。
judgment[min_index]=1;
for(int j=1;j<=g.vexnum;j++){
if(dist[j]>g.arcs[min_index][j]){
dist[j]=g.arcs[min_index][j];
}
}
sum+=min;
找到这个最小交叉边后,把非树集合中的min_index添加到树集合中。修正对应的judgment值。
接着遍历所有顶点,由新添加的顶点min_index出发,生成的新的交叉边,是否会使原来的非树集合元素对应集合的最小交叉边更小?如果会变得更小就修正。
注意,这里是遍历所有的元素,这样做对我们的程序是没有影响的,因为我们找最小交叉边的时候,遍历的是非树集合。
接着把这个最小交叉边的长度添加到sum中。
printf("最小生成树的权值和:%d\n",sum);
最后打印sum的值就可以了。
第二种方法利用dist一个一维数组存储了非树集合中每一个元素与树集合中集合的最小交叉边。使时间复杂度从O(n^3)变成了O(n^2),从效率上,第二种方法是更好的。但是第二种方法没有存储前驱顶点的功能,能不能保证O(n^2)的前提下,依旧实现树结构的展现呢?只要让dist[i]能够表示非树集合顶点i的前驱顶点就可以了,定义一个顶点的结构体,结构体里面有两个成员,分别是树集合顶点,权值,用空间换时间。
这样定义的dist[i].weight 就表示非树集合顶点i到树集合tree的交叉边权值。
感兴趣的小伙伴可以去实现一下这种方法的树结构的展示。
完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#define MAX 100
#define INFINITY 9999
enum graphType {DG, UG, DN, UN}; //图的类型定义:有向图,无向图,有向网,无项网
typedef char vertexType;
typedef struct {
vertexType vexs[MAX];
int arcs[MAX][MAX];
int vexnum, arcnum;
graphType kind;
} graph;
void initGraph(graph &g) {
g.kind = UN;
printf("输入顶点数和边数:\n");
scanf("%d%d", &g.vexnum, &g.arcnum);
for (int i = 1; i <= g.vexnum; i++) {
g.vexs[i] = i;
}
for (int i = 1; i <= g.vexnum; i++) {
for (int j = 1; j <= g.vexnum; j++) {
if (i == j) g.arcs[i][j] = 0;
else g.arcs[i][j] = INFINITY;
}
}
}
void createGraph(graph &g) {
int start_index, end_index, weight;
printf("输入每条边的起点终点下标和边的权重:\n");
for (int i = 1; i <= g.arcnum; i++) {
scanf("%d%d%d", &start_index, &end_index, &weight);
g.arcs[start_index][end_index] = weight;
g.arcs[end_index][start_index] = weight;
}
}
void showGraph(graph &g) {
printf("邻接矩阵:\n");
for (int i = 1; i <= g.vexnum; i++) {
for (int j = 1; j <= g.vexnum; j++) {
printf("%d ", g.arcs[i][j]);
}
printf("\n");
}
}
void prim1(graph g,int x){
int dist[MAX],judgment[MAX];
int sum=0;
for(int i=1;i<=g.vexnum;i++){
judgment[i]=0;
}
for(int i=1;i<=g.vexnum;i++){
dist[i]=g.arcs[x][i];
}
judgment[x]=1;
for(int i=2;i<=g.vexnum;i++){
int min=INT_MAX;
int min_index=0;
for(int j=1;j<=g.vexnum;j++){
if(dist[j]<min&&judgment[j]==0){
min=dist[j];
min_index=j;
}
}
judgment[min_index]=1;
for(int j=1;j<=g.vexnum;j++){
if(dist[j]>g.arcs[min_index][j]){
dist[j]=g.arcs[min_index][j];
}
}
sum+=min;
}
printf("最小生成树的权值和:%d\n",sum);
}
void prim2(graph g,int x){
int judgment[MAX]={0};
int path[MAX];
memset(path,-1,sizeof(path));
judgment[x]=1;
int sum=0;
int n=g.vexnum-1;
while(n--){
int min=INT_MAX,start,end;
for(int i=1;i<=g.vexnum;i++){
if(judgment[i]==1){
for(int j=1;j<=g.vexnum;j++){
if(i!=j&&judgment[j]==0&&g.arcs[i][j]<min){
min=g.arcs[i][j];
start=i;
end=j;
}
}
}
}
path[end]=start;
judgment[end]=1;
sum+=g.arcs[start][end];
}
printf("最小生成树的权值和:\n%d\n",sum);
printf("最小生成树连接情况:\n");
for(int i=1;i<=g.vexnum;i++){
if(path[i]!=-1){
printf("%d---%d\n",path[i],i);
}
}
}
int main() {
graph g;
initGraph(g);
createGraph(g);
showGraph(g);
prim2(g,1);
}
/*测试用例:
5 8
1 2 1
1 5 4
1 3 3
2 4 2
4 5 4
3 4 1
2 3 2
3 5 1
*/
代码运行截图:
可以验证,创建出来的最小生成树长度和连接情况是没有问题的。
结尾
我们今天学习了Prim算法求最小生成树。首先我们有两个集合,一个是树集合,一个树非树集合,随便选取一个顶点添加到树集合中,其他顶点都在非树集合中,接着找这两个集合之间不同顶点的交叉边的最小值,把最小交叉边的非树集合顶点添加到树集合中,依次循环,直到非树集合没有元素了,循环结束。
最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。
同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。
谢谢您的支持,期待与您在下一篇文章中再次相遇!