一、Dijskra 算法简介
1.0 引言
单源最短路径:指从同一个起始点到其它所有顶点的最短路径,共有 n-1 条最短路径。
多源最短路径:指每一对顶点的最短路径,共有 条对最短路径。
Dijskra 迪杰斯特拉 算法:解决带权图中的单源最短路径问题。
Floyd 弗洛伊德 算法:解决带权图中的多源最短路径问题。
1.1 Dijskra 算法思想
思想逻辑:
1. 设置三个数组,
Djst[n] 记录 起点至其它结点的最短路径的值,
Path[n] 记录 起点至其它结点的最短路径轨迹,
Set[n] 记录 是否已找到起点至其它结点最短路径(1-已找到,0-未找到)。
*数组下标对应结点的下标,例如:Dist[n] 表示 起点至结点 n 的最短路径值。
2. 初始,
Djst 数组中存储的是与起点直通的结点的路径值(非直通,则值为)
Path 数组中存储的是与起点直通的结点的下标(非直通,则为 -1)
Set 数组中存储的是0,表一条最短路径都没找到
3. 从Djst 数组中,选取权值最小的路径作为起点到此路径尾结点的最短路径,
然后 Path 数组记录最短路径尾结点的前驱结点(尾结点前面的第一个结点),
Set 数组再将最短路径的尾结点对应的值置为 1,表示已找到此结点最短路径。
4. 动态更新点至其它结点的最短路径值,即更新 Djst 数组。再重复步骤3和4,直到 Set[n] 全为 1,则找完了起点至其起它结点的最短路径。
*Dijskra 迪杰斯特拉 算法并不能直接得到最短路径,但将最短路径信息保存在了 Djst 数组和Path 数组中,可以通过 Djst 数组和 Path 数组间接得到完整的最短路径。
1.2 Dijskra 过程图解
1.3 Dijskra 核心问题
问题1 如何在起点至某一个结点的多条路径中得到那条权值最小的路径
应对策略:创建一个数组 Djst 保存起点至其它结点的当前最短路径,然而其中权值最小的那一条边就是真正的最短路径。
A) 为什么数组 Djst 中 权值最小的路径就是真正的最短路径,而其它的路径不是呢?
因为:有些结点对应的权值可能不是起点至这些结点真正的最短路径,因为在动态更新最短路径过程中,是在 直通路径+所有中转路径 中进行比较得到最短路径,但是又不能保证一轮寻找就得到中转结点至其相邻结点的最短路径,可能还有些中继结点至相邻结点的中转路径没有寻找。
B) 那如何动态更新得到起点至每一个其它结点最短路径呢?
如果得到每一个结点至其相邻结点的最短路径,然后寻找起点至相邻结点的最短路径,再由相邻结点出发寻找到与自己相邻的结点的最短路径,一直这样进行下去,真正的最短路径 = 每小段最短路径之和,就能得到起点至其它所有结点的最短路径了,由局部最优解得到全局最优解(局部最优解的叠加)。
问题2 如何得到起点至其它所有结点的最短路径(经过的点及边)
应对策略:创建一个 Path 数组用来保存每一个结点所在最短路径上的前驱结点,然后通过一步步向后回溯就得到完整的最短路径了
问题3 如何区分是否已经找到起点至某一结点的最短路径
应对策略:若找到起点至某一结点的最短路径,则对此结点作标记,将此结点在 Set 数组中对应位置的值 记为 1,表已找到;0表示未找到。
二、Dijskra 算法的实现
2.0 头文件
#include<stdio.h>
// 顶点的最大个数
#define MaxVerNum 30
#define INF 32767 // INF infinite 无穷大,表权重无穷大
// 状态值
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
// 状态码 -- 状态值
typedef int Status;
// 1.邻接矩阵 - 存储结构
// 定点、边的类型
typedef char VertexType; // 顶点的数据类型
typedef int ArcType; // 边的数据类型
// 构造数据类型
typedef struct
{
VertexType Verts[MaxVerNum];
ArcType DirArcs[MaxVerNum][MaxVerNum]; // 有向图 -- 矩阵表示法
int VerNum; // 顶点个数
int ArcNum; // 边的个数
}DirAMGraph; // Adjacency Matrix Graph 邻接矩阵
// 函数声明
Status CreateDN(DirAMGraph* G); // 创建有向图
void Print_DirGraph(DirAMGraph* G); // 打印有向图的邻接矩阵
int LocateDirVer(DirAMGraph* G, VertexType* v); // 获取 顶点 ver 的下标
// Dijsktra 狄杰斯特拉算法. 查找某顶点到其它任意顶点的最短路径
Status Dijsktra(DirAMGraph* G, int v, int djst[MaxVerNum], int path[MaxVerNum]);
// 打印起点到其它结点的最短路径 --Dijsktra
void PrintDijsktra(DirAMGraph* G, int djst[MaxVerNum], int path[MaxVerNum]);
2.1 函数实现文件
// 获取 顶点 ver 的下标
int LocateDirVer(DirAMGraph* G,VertexType* v)
{
if (!G) return ERROR;
int i;
for (i = 0; i < G->VerNum; i++)
if (*v == G->Verts[i])
return i;
return -1; // 返回 -1 表示未找到顶点
}
// 创建有向图
Status CreateDN(DirAMGraph* G) // UndirectNet
{
if (!G) return ERROR;
printf("请输入顶点及边个数(Vers Arcs): ");
scanf("%d %d", &G->VerNum, &G->ArcNum);
getchar();
int i;
//录入顶点
printf("\n请输入顶点的值(英文字母): ");
for (i = 0; i < G->VerNum; i++)
{
do
{
VertexType v;
scanf("%c", &v); //scanf("%[a-zA-Z]", G->VerNum); // 只接收26个英文字母
getchar();
if ((65 <= v && v <= 90) || (97 <= v && v <= 122))
{
G->Verts[i] = v;
break;
}
printf("输入错误,请输入英文字母!\n");
} while (1); // do-while循环用于处理错误输入
}
//初始化所有边的权
int j;
for (i = 0; i < G->VerNum; i++)
for (j = i; j < G->VerNum; j++)
{
G->DirArcs[i][j] = INF; // 权重为无穷大,表示两顶点非邻接
G->DirArcs[j][i] = INF;
}
//录入边的权值
printf("\n请输入边关联的顶点及权值(v1 v2 weight): \n");
for (i = 0; i < G->ArcNum; i++)
{
VertexType v1, v2;
int w;
do
{
scanf("%c %c %d", &v1, &v2, &w);
getchar();
if (v1 < 65 || (90 < v1 && v1 < 97) || 122 < v1)
{
printf("输入错误,请输入英文字母!\n");
continue;
}
if (v2 < 65 || (90 < v2 && v2 < 97) || 122 < v2)
{
printf("输入错误,请输入英文字母!\n");
continue;
}
//查找顶点位置
int a, b;
a = LocateDirVer(G, &v1);
b = LocateDirVer(G, &v2);
if (a < 0) // 判断顶点是否存在
{
printf("输入的顶点%c不存在,请重新输入!\n", v1);
continue;
}
if (b < 0) // 判断顶点是否存在
{
printf("输入的顶点%c不存在,请重新输入!\n", v2);
continue;
}
//链接到两顶点的边赋权值
G->DirArcs[a][b] = w;
//G->DirArcs[b][a] = w;
break;
} while (1); // do-while循环用于处理错误输入
}
return OK;
}
void Print_DirGraph(DirAMGraph* G)
{
int i, j;
printf("\n ");
for (i = 0; i < G->VerNum; i++)
printf("%-7c", G->Verts[i]);
//printf("\n %5c %5c %5c %5c %5c",);
for (i = 0; i < G->VerNum; i++)
{
//printf("%c ", 'A' + i);
for (j = 0; j < G->VerNum; j++)
{
if (j == 0) printf("\n%c ", G->Verts[i]);
printf("%-7d", G->DirArcs[i][j]);
}
}
}
Status Dijsktra(DirAMGraph* G, int star, int djst[MaxVerNum], int path[MaxVerNum])
{
/*
思路:
1.用三个数组,分别记录:
a.起点起点到其它顶点的最短路径开销
b.起点走最短路径到其它结点所经过的中介点,
且这个中介点是与结点直接相连的直接中介点(如:a->b->c->d 中c是d的直接中介点,而b不是)
c.已经找到的距离起点路径开销最小的结点
2.初始阶段,直接找与起点直连的结点,并将起点与它们之间的路径开销视作最小开销。起点属于最短路径集合
3.在不属于最短路径集合的结点中,找出到起点路径开销最小的结点,然后合并到最短路径集合
4.动态更新起点到非最短路径集合结点的最小路径开销
a.起点到中介点的开销 + 中介点到目的结点的开销 是否 小于之前计算得到的开销值,若是,则更新开销值
这里的逻辑是:局部最优,则整体最优。
如:a->b 是最优解,若 b->c是最优解,则 a->b->c 也是最优解
又比如:有一个复合函数 F(x) = f1(x) + f2(x) + ... + fn(x),求复合函数 F(x) 的最小值
你会怎么求?求出 f1,f2,...,fn 的最小值,然后加起来不就是 F(x)的最小值吗?
*/
//处理空指针、不存在的数组
if (!G || !djst || !path)
return ERROR;
// 找到的最短路径集。Shortest Path Set
int Set[MaxVerNum] = { 0 }; // 0表不属于最短路径集合,1表属于最短路径集合
int i;
for (i = 0; i < G->VerNum; i++)
{
//初始化起点到其它顶点的 最小路径开销值
djst[i] = G->DirArcs[star][i];
if (djst[i] < INF) // 权重小于无穷大,则两顶点连通
path[i] = star; // 将起点设置为与其相连通结点的前端结点
else
path[i] = -1; //初始化与起点非连通的结点的前端结点为-1,表示不是其中介点
}
Set[star] = 1; // 起点属于最短路径集合
int j, k;
for (i = 0; i < G->VerNum - 1; i++)
{
int min = INF,idx = star;
// 寻找起点到其它顶点的最小路径,然后将其并入最短路径
for (j = 0; j < G->VerNum; j++)
{
// 起点到其它结点路径最小的结点,且未不属于最短路径集合的结点
if (djst[j] < min && Set[j] != 1)
{
idx = j;
min = djst[j];
}
}
Set[idx] = 1; // 找到的最小路径,合并到最短路径集合
// 动态更新起点到其它结点的路径最小开销值
for(j = 0; j < G->VerNum; j++) // j 表从起点出发到其它结点所经过的中介点
for (k = 0; k < G->VerNum; k++)
{
// 若起点到中介点的路径开销 + 中介点到其它顶点的开销 < 之前计算的起点到其它结点的开销,就更新开销值
if (djst[j] + G->DirArcs[j][k] < djst[k])
{
// 更新起点到其它结点的 路径最小开销值
djst[k] = djst[j] + G->DirArcs[j][k];
// 起点到其它结点的路径最小开销值更新后,记录结点的前端结点(也是中介点)
path[k] = j;
}
}
}
return OK;
}
void Trail(DirAMGraph* G, int v, int djst[MaxVerNum], int path[MaxVerNum])
{
//若顶点 v 为起点 -1,则输出顶点
if (path[v] == -1)
{
printf("%c", G->Verts[v]);
return;
}
//顶点 v 不是起点,则寻找顶点 v 的直接中介点
Trail(G, path[v], djst, path);
// 打印当前结点及路径开销
int node = path[v];
printf("-%d->%c", G->DirArcs[node][v],G->Verts[v]);
}
// 打印起点到其它结点的最短路径
void PrintDijsktra(DirAMGraph* G, int djst[MaxVerNum], int path[MaxVerNum])
{
// 处理不存在的数组
if (!djst || !path) return;
int i,star = 0;
//找起点
for (i = 0; i < G->VerNum; i++)
if (path[i] == -1)
{
star = path[i];
break;
}
for (i = 0; i < G->VerNum; i++)
{
if (path[i] == star) continue;
Trail(G, i,djst, path);
printf(" -总开销:%d\n",djst[i]);
}
}
2.3 主函数
int main()
{
DirAMGraph amg;
CreateDN(&amg);
Print_DirGraph(&amg);
int path[MaxVerNum];
int djst[MaxVerNum];
VertexType v;
printf("\n请输入最短路径的起点: ");
scanf("%c", &v);
int index = LocateDirVer(&amg, &v);
if (index < 0)
{
printf("图中不存在顶点%c\n", v);
return 0;
}
Dijsktra(&amg, index, djst, path);
int i;
// 打印下标
printf("\n下标: ");
for (i = 0; i < amg.VerNum; i++)
printf("%-6d", i);
//打印起点到各顶点的最短路径开销
printf("\ndjst: ");
for (i = 0; i < amg.VerNum; i++)
printf("%-6d",djst[i]);
//打印路径
printf("\npath: ");
for (i = 0; i < amg.VerNum; i++)
printf("%-6d",path[i]);
printf("\n");
printf("\n起点%c到所有顶点的最短路径: \n",v);
PrintDijsktra(&amg, djst, path);
return 0;
}
/*
B -→7--→ E
↗ ↘1 6↗↑↘6
↗4 ↘ ↗ ↑ ↘
A--→6---→ C ↑1 G
↘6 ↗ ↘ ↑ ↗
↘ ↗2 4↘↑↗8
D -→5--→ F
请输入顶点及边个数(Vers Arcs): 7 12
请输入顶点的值(英文字母): A B C D E F G
请输入边关联的顶点及权值(v1 v2 weight):
A B 4
A C 6
A D 6
B C 1
B E 7
C E 6
C F 4
D C 2
D F 5
E G 6
F E 1
F G 8
A B C D E F G
A 32767 4 6 6 32767 32767 32767
B 32767 32767 1 32767 7 32767 32767
C 32767 32767 32767 32767 6 4 32767
D 32767 32767 2 32767 32767 5 32767
E 32767 32767 32767 32767 32767 32767 6
F 32767 32767 32767 32767 1 32767 8
G 32767 32767 32767 32767 32767 32767 32767
请输入最短路径的起点: A
下标: 0 1 2 3 4 5 6
djst: 32767 4 5 6 10 9 16
path: -1 0 1 0 5 2 4
起点A到所有顶点的最短路径:
A-4->B -总开销:4
A-4->B-1->C -总开销:5
A-6->D -总开销:6
A-4->B-1->C-4->F-1->E -总开销:10
A-4->B-1->C-4->F -总开销:9
A-4->B-1->C-4->F-1->E-6->G -总开销:16
*/
em ~ ~ ~ ~ ~
在此,感谢您的倾心阅读!
在茫茫人海中,你我能够相遇属实是一件难得的事!
以上是本人的一些浅显理解,如有不妥之处,还望指出,咋们共同进步哟!