两个顶点之间的最短路径问题就是求一条路径可以令两顶点沿途各边权值之和最小。
对于这个问题,可以分为两种情况:
1.单源最短路径:从固定起点出发,求最短路径;
2.多源最短路径:求任意两顶点间最短路径。
除此以外,在每种情况内部有权图跟无权图,有向图跟无向图,
但有权图是无权图的一般形式,而有向图也是无向图的一般形式,所以在讨论中我们只会简单提及特殊形式,而更看重有权有向图的普适性。
虽为无权,其实可以视作各边的权重相同,那么问题就转换成了我们熟悉的BFS问题,两点间的最短路径也就名副其实了,BFS可以很方便的求出最短路径。
复杂度也容易得知,为O(|V|+|E|),意味着每条边跟每个节点最多访问一次。
我们最终的目标还是需要求出这个最短路径的具体内容,于是乎我们需要通过一个特殊的数组来回溯路径,每个元素的下标代表着这个节点的标号,而元素中的数据就是这个节点在路径中的上一个节点
例如:若起点为节点1,终点为节点8,此时有 path[8]=6,path[6]=4,path[4]=1,即意味着经过上述操作后,通过给定的节点8便可回溯得知路径为:8->6->4->1。
在讨论问题之前我们先要确定这个问题的前提:各边权值非负。
在右图中,存在一个边的权值为负,且每经过这个“负值圈”,都可令权值和变小,那么在这种情况下则不存在最短路径。
思路有点像动态规划跟BFS的结合,由已知推未知,步步求优,又是一个层次向下一个层次的不断推广
#include
using namespace std;
#define MaxNum 50
#define MaxInt 32767
#define MaxEdgeNum 50
//邻接矩阵
typedef int VertexType;
typedef int EdgeType;
struct Graph{
VertexType vexs[MaxNum];//顶点表
EdgeType arcs[MaxNum][MaxNum];//邻接矩阵表
int vexnum,edgenum;//顶点数,边数
};
void createGraph(Graph *H){
printf("请输入顶点数:");
cin>>H->vexnum;
printf("\n请输入边数:");
cin>>H->edgenum;
//初始化顶点表
for(int i=0;i<H->vexnum;i++){
H->vexs[i]=i;
}
for(int i=0;i<H->vexnum;i++){
for(int j=0;j<H->vexnum;j++){
H->arcs[i][j]=MaxInt;
if(i==j) H->arcs[i][j]=0;
}
}
printf("请输入边的信息:\n");
for(int i=0;i<H->edgenum;i++){
int x,y,w;
scanf("%d%d%d",&x,&y,&w);
H->arcs[x][y]=w;
}
}
//------------------------------------------------------------------
void Dijkstra(Graph *H,int dist[],int path[],int v){//此时v为起点
int n=H->vexnum;
int set[n];//set数组用于记录该顶点是否被标记,即是否经过
//1.初始化起点,并更新其周围的邻接点
for(int i=0;i<n;i++){
set[i]=0;
dist[i]=H->arcs[v][i];//有向图,只需要在邻接矩阵里面v的那一行中检索即可
(dist[i]<MaxInt)?path[i]=v:path[i]=-1;
//当小于即可更新数据,说明两点相邻
}
set[v]=1; //标记,表示这个点已经被经过
path[v]=-1; //起点不存在“父节点 ”,需要从值v更正为-1
//2.处理完起点之后,处理其余顶点
for(int i=1;i<n;i++){//共n-1个顶点
int min=MaxInt;
//第一步:从剩余的顶点中选出一个dist最小的顶点
//全扫描,适用于稠密图,边的个数远多于顶点个数,时间复杂度为 O(|V|^2+|E|)
for(int j=0;j<n;j++){
if(set[j]==0&&dist[j]<min){
v=j;
min=dist[j];
}
}
/* 最小堆,适用于稀疏图,边的个数与顶点数相差不大,时间复杂度为O(|E|*log|V|)
将会专门用一篇博客来讲*/
set[v]=1;
//第二步:在将新结点并入后,起点到各顶点的最短距离有可能将会发生变化
/*对顶点V的未被标记过的邻接点的dist值进行更新,设该点为W,W可能会被多次更新
下面没有直接区分邻接点,但非邻接点的边已经被初始化为了MaxInt*/
for(int j=0;j<n;j++){
if(set[j]==0&&dist[v]+H->arcs[v][j]<dist[j]){
dist[j]=dist[v]+H->arcs[v][j];
path[j]=v;
}
}
}
}
//------------------------------------------------------------------
void print(Graph g,int dist[],int path[]){
//这里仅仅是以表格形式输出结果,如果要实现仅输入终点便可输出路径的功能可以使用递归
int n=g.vexnum;
printf(" ");
for(int i=0;i<n;i++) printf("%d ",i);
printf("\ndist[]:");
for(int i=0;i<n;i++) printf("%d ",dist[i]);
printf("\npath[]:");
for(int i=0;i<n;i++) printf("%d ",path[i]);
}
int main(){
Graph g;
createGraph(&g);
int dist[g.vexnum];//用来记录“局部 ”最优解,到起点的最短路径和
int path[g.vexnum];//用来回溯路径
Dijkstra(&g,dist,path,0);
print(g,dist,path);//以表格形式输出
}
朴素Dijkstra模板
朴素Dijkstra例题
#include
#include
using namespace std;
const int N = 505;
int g[N][N];
bool st[N];
int dis[N];
int n,m;
int d(){
memset(dis,0x3f,sizeof dis);
int t=-1;
dis[1]=0;
for(int i=0;i<n;i++){
for(int j=1;j<=n;j++)
if(!st[j]&&(t==-1||dis[t]>dis[j]))
t=j;
st[t]=1;
for(int j=1;j<=n;j++){
if(!st[j])
dis[j]=min(dis[j],dis[t]+g[t][j]);
}
}
if(dis[n]==0x3f3f3f)return -1;
else return dis[n];
}
int main(){
cin>>n>>m;
memset(g,0x3f,sizeof g);
for(int i=0;i<m;i++){
int x,y,z;
cin>>x>>y>>z;
if(x==y)continue;
g[x][y]=min(g[x][y],z);
}
cout<<d()<<endl;
}
对于朴素Dijkstra算法来说时间复杂度为O( n 2 n^2 n2),只与节点数有关,所以适用于稠密图,也就是边数比较多的图中,而我们通常用邻接矩阵来存储稠密图。
对于多源问题,可以将其视为单源的更一般化的形式,那么我们的问题就转化为了如何将不同的单源最短路径结合在一起。
在循环中重复调用单源算法,那么我们已知原来的时间复杂度为O(|V|^2+|E|),而此时需要重复调用|V|次,于是枚举思路的时间复杂度为:
O(|V|^3+|E|*|V|)
这种思路适用于稀疏图。
对于顶点 i,j,满足i不等于j的前提下,求dist[i][j],我们可以引入另一个顶点k,将i到j的过程划分为i->k->j(k可以与i或j的值相同,dist[i][i]=0,即便i,j相邻也无影响)。
由此可知,存在式子:
dist[i][j]=min(dist[i][j],dist[i][k]+dist[k][j])
这个式子让我们联想到了动态规划,对于变量i,j,k只需要使用三重循环,便可以保证能够自左向右,自上而下**的遍历每种情况,并不断完善dist的结果
#include
using namespace std;
#define MaxNum 50
#define MaxInt 32767
#define MaxVexNum 50
//邻接矩阵
typedef int VertexType;
typedef int EdgeType;
struct Graph{
VertexType vexs[MaxNum];//顶点表
EdgeType arcs[MaxNum][MaxNum];//邻接矩阵表
int vexnum,edgenum;//顶点数,边数
};
void createGraph(Graph *H){
printf("请输入顶点数:");
cin>>H->vexnum;
printf("\n请输入边数:");
cin>>H->edgenum;
//初始化顶点表
for(int i=0;i<H->vexnum;i++){
H->vexs[i]=i;
}
for(int i=0;i<H->vexnum;i++){
for(int j=0;j<H->vexnum;j++){
H->arcs[i][j]=MaxInt;
if(i==j) H->arcs[i][j]=0;
}
}
printf("请输入边的信息:\n");
for(int i=0;i<H->edgenum;i++){
int x,y,w;
scanf("%d%d%d",&x,&y,&w);
H->arcs[x][y]=w;
}
}
//------------------------------------------------------------------
void Floyd(Graph g,int path[][MaxVexNum]){
int n=g.vexnum;
int dist[n][n];
//第一步:初始化path[][]和dist[][]数组
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
dist[i][j]=g.arcs[i][j];
path[i][j]=-1;
}
}
//第二步:三重循环,寻找最短路径
for(int k=0;k<n;k++){//第一层是代表中间结点
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(dist[i][j]>dist[i][k]+dist[k][j]){
dist[i][j]=dist[i][k]+dist[k][j];
path[i][j]=k;
}
}
}
}
}
//------------------------------------------------------------------
void PrintGraph(Graph g){//以列表形式输出各顶点之间的最短路径
printf("邻接矩阵为:\n");
for(int i=0;i<g.vexnum;i++) {
printf(" %d",g.vexs[i]);
}
printf("\n");
for(int i=0;i<g.vexnum;i++){
printf("%d ",g.vexs[i]);
for(int j=0;j<g.vexnum;j++){
if(g.arcs[i][j]==32767){
printf("∞ ");
}
else{
printf("%d ",g.arcs[i][j]);
}
}
printf("\n");
}
}
//------------------------------------------------------------------
int main(){
Graph g;
createGraph(&g);
PrintGraph(g);
int path[MaxVexNum][MaxVexNum];
Floyd(g,path);
//同样可以用递归回溯出任意两点间的最短路径
}