图的应用
一、最小生成树
针对带权值的无向图,即网结构,使用n-1条边连接n个顶点,且使权值的和最小。
我们把构造连通网的最小代价生成树称为最小生成树
1.普利姆(Prim)算法
对于如下例子
输入以上的邻接矩阵,利用prim算法生成最小数
const int MAXVEX = 9; //顶点最大数,设为9
const int INFINITY = 65535; //最大权值
void MiniSpanTree_Prim(MGraph G)
{
int min,i,j,k;
int adjvex[MAXVEX]; //保存相关顶点下标,adjvex[2]=1代表顶点v2与顶点v1相连
//lowcost[i]表示顶点adjvex[i]与i两个顶点之间的权值
int lowcost[MAXVEX]; //保存相关顶点间的权值,为0表示该顶点完成任务
lowcost[0] = 0; //初始化第一个权值,v0加入生成树
adjvex[0] = 0; //初始化第一个顶点的下标
for(i=1;i<G.numVertexes;i++)
{
lowcost[i] = G.arc[0][i]; //将与v0相关的边的权值全存入顶点
adjvex[i] = 0; //初始化均为v0的下标
}
for(i=1;i<G.numVertexes;i++) //循环所有除v0外的顶点
{
k=0;
j=1;
min = INFINITY; //初始化权值,设为无穷
while(j<G.numVertexes) //循环所有结点,得到当前lowcost最小值和该值的下标
{
if(lowcost[j]!=0 && lowcost[j]<min) //找到未访问过的且与v0之间权值最小的顶点
{
min = lowcost[j]; //获取当前lowcost数组中最小权值
k = j; //记录最小权值的顶点的下标值
}
j++;
}
cout<<adjvex[k]<<k; //打印当前顶点边中权值最小的边
lowcost[k] = 0; //当前顶点权值设为0,表示该顶点已经完成任务
for(j=1;j<G.numVertexes;j++) //循环所有结点
{
//此时的lowscost还未纳入顶点k
//若下标为k顶点的各边权值小于此前这些顶点未被加入生成树的权值
if(lowcost[j]!=0 && G.arc[k][j]<lowcost[j])
{
lowcost[j] = G.arc[k][j]; //将较小的权值存入lowcost
adjvex[j] = k; //将下标k存入adjvex,代表j与k两个顶点
}
}
}
}
核心思想:
假设N=(P,{E})是连通网,TE是N上最小生成树中边的集合。算法从U={u0}(u0属于V)TE={}开始。重复以下操作:在所有边u属于U,v属于V-U的边(u,v)中找到一条代价最小的边(u0,v0)并入集合TE,同时并入顶点v0,直到U=V。此时必有n-1条边,则T=(V,{TE})为N的最小生成树。时间算法复杂度为O(n^2).
个人看法:lowcost数组代表未加入的结点到已生成树的某个结点adjvex[i]的权重大小,最后的for循环即是更新权重步骤
2.克鲁斯卡尔算法
使用边集数组的存储形式,直接以边为目标去构建,寻找最小的权值的边来构建树,只需防止形成环路而已。
//边集数组的结构定义
typedef struct
{
int begin;
int end;
int weight;
}Edge;
利用排序算法,将边集数组按权值从小到大排列,如下图
最小生成树的生成代码
void MiniSpanTree_Kruskal(MGraph G)
{
int i,n,m;
Edge edges[MAXEDGE]; //定义边集数组
int parent[MAXEDGE]; //利用一维数组来判断是否形成环路
/*****************/
/*此处省略排序算法*/
/*****************/
for(i=0;i<G.numVertexes;i++)
{
parent[i] = 0; //初始化数组为0
}
for(i=0;i<G.numEdges;i++) //循环每一条边
{
n = Find(parent,edges[i].begin);
m = Find(parent,edges[i].end);
if(n != m) //假设n与m不等,说明没有生成环路
{
parent[n] = m; //将边尾顶点放在下标起点的parent
cout<<edges[i].begin<<edges[i].end<<edges[i].weight<<endl;
}
}
}
int Find(int *parent,int f) //查找连线顶点的尾部下标
{
while(parent[f]>0) //如果该顶点已经有了连线,则循环递推直到为0退出
f = parent[f]; //parent[i] = j即代表已有边i-j存在
return f;
}
核心思想:
假设N=(V,{E})是连通网,则令最小生成树的初始状态为只有n个顶点而无边的非连通图T={V,{}},图中每个顶点自成一个连通分量。E选择代价最小的边,若该边依附的顶点落在T中的不同的连通分量上,则将此边加入T中,否则舍去此边而选择下一条代价最小的边。以此类推,直到T中所有顶点都在同一个连通分量上为止。其时间复杂度为O(eloge)。
二、最短路径
对于无向带权值的图,即网图而言,最短路径是指两顶点之间经过边上权值之和最少的路径,并且我们称路径上的第一个顶点是源点,最后一个为终点。
1.迪杰斯特拉算法
计算从特定点出发到其余顶点的最短路径问题
核心思想:先求v0到v1的最短距离,再求v0到v2的距离并与之前v0到v1加v1到v2的距离比较得出最短距离,依次基于之前的最短距离进行比较求出更远点的路径。
#define MAXVEX 9
#define INFINITY 65535
typedef int Pathmatirx[MAXVEX]; //存储最短路径的下标数组
typedef int ShortPathTable[MAXVEX]; //存储到各点最短路径的权值和
//针对有向网G的v0顶点到其余顶点v最短路径P[v]及带权长度D[v]
//P[v]的值为前驱顶点下标,D[v]表示v0到v的最短路径长度和
void ShortestPath_Dijkstra(MGraph G,int v0,Pathmatirx *P,ShortPathTable *D)
{
int v,w,k,min;
int final[MAXVEX]; //最短路径标志位,设为1表示最短路径已求
for(v=0;v<G.numVertexes;v++) //初始化数据
{
final[v] = 0;
(*D)[v] = G.matirx[v0][v]; //将与v0有连线的顶点加上权值
(*P)[v] = 0; //初始化路径数组P为0
}
(*D)[v0] = 0; //v0到v0路径为0
final[v0] = 1; //v0到v0不需要求路径
/*开始主循环,每次求得v0到某个v顶点的最短路径*/
for(v=1;v<G.numVertexes;v++)
{
min = INFINITY; //设当前离v0顶点的最近距离
for(w=0;w<G.numVertexes;w++) //寻找离v0最近的顶点
{
if(!final[w] && (*D)[w]<min)
{
k = w; //记录顶点w记为k
min = (*D)[w]; //顶点k离v0最近
}
}
final[k] = 1; //将目前找到的最近的顶点设为1
for(w=0;w<G.numVertexes;w++) //修正比较最短的路径距离
{
//增加了最短距离v0-k之后,基于此更新所有的其他顶点到v0的最短路径值
if(!fianl[w] && (min+G.matirx[k][w]<(*D)[w]))
//说明找到了更短的路径,故修改D[w]和P[w]
(*D)[w] = min + G.matirx[k][w];
(*P)[w] = k;
}
}
}
该算法的时间复杂度为O(n^2)
2.弗洛伊德算法
若顶点数为n,使用两个nxn的矩阵来D-1和P-1,根据以下公式
循环修改D矩阵,同时P对应矩阵也根据当前D的变化进行修改,注意D为对称矩阵,可简化计算,例子如下
算法如下
#define MAXVEX 9
#define INFINITY 65535
typedef int Pathmatirx[MAXVEX][MAXVEX]; //存储最短路径的下标数组
typedef int ShortPathTable[MAXVEX][MAXVEX]; //存储到各点最短路径的权值和
//针对有向网G的各顶点v到其余顶点w最短路径P[v][w]及带权长度D[v][w]
//P[v]的值为前驱顶点下标,D[v]表示v0到v的最短路径长度和
void ShortestPath_Floyd(MGraph G,int v0,Pathmatirx *P,ShortPathTable *D)
{
int v,w,k;
for(v=0;v<G.numVertexes;v++) //初始化D、P
{
for(w=0;w<G.numVertexes;w++)
{
(*D)[v][w] = G.matirx[v][w]; //初始化权值
(*P)[v][w] = w; //初始化P
}
}
for(k=0;k<G.numVertexes;k++)
{
for(v=0;v<G.numVertexes;v++)
{
for(w=0;w<G.numVertexes;w++)
{
if((*D)[v][w]>(*D)[v][k]+(*D)[k][w])
//如果经过下标为k的顶点的路径比原来两点之间更短
//将两点间的权值设为更小的一个
(*D)[v][w] = (*D)[v][k] +(*D)[k][w];
(*P)[v][w] = (*P)[v][k]; //路径设置为经过下标为k的顶点
}
}
}
}
//由最后的P得出最短路径
for(v=0;v<G.numVertexes;v++)
{
for(w=v+1;w<G.numVertexes;w++)
{
printf("v%d - v%d weight: %d",v,w,D[v][w]);
k = P[v][w]; //获得第一个路径顶点下标
printf("path :%d",v); //打印源点
while(k!=w)
{
printf(" -> %d",k); //打印路径顶点
k = P[v][w]; //获得下一个路径顶点下标
}
printf(" -> %d\n",w); //打印终点
}
printf("\n");
}
以上弗洛伊德算法能算出所有顶点到所有顶点之间的最短路径,其主要就是二重循环初始化加上三重循环权值修正。但是其时间复杂度为O(n^3)。