最短路径问题是图论研究中的一个经典问题, 旨在寻找图中两结点之间的最短路径,即从某顶点出发,沿图的边到达另一顶点所经过的路径中,各边上权值之和最小的一条路径——最短路径。
最短路径计算根据原点的数量,可以分为单源点最短路径和全源最短路径。单源点的最短路径问题,是在带权有向图中,求指定的一个顶点到其余各点的最短路径;全源最短路径是在有向或无向带权图中,求出所有顶点对之间的最短路径。
求单源点最短路径经典算法有 Dijkstra 算法和 Bellman-Ford 算法。全源最短路径算法主要有 Floyd-Warshall 算法和 Johnson 算法。
最短路径计算从运算数据是否变化,可分为静态最短路计算和动态最短路计算。静态路径最短算法是外界环境不变,计算最短路径,主要有 Dijkstra 算法、A*(A Star)算法。动态路径最短路径计算,是在外界环境不断发生变化,即不能预测的情况下计算最短路径,如在游戏中敌人或障碍物不断移动的情况下,典型的有 D* 算法。
单源最短路径算法——Dijkstra 算法
Dijkstra 算法是典型的最短路径路由算法(为数据包从出发端到目的端选择路径的过程被称为“路由”,相应的算法为“路由算法”),用于计算有向图中一个顶点到其他所有顶点的最短路径。算法使用了广度优先搜索策略,解决非负权值有向图的单源最短路径问题,算法最终得到一个最短路径树。
- 问题描述
给定带权有向图 G 和源点 v,求从 v 到 G 中其余各项点的最短路径。图中权值非负。
- 问题分析
求从源点 v 到 G 中其余各项点的最短路径,一种可能的方法是枚举出所有路径,并计算出每条路径的长度,然后选择最短的一条。
从一个顶点 v 到任意顶点 vi 的最短路径不外乎两种可能,一是从 v 到 vi 只有一条路径,则它就是最短路径;二是从 v 到 vi 有多条路径,则需要在这 n 条路径中确定哪一条最短。
中间点:第八行二列单元格(v2,v3)。
- Dijkstra 算法思路描述
(a):设源点为 v0,源点 v0 的值为 0,v0 到其余点最短路径为 ∞,(路径值记录在各项点圆圈中)。
(b):首次从 v0 出发有3条直接的路径,分别到顶点 v2、v4、v5,长度分别为 10 、30 、100,修改相应顶点圈中的值,其中最短的路径对应的顶点为 v2;
(c):确定新的扩展点 u=v2(v2点变深色),以 u 做中间点:Dist(v0,v3)=Dist(v0,v2)+Dist(v2,v3)=60,比原先的值 ∞ 小,修改之。
此时,v0 到 v3、v4、v5的路径中(v0到v2的最短距离已经确认,就不再考虑v2点),最短的路径对应顶点为 v4,距离为 30 。
(d):u=v4,可以修改 v3、v5点的值。
(e):u=v3,无修改。
(f):u=v5,与 v0 有通路的顶点均已处理完毕。
把上面的计算过程的中间结果,记录下来。
- Dijkstra 算法具体步骤
(1)在Dist中找最小值对应的点u;将u加入S。
(2)以u做中间点,分别计算源点v0到其他各顶点的距离;若小于原来的距离,则修改Dist和Path数组。
具体到数据结构,则可表述为:
如果Dist[x]>Dist[u]+AdjMatrix[u][x],则Dist[x]=Dist[u]+AdjMatrix[u][x]。
(3)重复步骤(1)(2)直到所有与v0有路径的顶点全部都加入到S中。
为什么 Dijkstra 算法的限定条件为非负权值?
讨论:当所有边权都为正时,由于不会存在一个距离更短的没扩展过的点 u,所以 v0 到这个 u 点的距离一旦确认就不会再被改变;若存在负权边,则会在扩展时产生更短的距离,有可能就迫害了已经更新的点的距离不会改变的性质。所以只有当所有边权都非负时,才能保证算法的正确性,故用 Dijkstra 求最短路的图不能有负权边。
- 算法代码实现
#include <stdio.h>
#define N 20 //图的最大顶点数
#define MAX 32767
typedef struct //图的邻接矩阵类型
{
int AdjMatrix[N][N]; //邻接矩阵AdjMatrix
int VexNum,ArcNum; //顶点数,弧数
//int vexs[N]; //存放顶点信息---如该顶点的下一个顶点
} AM_Graph;
void DisplayAM(AM_Graph g); //输出邻接矩阵
void Dijkstra(AM_Graph g,int v0); //Dijkstra算法从顶点v0到其余各顶点的最短路径
void DisplayPath(int dist[],int path[],int s[],int n,int v0); //由path计算最短路径
void PPath(int path[],int i,int v0);
/*======================================
Dijkstra算法
函数功能:从源点到其余各顶点的最短路径
函数输入:图的邻接矩阵、源点v0
函数输出:无
=========================================*/
void Dijkstra(AM_Graph g,int v0)
{
int i,j;
int Dist[N]; //最短距离数组,记录v0到顶点j的最短距离
int Path[N]; //中间点路径数组,记录顶点j的前一个顶点(j的前趋)
int S[N]; //最短路径顶点集,值 1:顶点入集,值0:顶点未入集
int MinDis; //距v0的最小距离
int u; //距v0最小距离的顶点
for (i=0;i<g.VexNum;i++)
{
Dist[i]=g.AdjMatrix[v0][i]; //距离初始化
if (g.AdjMatrix[v0][i]<MAX) //若v0到i有路径
Path[i]=v0; //i的前趋为v0
else Path[i]=-1; //i无前趋,标记为-1
S[i]=0; //S[]=0,表示顶点i不在S集中
}
S[v0]=1; //首次,源点v0入S集
for (i=0;i<g.VexNum;i++)
{
MinDis=MAX; //初始时设置到v0的最小距离为MAX
u=-1; //u为-1表示无对应顶点
//在Dist中找最小值及对应顶点u
for (j=0; j<g.VexNum; j++)
if (S[j]==0 && Dist[j]<MinDis)
{
MinDis=Dist[j];
u=j;
}
if(MinDis!=MAX) S[u]=1;//顶点u加入S集中
else break;
//以u做中间点,查看v0到其他各点的距离
for (j=0;j<g.VexNum;j++)
{//选取不在S集中且与u连通的点j
if (S[j]==0 && g.AdjMatrix[u][j]<MAX)
{ //若[v0到j的距离]>[v0到u的距离+u到j的距离]
if (Dist[j]>Dist[u]+g.AdjMatrix[u][j])
{
//修改v0到j的距离
Dist[j]=Dist[u]+g.AdjMatrix[u][j];
Path[j]=u;//修改j的前趋点
}
}
}
}
printf("输出最短路径:\n");
DisplayPath(Dist,Path,S,g.VexNum,v0); //输出最短路径
}
/*===========================================
函数功能:输出源点到其余各点的最短路径
函数输入:最短距离数组、路径数组、最短路径顶点集、顶点数、源点
函数输出:无
===========================================*/
void DisplayPath(int Dist[],int Path[],int S[],int n,int v0)
{
int i;
for (i=0;i<n;i++)
if (S[i]==1 && i!=v0) //在S集中的顶点才有路径输出
{
printf("从%d到%d的最短路径长度为:%d",v0,i,Dist[i]);
printf("\t路径为:%d—",v0);
PPath(Path,i,v0);
printf("%d\n",i);
}
else
printf("从%d到%d不存在路径\n",v0,i);
}
/*==================================
函数功能:打印源点到指定顶点的最短路径
函数输入:路径数组、终点、源点
函数输出:无
屏幕输出:最短路径
=====================================*/
void PPath(int Path[],int i,int v0)
{
int k=Path[i];
if (k==v0) return;
else PPath(Path,k,v0);
printf("%d—",k);
}
/*==================================
函数功能:输出邻接矩阵
函数输入:邻接矩阵
函数输出:无
屏幕输出:邻接矩阵
=====================================*/
void DisplayAM(AM_Graph g)
{
int i,j;
for (i=0;i<g.VexNum;i++)
{
for (j=0; j<g.VexNum; j++)
{
if (g.AdjMatrix[i][j]==MAX) printf("%4s","∞");
else printf("%4d",g.AdjMatrix[i][j]);
}
printf("\n");
}
}
int main()
{
int A[N][6]={{MAX,MAX,10 ,MAX,30 ,100},
{MAX,MAX,5 ,MAX,MAX,MAX},
{MAX,MAX,MAX,50 ,MAX,MAX},
{MAX,MAX,MAX,MAX,MAX,10 },
{MAX,MAX,MAX,20 ,MAX,60 },
{MAX,MAX,MAX,MAX,MAX,MAX}
};
AM_Graph g; //定义邻接矩阵g
g.VexNum=6;
g.ArcNum=8;
for (int i=0;i<g.VexNum;i++) //给邻接矩阵赋值
for (int j=0;j<g.VexNum;j++)
g.AdjMatrix[i][j]=A[i][j];
printf("有向图G的邻接矩阵:\n");
DisplayAM(g); //输出邻接矩阵
int v0=1; //设置起始点
Dijkstra(g,v0);
return 0;
}
Dijkstra算法每次循环权值最小结点加入到 S集内;
每次结点加入S集,都需要更新Dist数组,将较小者加入Dist数组内;
算法使用循环实现,若使用递归呢?
各顶点对间最短路径算法——Floyd 算法
Floyd-Warshall 算法,又称为插点法,是一种用于寻找给定的加权图中多源点之间最短路径的算法。通常可以在任何图中使用,包括有向图、带负权边的图。
(1)问题描述:求带权图中各顶点对间的最短距离。
(2)问题分析:首先设置 Dist 矩阵和 Path 矩阵来记录顶点间的距离和对应的中间点。
步骤2:
(1)Path(-1)矩阵:顶点 2 到顶点 1,经过中间点 2,记为 Path(2,1)=2
Dist(-1)矩阵:顶点 2 到 1 路径长度为 5,记为 Dist(2,1)=5
(2)以顶点 0 为中间点
因为:Dist(2,1)=Dist(2,0)+Dist(0,1)=3+1=4<5
故有:
Dist(0)矩阵中:Dist(2,1)=4
Path(0)矩阵中:Path(2,1)=0
同理 Dist(2,3)=7;Path(2,3)=0
步骤 3 至 5 中 Dist 和 Path 矩阵的变化原理一样。最后将各顶点间的最短路径值以及经过的顶点路径列表给出。
- Floyd 算法描述
- 程序实现
#include<stdio.h>
#define N 100 //图最大顶点个数
#define MAX 32767
typedef struct //图的邻接矩阵类型
{
int AdjMatrix[N][N]; //邻接矩阵
int VexNum,ArcNum; //顶点数,弧数
//int vexs[N]; //存放顶点信息——如该顶点的下一个顶点
} AM_Graph;
void DisplayAM(AM_Graph g); //输出邻接矩阵
void Floyd(AM_Graph g); //弗洛伊德算法——计算每对顶点之间的最短路径
void DisplayPath(int A[][N],int Path[][N],int n); //输出路径
void PPath(int Path[][N],int i,int j);
int main()
{
int A[N][4]={ {0,1,MAX,4},
{MAX,0,9,2},
{3,5,0,8},
{MAX,MAX,6,0 },
};
AM_Graph g; //定义邻接矩阵
g.VexNum=4;
g.ArcNum=8; //4个顶点,8条边
//给邻接矩阵赋值
for (int i=0;i<g.VexNum;i++)
for (int j=0;j<g.VexNum;j++)
g.AdjMatrix[i][j]=A[i][j];
printf("有向图G的邻接矩阵:\n");
DisplayAM(g); //输出邻接矩阵
Floyd(g); //调用算法并输出每两点之间的距离
return 0;
}
/*======================================
Floyd算法
函数功能:求出图中每对顶点之间的最短路径
函数输入:邻接矩阵
函数输出:无
=========================================*/
void Floyd(AM_Graph g)
{
int i,j,k;
int Dist[N][N],Path[N][N];//Dist矩阵记录各点间的最小距离,path矩阵记录路径的中间结点
//Dist和Path矩阵初始化
for (i=0;i<g.VexNum;i++)
{
for (j=0;j<g.VexNum;j++)
{
Dist[i][j]=g.AdjMatrix[i][j]; //Dist矩阵初始状态为邻接矩阵
Path[i][j]=-1; //无中间点时为-1
}
}
//分别以图中各点做中间点k,遍历Dist矩阵
for (k=0; k<g.VexNum; k++)
{
//在Dist矩阵中查看经过k点到其他结点的距离有无改善(变小)
for (i=0;i<g.VexNum;i++)
for (j=0;j<g.VexNum;j++)
{
//经过k点的ij距离比原先小
if (Dist[i][j]>(Dist[i][k]+Dist[k][j]))
{
Dist[i][j]=Dist[i][k]+Dist[k][j]; //修改顶点ij间的距离
Path[i][j]=k;//记录中间点k
}
}
}
printf("\n输出最短路径:\n");
DisplayPath(Dist,Path,g.VexNum); //输出最短路径
}
/*======================================
函数功能:输出各点的最短路径
函数输入:邻接矩阵、路径数组、顶点数
函数输出:无
======================================*/
void DisplayPath(int A[][N],int Path[][N],int n)
{
int i,j;
for (i=0;i<n;i++)
for (j=0;j<n;j++)
if(A[i][j]==MAX)
{
if(i!=j)//交通路径中——到达自身结点本就是0距离
printf("从%d到%d没有路径\n",i,j);
}
else
{
printf("从%d到%d路径长度为:%d",i,j,A[i][j]);
printf("\t路径为:");printf("%d-",i);PPath(Path,i,j);
printf("%d\n",j);
}
}
/*======================================
函数功能:打印指定两个顶点间的路径
函数输入:路径数组、顶点1、顶点2
函数输出:无
屏幕输出:顶点间路径
========================================*/
void PPath(int Path[][N],int i,int j)
{
int k=Path[i][j];
if (k==-1) return;
PPath(Path,i,k);
printf("%d-",k);
PPath(Path,k,j);
}
/*======================================
函数功能:输出邻接矩阵
函数输入:邻接矩阵
函数输出:无
屏幕输出:邻接矩阵
======================================*/
void DisplayAM(AM_Graph g)/*输出邻接矩阵*/
{
int i,j;
for (i=0;i<g.VexNum;i++)
{
for (j=0;j<g.VexNum;j++)
if (g.AdjMatrix[i][j]==MAX)
printf("%4s","∞");
else printf("%4d",g.AdjMatrix[i][j]);
printf("\n");
}
}
循环变量 k ,从上一步矩阵取始点到该变量的距离,加该变量到终点的距离同上一步矩阵始点到终点距离比较,取较小值存于矩阵。
- 时间复杂度分析
每一个顶点分别要与其他 n-1 个顶点作边的长度比较,因此其时间复杂度为 O(n^3) 。
最短路径问题小结
Floyd算法时间复杂度比较高,不适合计算大量数据。Floyd算法不能直观反映出各个顶点之间最短路径序列的先后关系。