C++ 最小生成树和最短路径的实现
一.生成树的概念
一个有n个顶点的无向连通图的生成树是一个极小连通图,它含有图中的所有顶点,但只包含构成一棵树的n-1条边。如果在一棵生成树上添加一条边,必定构成一个环,因为这条边使得它依附的那两个顶点之间有了第二条路径。
如果一个无向图有n个顶点切少于n-1条边,则是非连通图。如果它多于n-1条边,则一定有回路。但是,有n-1条边的图不一定都是连通图。
二.最小生成树的概念
一个带权连通图无向图G(假定每条边上的权值均大于零)中可能有多棵生成树,每棵生成树中所有边上的权值之和可能不同;图的所有生成树中具有边上的权值之和的树称为图的最小生成树。
按照定义,n个顶点的连通图的生成树有n个顶点、n-1条边,因此,构造最小生成树的准则有以下几条:
- 必须只使用该图中的边来构造最小生成树
- 必须使用且仅使用n-1条边来连接图中的n个顶点,生成树一定是连通的
- 不能使用产生回路的边
- 最小生成树的权值之和是最小的,但一个图的最小生成树不一定是唯一的。
三.实现最小生成树的算法
建立一个图类(采用邻接矩阵的方式存储)
对邻接矩阵的介绍可以查看 C++ 实现图的存储和遍历
#include <iostream>
using namespace std;
#define MAX 100
#define INF 0x3f3f3f3f
//图的邻接矩阵存储方法
class VertexType{//顶点类型
public:
int no;
char data[MAX];
};
class MGraph{//图邻接矩阵类型
public:
int edges[MAX][MAX];
int n,e;
VertexType vexs[MAX];
};
class Edge{//克鲁斯卡尔求最小生成树需要用到
public:
int u;
int v;
int w;
};
class Graph{
public:
void createMGraph(int a[][MAX],int n,int e);
void prim(int v);
void kruskal();
void dijkstra(int v);
private:
MGraph g;
void sortEdge(Edge E[],int e);
void dispAllPath(int dist[],int path[],int S[],int v,int n);
};
(一)普里姆(Prim)算法
普里姆(Prim)算法是一种构造性算法。假设G=(V,E)是一个具有n个顶点的带权无向连通图,T=(U,TE)是G的最小生成树,其中,U是T的顶点集,TE是T的边集,则由G构造从起始点v出发的最小生成树T的步骤:
- 首先初始化U={v},以v到其它顶点的所有边为候选边。
- 然后重复以下步骤n-1次,使得其它n-1个顶点被加入到U中。
①从候选边中挑选权值最小的边加入TE,设该边在V-U中的顶点是k,将k加入U中;
②考察当前V-U中的所有顶点j,修改侯选边,若(k,j)的权值小于原来和顶点j关联的侯选边,则用(k,j)取代后者作为侯选边。
例:当邻接矩阵为
{0,6,1,5,INF,INF},{6,0,5,INF,3,INF},{1,5,0,5,6,4}
{5,INF,5,0,INF,2},{INF,3,6,INF,0,6},{INF,INF,4,2,6,0}
步骤:
-
初始化图,lowcost[2]最小
-
选择(0,2)边并调整,lowcost[4]最小
-
选择(2,5)边并调整,lowcost[3]最小
-
选择(5,3)边并调整,lowcost[1]最小
-
选择(2,1)边并调整,lowcost[4]最小
-
选择(1,4)边,结束
Prim算法
void Graph::prim(int v){
int lowcost[MAX];
int closest[MAX];
int min,i,j,k;
for(i=0;i<this->g.n;i++){//置初值
lowcost[i] = this->g.edges[v][i];
closest[i] = v;
}
for(i=1;i<this->g.n;i++){//找出n-1个顶点
min = INF;
k = -1;
for(j=0;j<this->g.n;j++){//在V-U中找出离U最近的顶点k
if(lowcost[j]!=0&&lowcost[j]<min){
min = lowcost[j];
k = j;
}
}
cout << "边(" << closest[k] << "," << k << "),权为" << min << endl;
lowcost[k] = 0;//标记k已经加入U
for(j=0;j<this->g.n;j++){//修改数组lowcost和closest
if(this->g.edges[k][j]!=0 && this->g.edges[k][j]<lowcost[j]){
lowcost[j] = this->g.edges[k][j];
closest[j] = k;
}
}
}
}
(二)克鲁斯卡尔(Kruskal)算法
克鲁斯卡尔(Kruskal)算法是一种按权值的递增次序选择合适的边来构造最小生成树的方法。假设G=(V,E)是一个具有n个顶点的带权连通无向图,T=(U,TE)是G的最小生成树,则构造最小生成树的步骤如下:
- 置U的初值等于V(即包含有G中的所有顶点),TE的初值为空集(即图T中的每一个顶点都构成一个分量)。
- 将图G中的边按权值从小到大的顺序依次选取,若选取的边未使生成树T形成回路,则加入TE,否则舍弃,直到TE中包含n-1条边为止。
注意:该算法的关键在于判断选取的边是否与生成树中已保留的边形成回路,可通过判断边的两个顶点所在的连通分量的方法解决。设置辅助数组vset[0…n-1]判断两个顶点之间是否连通。
例:克鲁斯卡尔算法求解最小生成树的过程
Kruskal算法
void Graph::kruskal(){
int i,j,u1,v1,sn1,sn2,k;
int vset[MAX];//建立数组vset
Edge E[MAX];//建立存放所有边的数组E
k = 0;//E的下标从0开始
for(i=0;i<this->g.n;i++){//由图的邻接矩阵g产生的边集数组E
for(j=0;j<this->g.n;j++){//对于无向图仅考虑上三角部分的边
if(this->g.edges[i][j]!=0 && this->g.edges[i][j]!=INF && i<j){
E[k].u = i;
E[k].v = j;
E[k].w = this->g.edges[i][j];
k++;
}
}
}
sortEdge(E,this->g.e);//对E数组按权值递增排序
for(i=0;i<this->g.n;i++){//初始化辅助数组
vset[i] = i;
}
k = 1;//表示当前构造生成树的第几条边,初值为1
j = 0;//E中边的下标,初值为0
while(k<this->g.n){//生成的边数小于n时循环
u1 = E[j].u;
v1 = E[j].v;//取一条边的头、尾结点
sn1 = vset[u1];
sn2 = vset[v1];//分别得到两个顶点的所属的集合编号
if(sn1!=sn2){//两个顶点属于不同的集合,加入不会构成回路
cout << "边(" << u1 << "," << v1 << "),权为" << E[j].w << endl;
k++;//生成边数加1
for(i=0;i<this->g.n;i++){//两个集合统一编号
if(vset[i]==sn2){//集合编号为sn2的改为sn1
vset[i] = sn1;
}
}
}
j++;//扫描下一条边
}
}
void Graph::sortEdge(Edge E[],int e){//插入排序
int i,j;
Edge temp;
for(i=1;i<e;i++){
if(E[i].w<E[i-1].w){
temp = E[i];
j = i-1;
do{
E[j+1] = E[j];
j--;
}while(j>=0&&E[j].w>temp.w);
E[j+1] = temp;
}
}
}
四.最短路径概念
- 不带权图:
经过边数最少的路径称为最短路径,其路径长度称为最短路径长度或最短距离。 - 带权图:
经过的边的权值之和最小的路径称为最短路径,其路径长度(权值之和)为最短路径长度或最短距离。
(一)狄克斯特拉(Dijkstra)算法
求单源最短路径(一个顶点到其余各顶点的最短路径)算法是由狄克斯特拉提出的。具体步骤如下:
- 初始时,顶点集S只包含源点,即S={v},顶点v到自己的距离为0。顶点集U包含除v以外的其他顶点,源点v到U中顶点i的距离为边上的权(若v和i有边<v,i>)或∞(若顶点i不是v的出边邻接点)。
- 从U中选取一个顶点u,它是源点v到U中距离最小的一个顶点,然后把顶点u加入S中(该选定距离就是源点v到顶点u的最短路径长度)。
- 以顶点u为新考虑的中间点,修改源点v到U中各顶点j(j∈U)的距离,若从源点v到顶点j经过u的距离比原来不经过顶点u的距离更短,则修改从源点v到顶点j的最短距离值。
- 重复步骤2和3,直到S包含所有的顶点。
例:path[j]只保存当前最短路径中的前一个顶点的编号
Dijkstra算法
void Graph::dijkstra(int v){
int dist[MAX];//存放源点v到顶点i的目前最短路径
int path[MAX];//存放源点v到顶点i的最短路径
int S[MAX];//用于添加顶点用
int mindis,i,j,n,u=0;
n = this->g.n;//图中顶点个数为n
for(i=0;i<n;i++){
dist[i] = this->g.edges[v][i];//初始化距离
S[i] = 0;//S[i]置空
if(this->g.edges[v][i] < INF){
path[i] = v;//顶点v到顶点i有边时,置顶点i的前一个顶点为v
}else{
path[i] = -1;//顶点v到顶点i没有边时,置顶点i的前一个顶点为-1
}
}
S[v] = 1;//将源点编号v放入S中
for(i=0;i<n-1;i++){//向S中循环添加n-1个顶点
mindis = INF;//mindis置最小长度初值
for(j=0;j<n;j++){//选取不在S中且具有最小距离的顶点u
if(S[j]==0 && dist[j]<mindis){
u = j;
mindis = dist[j];
}
}
S[u] = 1;//将顶点u放入S中
for(j=0;j<n;j++){//修改不在S中的顶点的距离
if(S[j]==0){
if(this->g.edges[u][j]<INF && dist[u]+this->g.edges[u][j]<dist[j]){
dist[j] = dist[u] + this->g.edges[u][j];
path[j] = u;
}
}
}
}
dispAllPath(dist,path,S,v,n);//输出最短路径及长度
}
void Graph::dispAllPath(int dist[],int path[],int S[],int v,int n){
int i,j,k;
int apath[MAX],d;//存放一条最短路径(逆向)及其顶点个数
for(i=0;i<n;i++){//循环输出顶点v到i的路径
if(S[i]==1 && i!=v){
cout << " 从" << v << "到" << i << "最短路径长度为:" << dist[i] << "\t路径:";
d=0;
apath[d] = i;//添加路径上的终点
k = path[i];
if(k==-1){//没有路径的情况
cout << "无路径\n" ;
}else{//存在时输出该路径
while(k!=v){
d++;
apath[d] = k;//将顶点k加入到路径中
k = path[k];
}
d++;
apath[d] = k;..添加路径上的起点
cout << apath[d];//先输出起点
for(j=d-1;j>=0;j--){//再输出其他顶点
cout << "→" << apath[j];
}
cout << endl;
}
}
}
}
(二)弗洛伊德(Floyd)算法
此算法用于求解每对顶点之间的最短路径的一个方法。也可以每次以一个顶点为源点,重复执行Dijkstra算法n次。
五.main函数
int main() {
Graph g;
int n=6,e=9;
int A[MAX][MAX] = {{0,6,1,5,INF,INF},{6,0,5,INF,3,INF},{1,5,0,5,6,4},
{5,INF,5,0,INF,2},{INF,3,6,INF,0,6},{INF,INF,4,2,6,0}};
g.createMGraph(A,n,e);
g.prim(0);
cout << endl;
g.kruskal();
cout << endl;
g.dijkstra(0);
return 0;
}