一、基础概念
- 生成树:在连通图中,包含图中全部顶点的一个极小连通子图(n个顶点的生成树有且仅有n-1条边,因为n个顶点至少要有n-1条边才能够保证连通)。
- 生成森林:在非连通图中,由每个连通分量都可以得到一棵生成树,这些生成树就组成了非连通图的生成森林。
- 生成树的代价:无向连通网的生成树上各边的权值纸盒称为该生成树的代价。
- 最小生成树:在图中所有生成树中,代价最小的生成树。
- MST性质:
二、Prim算法(从顶点的角度出发)
设G=(V,E)是无向连通网,T=(U,TE)是G的最小生成树。
①基本思想:
- 设置初始状态为U={v}(v属于V)、TE={}。
- 重复执行[在所有i属于U,j属于V-U的边中找一条代价最小的边(i,j)并入TE,同时j并入U]直至U=V为止。
- 改进:对于U-V中的每个顶点,只需要保留从该顶点到U中某个顶点的最短边即可。
②存储结构:
- 图的存储结构:邻接矩阵
- 候选最短边集:数组
adjvex[n]表示最短边的邻接点(adjvex[i] = j)
lowcost[n]表示权值(lowcost[i] = w)
其含义是候选最短边(i,j)的权值为w,其中i属于V-U,j属于U。
lowcost[i] = min{lowcost[i],边(i,j)的权值}
adjvex[i] = j(如果边(i,j)的权值<lowcost[i])
const int MAX = 1000;
void Prim(int v)
{
int i,j,k;
int adjvex[MaxSize],lowcost[MaxSize];//邻接点矩阵,权值矩阵
for(i=0 ; i<vertexNum ; i++)//初始化辅助数组,从v开始,U中进入第一个点v
{
lowcost[i] = edge[v][i];//目前U中只有一个v,权值数组中都是V-U点到v的距离
adjvex[i] = v;
}
lowcost[v] = 0;//将顶点V加入集合U(赋一个常值表示加入则无需再开一个visited数组)
for(k = 1 ; k<vertexNum ; k++)//对剩下U-V中每个顶点都进行迭代
{
j = MinEdge(lowcost,vertexNum);//查找最小权值并返回其下标
cout<<j<<adjvex[j]<<lowcost[j]<<endl;
lowcost[j] = 0;//将顶点j加入集合U
for(i = 0 ; i<vertexNum ; i++)//对辅助数组进行调整
{
if(edge[i][j]<lowcost[i])//U中加入了新的j点,看是否需要更新V-U中顶点到U中顶点的最小权值
{
lowcost[i] = edge[i][j];
adjvex[i] = j;
}
}
}
}
//MinEdge函数的实现:查找最小权值并返回其下标
int MinEdge(int lowcost[],int vertexNum)
{
int i,min;
min = Max;//标记最小权值
for(i = 0 ; i<vertexNum ; i++)
{
if(lowcost[i]<lowcost[min])
min = i;
}
return min;
}
Prim算法的时间复杂度是O(n^2),与网中的边数无关,因此适用于求稠密图的最小生成树。
实例:以邻接矩阵为存储结构,实现的无向带权图的最小生成树算法Prim、深度广度优先遍历,完整代码如下:
#include <iostream>
using namespace std;
const int MaxSize = 10; //图中最多顶点个数
int visited[MaxSize]={0};
const int MAX = 1000;
struct element //改进:使用结构体数组代替lowcast数组以及adjvex数组
{
int lowcost, adjvex;
};
int MinEdge(element *e,int vertexNum);
template <class DataType>
class MGraph
{
public:
MGraph(DataType a[ ], int n, int e); //构造函数,建立具有n个顶点e条边的图
~MGraph( ) { } //析构函数为空
void DFSTraverse(int v); //深度优先遍历图
void BFSTraverse(int v); //广度优先遍历图
void Prim(int v) ;
private:
DataType vertex[MaxSize]; //存放图中顶点的数组
int edge[MaxSize][MaxSize]; //存放图中边的数组
int vertexNum, edgeNum; //图的顶点数和边数
};
template <class DataType>
MGraph<DataType>::MGraph(DataType a[ ], int n, int e)
{
int i, j, k ,x;
vertexNum = n;
edgeNum = e;
for (i = 0; i < vertexNum; i++)
vertex[i] = a[i];
for (i = 0; i < vertexNum; i++)
for (j = 0; j < vertexNum; j++)
edge[i][j] = MAX;
for (k = 0; k < edgeNum; k++)
{
cout << "请输入两个边顶点的序号:";
cin >> i >> j; //输入边依附的两个顶点的编号
cout << "请输入边的权值:";
cin >> x;
edge[i][j] = x; //置边的标志为1
edge[j][i] = x; //无向图 边是相通的
}
}
template <class DataType>
void MGraph<DataType>::DFSTraverse(int v)
{
cout << vertex[v];
visited[v] = 1;
for (int j = 0; j < vertexNum; j++)
{
if (edge[v][j] != MAX && visited[j] == 0)DFSTraverse(j); //存在与v相连的j点且j点未被访问过 则进一步遍历j点
}
}
template <class DataType>
void MGraph<DataType>::BFSTraverse(int v)
{
int w, j, Q[MaxSize];
int front = -1, rear = -1;
cout << vertex[v];
visited[v] = 1;
Q[++rear] = v; //v点入队
while (front != rear)
{
w = Q[++front]; //Q出队值 w
for (j = 0; j < vertexNum; j++)//遍历所有可能与w相连的点 若有,则输出、标记、入队
{
if (edge[w][j] != MAX && visited[j] == 0)//存在与w点相连的j点且j点未被访问过 则输出j点值 然后标记j点将j点入队
{
cout << vertex[j];
visited[j] = 1;
Q[++rear] = j;
}
}
}
}
template <class DataType>
void MGraph<DataType>::Prim(int v) //从顶点v出发
{
int i,j,k;
element e[MaxSize]; //定义出发点结构体,里面包含当前出发点的所有邻点权值
for( i = 0;i<vertexNum;i++)
{
e[i].adjvex = v;
e[i].lowcost = edge[v][i];
}
e[v].lowcost = 0;
for(k = 1;k<vertexNum;k++) //迭代n-1次 因为一开始已经取了一个起点 剩下n-1个
{
j = MinEdge(e,vertexNum); //找出还未连接的点中距离目前点最近的
cout<<"<"<<e[j].adjvex<<","<<j<<">"<<e[j].lowcost<<endl; //输出j点
e[j].lowcost = 0;
for(i = 0;i<vertexNum;i++)
{
if(edge[i][j]<e[i].lowcost) //寻找j点到各个点比v到各个点路径短的,并替换
{
e[i].lowcost = edge[i][j];
e[i].adjvex = j; //j与i的距离 比v与i的距离近 因此i的最近点换成j
}
}
}
}
int MinEdge(element *e,int vertexNum)
{
int min = MAX;
int res;
for(int i = 0;i<vertexNum;i++)
{
if(e[i].lowcost!=0&&(e[i].lowcost<min))
{
res = i;
min = e[i].lowcost ;
}
}
return res;
}
int main( )
{
char ch[]={'A','B','C','D','E','F'};
MGraph<char> MG(ch, 6, 9);
for (int i=0; i<MaxSize; i++)
visited[i]=0;
cout<<"深度优先遍历序列是:";
MG.DFSTraverse(0);
cout<<endl;
for (int i=0; i<MaxSize; i++)
visited[i]=0;
cout<<"广度优先遍历序列是:";
MG.BFSTraverse(0);
cout<<endl;
cout<<"最小生成树的生成过程为:"<<endl;
MG.Prim(0);
cout<<endl;
system("pause");
}
三、Kruskal算法(从边的角度出发)
设G=(V,E)是无向连通网,T=(U,TE)是G的最小生成树。
①基本思想:
- 设置初始状态为U=V、TE={}。
- T中的顶点各自构成一个连通分量,然后按照边的权值由小到大的顺序,依次考察边集E中的各条边。
- 若被考察边的两个顶点属于两个不同的连通分量,则将此边加入到TE中,同时把两个连通分量连接为一个连通分量;若被考察边的两个顶点属于同一个连通分量,则舍去此边(以免造成回路)。
- 重复3过程,当T中的连通分量数为1时,此连通分量便为G的一棵最小生成树。
②存储结构:
- 图的存储结构:采用边集数组进行存储,为提高查找最短边的速度,可以先对边集数组按边上的权值排序。
- 连通分量顶点所在的集合:用并查集实现集合的查找与合并,采用树的双亲表示法存储。parent[i]表示顶点i所在集合的根节点,初始时parent[i]=-1,对于边(u,v),设vex1和vex2分别为两个顶点所在集合的根节点,若vex1!=vex2,令parent[vex2] = vex1,实现两个集合的合并。
//定义边集数组的元素类型
struct EdgetType
{
int from,to,weight;
}
//图的边集数组存储的类定义
const int MaxVertex = 10;//图中最多顶点数
const int MaxEdge = 100;//图中最多边数
template<typename DataType>
class EdgeGraph
{
public:
EdgeGraph(DataType a[], int n, int e);//构造函数
~EdgeGraph();
void Kruskal();
private:
int FindRoot(int parent[], int v);//求顶点v所在集合的根
DataType vertex[MaxVertex];//存储顶点的一维数组
EdgeType edge[MaxEdge];//存边的边集数组
int vertexNum,edgeNum;
//kruskal算法的实现
void Kruskal()
{
int num = 0, i, verx1, vex2;
int parent[vertexNum];//双亲表示法存储并查集
for(i = 0 ; i<vertexNum ; i++)
{
parent[i] = -1;//初始化n个连通分量
}
for(num = 0,i = 0 ; num<vertexNum ; i++)//依次考察最短边(i控制),直到T中的连通分枝数为1(vertexNum个顶点,vertexNum-1条边)为止(num控制)
{
vex1 = FindRoot(parent, edge[i].from);
vex2 = FindRoot(parent, edge[i].to);
if(vex1!=vex2)//如果两个点的根节点不同
{
cout<<"("<<edge[i].from<<","<<edge[i].to<<")"<<edge[i].weight;
parent[vex2] = vex1;//合并集合
num++;//最小生成树中的边的数量+1
}
}
}
//FindRoot函数的实现:求顶点v所在集合的根
int FindRoot(int parent[], int v)
{
int t = v;
while(parent[t]>-1)//求顶点t的双亲一直到根节点(-1)
t = parent[t];
return t;
}
Kruskal算法时间复杂度为O(elog2e),与网中的顶点数无关,适用于求稀疏图的最小生成树。