1、任务简述:
利用普利姆算法和克鲁斯卡尔算法实现最小生成树问题
要求:
(1).自行建立图的数据文件,第一行是顶点个数,然后依次是顶点名,接下来是边,用float表示边的权值;
(2).以邻接表或者邻接矩阵表示图皆可,显示输出原图(按照邻接表的样式);
(3).分别利用prim和kruscal算法实现最小生成树(最小生成树用邻接表或邻接矩阵表示均可)。
(4).输出最小生成树(按照邻接表的样式);
(5).比较这两种算法
2、算法描述:
数据结构:
typedef struct arc //邻接表的节点
{
int index; //编号
float weight; //权重
struct arc *next; //指向下一个节点
}AR;
typedef struct MyGraph//用了邻接矩阵和邻接表两种方式来存边:prim算法用邻接表,kruscal算法用邻接矩阵
{
int type;//0表示无向网,1表示有向网
int arcnum; //点的个数
int vexnum; //边的个数
char **vexname; //点的名字
AR *N; //邻接表
float **A;//邻接矩阵动态数组
}GH;
prim算法:
1).输入:一个加权连通图,其中顶点集合为V,边集合为E;
2).初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {},为空;
3).重复下列操作,直到Vnew = V:
a.在集合E中选取权值最小的边,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
b.将v加入集合Vnew中,将边加入集合Enew中;
4).输出:使用集合Vnew和Enew来描述所得到的最小生成树。
kruscal算法:
1.新建图G,G中拥有原图中相同的节点,但没有边;
2.将原图中所有的边按权值从小到大排序;
3.从权值最小的边开始,如果这条边连接的两个节点于图G中不在同一个连通分量中,则添加这条边到图G中;
4.重复3,直至图G中所有的节点都在同一个连通分量中。
本题我分别运用两个不同的算法编写了不同的代码,发现,只有当每条边的权重不一样时,两种算法算出的结果一定是相同的,但是如果有相同的权重,那么可能两种算法得到的最小生成树是不一样的。
3、源代码
#include
#include
#include
#define maxx 9999
typedef struct arc //邻接表的节点
{
int index; //编号
float weight; //权重
struct arc *next; //指向下一个节点
}AR;
typedef struct MyGraph//用了邻接矩阵和邻接表两种方式来存边:prim算法用邻接表,kruscal算法用邻接矩阵
{
int type;//0表示无向网,1表示有向网
int arcnum; //点的个数
int vexnum; //边的个数
char **vexname; //点的名字
AR *N; //邻接表
float **A;//邻接矩阵动态数组
}GH;
void DFS(GH *G,int *visit,int index)//深度优先搜索
{
AR *p;
visit[index]=1;
p=G->N[index].next;
while(p)
{
if(visit[p->index]==0)
DFS(G,visit,p->index);
p=p->next;
}
}
int isconnect(GH *G)//判断是否连通
{
int i;
int ans;//连通分支
ans=0;
int *visit=(int *)malloc(sizeof(int)*G->vexnum);//标记是否已经经过这个节点
memset(visit,0,sizeof(int)*G->vexnum);
for(i=1;i<G->vexnum;i++)
{
if(!visit[i])
{
DFS(G,visit,i);
ans++;
}
}
free(visit);
if(ans==1)
return 1; //连通
else
return 0; //不联通
}
int findvex(GH *G,char *s)//找点对应的编号
{
int i;
for(i=0;i<G->vexnum;i++)
{
if(strcmp(s,G->vexname[i])==0)
return i;
}
printf("Error!\n");
exit(0);
}
void creatgraph(GH *G)//创建图
{
FILE *fp;
int i,j,mg,n,k;
char s1[20],s2[20];
AR *p;
fp=fopen("081810221zlh.txt","rb");//只读文件
if(!fp)
{
printf("Can not open file!\n");
exit(0);
}
fscanf(fp,"%d",&n);//读入点的个数
G->vexnum=n;
printf("图为有向图--1还是无向图--0:"); //手动输入图是有向图还是无向图
scanf("%d",&k);
G->type=k;
G->N=(AR *)malloc(n*sizeof(AR));
G->A=(float **)malloc(n*sizeof(int *));//为邻接矩阵分配n个行指针
G->vexname=(char **)malloc(n*sizeof(char *));//为姓名矩阵分配n个行指针
G->arcnum=0; //边的个数初始化为0
for(i=0;i<n;i++) //初始化
{
fscanf(fp,"%s",s1); //读入节点名字
G->vexname[i]=(char *)malloc(strlen(s1)*sizeof(char));
strcpy(G->vexname[i],s1);
G->N[i].next=NULL; //邻接表指针初始化指向空指针
G->A[i]=(float *)malloc(n*sizeof(int));
for(j=0;j<n;j++)
G->A[i][j]=0;//对第i行的元素初始化,初始值为0
}
while(fscanf(fp,"%s%s%d",s1,s2,&mg)!=EOF)//读入边
{
i=findvex(G,s1); //边的两个顶点之一
j=findvex(G,s2); //边的两个顶点之一
(G->arcnum)++; //边的数量加一
p=(AR *)malloc(sizeof(AR));
p->index=j; //i作为起点,j作为终点
p->weight=mg;//j的权重为i到j的权重
p->next=G->N[i].next;//把节点p插入到邻接表里(起点是i)
G->N[i].next=p;
G->A[i][j]=mg; //对邻接矩阵的对应元素赋值
if(G->type==0) //G为无向图,则要反着再来一次,和上面一样
{
p=(AR *)malloc(sizeof(AR));
p->index=i;
G->A[j][i]=mg;
p->weight=mg;
p->next=G->N[j].next;
G->N[j].next=p;
}
}
fclose(fp);//关闭文件
}
void showgraph(GH *G)//用邻接表显示图
{
int i;
float sum=0.;
AR *p;
for(i=0;i<G->vexnum;i++)
{
printf("\n%s",G->vexname[i]);
p=G->N[i].next;
while(p)
{
sum=sum+p->weight;
printf("--%s(%.2f) ",G->vexname[p->index],p->weight);//控制输出的位数,保持美观
p=p->next;
}
}
printf("\n图的权值为:%f\n\n",sum);
}
int findmin(float *a,int n)//找到数组中最小的正值对应的编号
{
int min,i,j;
min=maxx;
j=0;
for(i=0;i<n;i++)//比较排序的思路,但是不能假设第一个为min,防止第一个为0
{
if((a[i]!=0)&&a[i]<min)
{
min=a[i];
j=i;
}
}
return j;
}
void prim(GH *G) //prim算法
{
GH *prim; //用来存放图G的最小生成树
AR *a;
int i,mmin,nex[G->vexnum]; //mmin为图中树外的点中到树里距离最小点的编号,nex表示与每个点相连的点的编号
float min[G->vexnum]; //每个点到当前树的距离
prim=(GH *)malloc(sizeof(GH)); //初始化图prim---开始
prim->vexnum=G->vexnum; //点的个数和原来一样
prim->arcnum=prim->vexnum-1; //最小生成树边的个数为顶点个数-1
prim->type=0; //无向图
prim->N=(AR *)malloc(prim->vexnum*sizeof(AR));
prim->vexname=(char **)malloc(prim->vexnum*sizeof(char *));
memset(nex,0,prim->vexnum*sizeof(int));
for(i=0;i<prim->vexnum;i++)
{
prim->vexname[i]=(char *)malloc(strlen(G->vexname[i])*sizeof(char));
strcpy(prim->vexname[i],G->vexname[i]);
prim->N[i].next=NULL;
} //初始化图prim---结束
min[0]=0; //先把第一个点放进树中
for(i=1;i<prim->vexnum;i++) //初始化min,都赋值为一个较大值
min[i]=maxx;
for(a=G->N[0].next;a;a=a->next) //现在树里面只有节点0,对那些距离到0更近的点做一次更新,把原来的距离换位到v0的距离
min[a->index]=a->weight;
for(i=1;i<prim->vexnum;i++) //每次找到离树距离最小的点并放到树中
{
a=(AR *)malloc(sizeof(AR));
mmin=findmin(min,prim->vexnum);
a->index=mmin;
a->weight=min[mmin];
a->next=prim->N[nex[mmin]].next;
prim->N[nex[mmin]].next=a;
a=(AR *)malloc(sizeof(AR));
a->index=nex[mmin];
a->weight=min[mmin];
a->next=prim->N[mmin].next;
prim->N[mmin].next=a;
min[mmin]=0;
for(a=G->N[mmin].next;a;a=a->next)//对min进行更新
{
if(min[a->index]>a->weight)
{
nex[a->index]=mmin;
min[a->index]=a->weight;
}
}
}
showgraph(prim);//展示prim生成的最小生成树
}
void findmmin(GH *G,float *mmin)//寻找矩阵中最小的正值及其所在的位置
{
int i,j;
float min=9999;
for(i=0;i<G->vexnum;i++)
{
for(j=0;j<G->vexnum;j++)
{
if(G->A[i][j]!=0&&G->A[i][j]<min)
{
min=G->A[i][j];
mmin[0]=i; //min[0]存储横坐标
mmin[1]=j; //min[1]存储纵坐标
mmin[2]=min;//min[2]存储相应的值
}
}
}
G->A[(int)(mmin[0])][(int)(mmin[1])]=0.;//float类型
}
int get(int *pre,int k)//得到该点最原始的祖先
{
int a;
a=k;
while(pre[a]!=a)
a=pre[a];
return a;
}
void show(GH *G)//用邻接矩阵展示图
{
int i,j;
float sum=0.;
for(i=0;i<G->vexnum;i++)
{
printf("\n%s",G->vexname[i]);
for(j=0;j<G->vexnum;j++)
{
if(G->A[i][j])
{
printf("--%s(%.2f)",G->vexname[j],G->A[i][j]);
sum=sum+G->A[i][j];
}
}
}
printf("\n图的权值为:%f\n\n",sum);
}
void kruscal(GH *G)//kruscal算法
{
GH *kruscal;//p为新的图,由p来存放生成树的数据
int i,j,pre[G->vexnum];//pre指向点的前缀
float mmins[3]; //min[0],min[1]表示权值最小的边的两个顶点,min[3]表示权重
kruscal=(GH *)malloc(sizeof(GH));//初始化
kruscal->vexnum=G->vexnum;
kruscal->arcnum=kruscal->vexnum-1;
kruscal->type=0;
kruscal->A=(float **)malloc(kruscal->vexnum*sizeof(float *));
kruscal->vexname=(char **)malloc(kruscal->vexnum*sizeof(char *));
for(i=0;i<kruscal->vexnum;i++)
{
pre[i]=i;
kruscal->vexname[i]=(char *)malloc(strlen(G->vexname[i])*sizeof(char));
strcpy(kruscal->vexname[i],G->vexname[i]);
kruscal->A[i]=(float *)malloc(kruscal->vexnum*sizeof(float));
for(j=0;j<kruscal->vexnum;j++)
kruscal->A[i][j]=0;
} //初始化结束
for(i=1;i<kruscal->vexnum;)//把每一个点连进去
{
findmmin(G,mmins);
if(get(pre,(int)(mmins[0]))!=get(pre,(int)(mmins[1]))) //如果两个点的祖先不同,则将两个点连起来
{
if(get(pre,(int)(mmins[0]))<get(pre,(int)(mmins[1])))
pre[get(pre,(int)(mmins[1]))]=get(pre,(int)(mmins[0]));
else
pre[get(pre,(int)(mmins[0]))]=get(pre,(int)(mmins[1]));
kruscal->A[(int)(mmins[0])][(int)(mmins[1])]=mmins[2];//修改相应的邻接矩阵
kruscal->A[(int)(mmins[1])][(int)(mmins[0])]=mmins[2];//修改相应的邻接矩阵
i++;
}
}
show(kruscal);//展示kruscal生成的最小生成树
}
int main()
{
GH G;
system("color 1E");
printf("------------------------------081810221朱林昊------------------------------\n");
printf("\n------------------------------最小生成树------------------------------\n\n");
creatgraph(&G);//创建图
printf("\n------------------------------原图------------------------------\n\n");
showgraph(&G);//显示图
printf("\n------------------------------原图显示完毕------------------------------\n");
if(isconnect(&G))//连通图才有最小生成树
{
printf("\n------------------------------prim算法得到的最小生成树------------------------------\n");
prim(&G);
printf("\n------------------------------prim显示完毕------------------------------\n");
printf("\n------------------------------kruscal算法得到的最小生成树------------------------------\n");
kruscal(&G);
printf("\n------------------------------kruscal显示完毕------------------------------\n");
}
else
printf("\n不是连通图,没有最小生成树!!!\n");
return 0;
}//321行
4、运行结果
原图:
prim算法:
Kruscal算法:
5、总结
性能分析:
prim时间复杂度:O(n^2)
Kruskal时间复杂度:O(eloge)
遇到的问题与解决方法:
对于Kruskal算法:难点在于,选好边以后,需要对目前的图进行一次判断是否有圈的操作,不过这个之前上课讲过了,如果没有,就继续做下一个,如果有圈,那么就寻找除去该条边以外最短的边,直到寻找的边的个数为n-1即可,或者可以用是否连通的函数,直到联通即可。
心得体会:
运行结果正确,输出较为美观,可以发现用最小生成树算出来的树和最短路径得到的树是不一样的,甚至,如果图里面有的边的权重一样的话,那么prim算法和kruscal算法得到的树也是可能不一样的。
存在问题和改进方法:
都是一些经典的算法,只是设计的较为繁琐,使用了邻接矩阵和邻接表来处理,但是网上很多人是通过并查集来实现的,并查集:是一种简单的用途广泛的集合。 并查集是若干个不相交集合,能够实现较快的合并和判断元素所在集合的操作,应用很多,如其求无向图的连通分量个数、最小公共祖先、带限制的作业排序,还有最完美的应用:实现Kruskar算法求最小生成树。