个连通图的生成树是一个极小的连通子图,它含有图中全部的顶点,但只有足以构成一棵树的n−1条边,若砍去它的一条边,则会使生成树变成非连通图;若给它增加一条边,则会形成图中的一条回路。对于一个带权连通无向图G=(V,E),生成树不同,其中边的权值之和最小的那棵生成树(构造连通网的最小代价生成树),称为G的最小生成树(Minimum-Spanning-Tree, MST)。
构造最小生成树有多种算法,但大多数算法都利用了最小生成树的下列性质:假设G=(V,E)是一个带权连通无向图,U是顶点集V的一个非空子集。若(u,v)是一条具有最小权值的边,其中u∈U,v∈V−U,则必存在一棵包含边(u,v)的最小生成树。
基于该性质的最小生成树算法主要有Prim算法和Kruskal算法,它们都基于贪心算法的策略。
通用的最小生成树算法:
GENERIC_MST(G){
T=NULL;
while T 未形成一棵生成树;
do 找到一条最小代价边(u, v)并且加入T后不会产生回路;
T=T U (u, v);
}
通用算法每次加入一条边以逐渐形成一棵生成树
如果是稠密图,用Prim算法;如果是稀疏图,用Kruskal算法。
1.prim算法
Prim算法构造最小生成树的过程如下图所示。初始时从图中任取一顶点(如顶点加入树T,此时树中只含有一个顶点,之后选择一个与当前T中顶点集合距离最近的顶点,=并将该顶点和相应的边加入T,每次操作后T中的顶点数和边数都增1。以此类推,直至图中所有的顶点都并入T,得到的T就是最小生成树。此时T中必然有n-1条边。
通俗点说就是:从一个顶点出发,在保证不形成回路的前提下,每找到并添加一条最短的边,就把当前形成的连通分量当做一个整体或者一个点看待,然后重复“找最短的边并添加”的操作。
Prim算法的步骤如下:
假设G={V,E}是连通图,其最小生成树T=(U,ET),ET是最小生成树中边的集合。
初始化:向空树T=(U,ET)中添加图G=(V,E)的任一顶点u0,使U={u0},ET=NULL
循环(重复下列操作直至U=V):从图G中选择满足{(u,v)∣u∈U,v∈V−U}且具有最小权值的边(u,v),加入树T,置U=U∪{v},ET=ETU{(u,v)}
,我们先构造一个邻接矩阵,如下图的下图所示。
void MiniSpanTree_Prim(AMGraph G,char u){ //最小生成树(Prim算法)
cout<<endl<<"最小生成树:";
int k = LocateVex(G,u);
int min;
for(int j = 0; j < G.vexnum; j++) //初始化(u与其他顶点的权值,不直接连接的为极大值)
if(j != k){
closedge[j].adjvex = u;
closedge[j].lowcost = G.arcs[k][j];//将v0顶点与之组成边的权值存入数组
}
closedge[k].lowcost = 0;
for(int i = 1; i < G.vexnum; i++){
min = MaxInt;//初始化最下权值为∞,通常设置一个不可能的很大的数字
for(int j = 0; j < G.vexnum; j++)
if(closedge[j].lowcost != 0 && closedge[j].lowcost < min){//如果权值不为0且权值小于min
min = closedge[j].lowcost;/则让当前权值成为最小值
k = j;//将当前最小值的下标存入k
}
cout<<closedge[k].adjvex<<G.vexs[k]<<" "; //打印当前顶点边中权值的最小边
closedge[k].lowcost = 0;
for(int j = 0;j < G.vexnum; j++)
if(G.arcs[k][j] < closedge[j].lowcost){
closedge[j].adjvex = G.vexs[k];//将较小的权值存入 closedge
closedge[j].lowcost = G.arcs[k][j];
}
}
}
由算法代码中的循环嵌套可得知此算法的时间复杂度为O(n^2)
Prim算法的精髓是根据点的不同,最小生成树也可能不同。根据起始点,找到与该点相关联的其他顶点,选择其中边权值最小的纳入集合,以此类推,最终将所有顶点都纳入集合中,这个就是Prim算法的最小生成树。
完整代码
#include<iostream>
using namespace std;
#define MaxInt 32767 //表示极大值,用于初始化无向网
#define MAXNUM 100
char visited1[MAXNUM];
typedef struct{
char vexs[MAXNUM]; //顶点
int arcs[MAXNUM][MAXNUM];//边
int vexnum,arcnum;
}AMGraph; //邻接矩阵的数据类型
struct{
char adjvex;//最小边上的已选择顶点
int lowcost;//最小边上的权值
}closedge[MAXNUM];//辅助数组
int LocateVex(AMGraph G,char v){
for(int i = 0; i < G.vexnum; i++){
if(G.vexs[i] == v)return i;
}
return -1;
}
int CreateUNG(AMGraph &G){ //创建无向图
char v1,v2;
int w;//权值
cout<<"请输入顶点数和边数:";
cin>>G.vexnum>>G.arcnum;
cout<<"请依次输入顶点:";
for(int i = 0; i < G.vexnum; i++)cin>>G.vexs[i];
for(int j = 0; j < G.vexnum; j++)
for(int i = 0; i < G.vexnum; i++)
G.arcs[j][i] = MaxInt; //初始化邻接矩阵
cout<<"请依次输入邻接边和权值:"<<endl;
for(int k = 0; k < G.arcnum; k++){
cin>>v1>>v2>>w;
int i = LocateVex(G,v1);
int j = LocateVex(G,v2);
G.arcs[i][j] = w;
G.arcs[j][i] = w;
}
return 1;
}
void MiniSpanTree_Prim(AMGraph G,char u){ //最小生成树(Prim算法)
cout<<endl<<"最小生成树:";
int k = LocateVex(G,u);
int min;
for(int j = 0; j < G.vexnum; j++) //初始化(u与其他顶点的权值,不直接连接的为极大值)
if(j != k){
closedge[j].adjvex = u;
closedge[j].lowcost = G.arcs[k][j];
}
closedge[k].lowcost = 0;
for(int i = 1; i < G.vexnum; i++){
min = MaxInt;
for(int j = 0; j < G.vexnum; j++)
if(closedge[j].lowcost != 0 && closedge[j].lowcost < min){
min = closedge[j].lowcost;
k = j;
}
cout<<closedge[k].adjvex<<G.vexs[k]<<" ";
closedge[k].lowcost = 0;
for(int j = 0;j < G.vexnum; j++)
if(G.arcs[k][j] < closedge[j].lowcost){
closedge[j].adjvex = G.vexs[k];
closedge[j].lowcost = G.arcs[k][j];
}
}
}
int main(){
AMGraph G;
CreateUNG(G);
MiniSpanTree_Prim(G,'a');
return 0;
}
样例
6 10
a b 6
a c 1
a d 5
b c 5
c d 5
b e 3
c e 6
c f 4
d f 2
e f 6
2.Kruskal算法
Kruskal算法构造最小生成树的过程如下图所示。初始时为只有n个顶点而无边的非连通图 T=V,每个顶点自成一个连通分量,然后按照边的权值由小到大的顺序,不断选取当前未被选取过且权值最小的边,若该边依附的顶点落在T中不同的连通分量上,则将此边加入T,否则舍弃此边而选择下一条权值最小的边。以此类推,直至T中所有顶点都在一个连通分量上。
时间复杂度为O(loge)
struct node
{
VerTexType Head;
VerTexType Tail;
ArcType lowcost;
}Edge[1000];
int Vexset[MVNum];
bool cmp(node a,node b)
{
return a.lowcost<b.lowcost;
}
void MiniSpanTree_Kruskal(AMGraph G)
{
int k=0;
for (int i=0;i<G.vexnum;i++)
{
for (int j=0;j<G.vexnum;j++)
{
if (G.arcs[i][j]!=MaxInt)
{
Edge[k].Head=G.vexs[i];
Edge[k].Tail=G.vexs[j];
Edge[k].lowcost=G.arcs[i][j];
k++;
}
}
}
sort(Edge,Edge+k,cmp);
for (int i=0;i<k;i++)
Vexset[i]=i;
int wpl=0;
for (int i=0;i<k;i++)
{
int v1=LocateVexAMG(G,Edge[i].Head);
int v2=LocateVexAMG(G,Edge[i].Tail);
int vs1=Vexset[v1];
int vs2=Vexset[v2];
if (vs1!=vs2)
{
cout<<Edge[i].Head<<" "<<Edge[i].Tail<<" "<<Edge[i].lowcost<<endl;
wpl+=Edge[i].lowcost;
for (int j=0;j<G.vexnum;j++)
{
if (Vexset[j]==vs2)
Vexset[j]=vs1;
}
}
}
cout<<"最小生成树总权值为:"<<wpl<<endl;
}
完整代码
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
int parent[10];
int n,m;
int i,j;
struct edge{
int u,v,w; //边的顶点,权值
}edges[10];
//初始化并查集
void UFset(){
for(i=1; i<=n; i++) parent[i] = -1;
}
//查找i的根
int find(int i){
int temp;
//查找位置
for(temp = i; parent[temp] >= 0; temp = parent[temp]);
//压缩路径
while(temp != i){ //表明没找到根节点,因为根节点的父节点是-1
int t = parent[i];
parent[i] = temp;
i = t;
}
return temp;
}
//合并两个元素a,b
void merge(int a,int b){
int r1 = find(a);
int r2 = find(b);
int tmp = parent[r1] + parent[r2]; //两个集合节点数的和
if(parent[r1] > parent[r2]){
parent[r1] = r2;
parent[r2] = tmp;
}else{
parent[r2] = r1;
parent[r1] = tmp;
}
}
void kruskal(){
int sumWeight = 0;
int num = 0;
int u,v;
UFset();
for(int i=0; i<m; i++)
{
u = edges[i].u;
v = edges[i].v;
if(find(u) != find(v)){ //u和v不在一个集合,两者的根不同
printf("加入边:%d %d,权值: %d\n", u,v,edges[i].w);
num ++;
merge(u, v); //把这两个边加入一个集合。
sumWeight+=edges[i].w;
}
}
printf("最小生成树的权值之和为:%d \n", sumWeight);
}
//比较函数,用户排序
int cmp(const void * a, const void * b){
edge * e1 = (edge *)a;
edge * e2 = (edge *)b;
return e1->w - e2->w;
}
int main() {
scanf("%d %d", &n, &m);
for(i=0; i<m; i++){
scanf("%d %d %d", &edges[i].u, &edges[i].v, &edges[i].w);
}
qsort(edges, m, sizeof(edge), cmp);
kruskal();
system("pause");
return 0;
}
/*
测试数据:
6 10
1 2 6
1 3 1
1 4 5
2 3 5
2 5 3
3 4 5
3 5 6
3 6 4
4 6 2
5 6 6
*/