实验原理
单元最短路径算法,即在一个给定的图中,找到从某一源节点到其他所有节点的最短路径。这里我以Dijkstra为例,Dijkstra算法是一种贪心算法,用于计算一个节点到其他所有节点的最短路径。
Dijkstra算法基本原理是:
- 初始化一个距离数组,将源节点到自身的距离设为0,其他节点到源节点的距离设为无穷大。
- 创建一个未访问节点的队列,初始时包含所有节点。
- 从未访问节点集合中选择距离源节点最近的节点,标记为已访问,并更新其相邻节点的距离。
- 重复步骤3,直到所有节点都被访问。
Dijkstra算法的核心思想是:
每次从未访问节点中选择距离源节点最近的节点,并更新其相邻节点的最短路径估计。这个过程通过贪心策略逐步逼近最优解,Dijkstra算法的关键特性就是贪心策略,即在每一步都选择当前看起来最优的顶点(距离最短的顶点),并更新其邻接顶点的距离。算法通过选择当前未访问顶点中距离源点最近的顶点,保证了每次选择的顶点都是源点到该顶点的最短路径上的,最终实现求得单源最短路径。
流程分析
1.初始化:
创建一个距离数组dist,用于存储从源节点s到其他所有节点的最短距离。将dist[s]设为0(源节点到自身的距离为0),其他所有节点的距离设为无穷大(表示尚未访问,未知路径)。
再创建一个优先队列Q,用于存储待处理的节点及其当前估计的最短距离。将源节点s及其距离0加入队列。
2.执行循环操作:
当优先队列Q不为空时,重复以下步骤:从未访问顶点集合中选择一个距离源点最近的顶点,称为当前顶点u。
(1)更新距离:
对于当前顶点u的每一个邻接顶点v,计算通过当前顶点到达该邻接顶点的距离。如果新计算出的距离小于之前记录的距离,则更新该邻接顶点的距离。
(2)标记访问:
将当前顶点v标记为已访问,并从未访问顶点集合中移除。
(3)重复过程:
如果未访问顶点集合不为空,重复上述步骤;否则算法结束。
3.输出结果:
输出源点到所有其他顶点的最短路径和距离。
程序实现
#include<stdio.h>
#define MaxVerNum 30 // 顶点的最大个数
#define INF 32767 // 定义无穷大
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
// 邻接矩阵作为存储结构
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); // 获取顶点下标
// Dijsktra 狄杰斯特拉算法. 查找某顶点到其它任意顶点的最短路径
Status Dijsktra(DirAMGraph* G, int v, int djst[MaxVerNum], int path[MaxVerNum]);
// 打印起点到其它结点的最短路径 --Dijsktra
void PrintDijsktra(DirAMGraph* G, int djst[MaxVerNum], int path[MaxVerNum]);
// 获取顶点 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)
{
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);
getchar();
if ((65 <= v && v <= 90) || (97 <= v && v <= 122))
{
G->Verts[i] = v;
break;
}
printf("输入错误,请输入英文字母!\n");
} while (1);
}
//初始化所有边的权为无穷大
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;
break;
} while (1);
}
return OK;
}
void Print_DirGraph(DirAMGraph* G)
{
int i, j;
printf("\n ");
for (i = 0; i < G->VerNum; i++)
printf("%-7c", G->Verts[i]);
for (i = 0; i < G->VerNum; 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])
{
if (!G || !djst || !path)
return ERROR;
// 找到的最短路径集。
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])
{
if (path[v] == -1)
{
printf("%c", G->Verts[v]);
return;
}
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]);
}
}
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;
}
调试分析
(1)经过输入验证的调试,保证了图的有效性:确保图是连通的,且没有负权重的边;验证节点和边的数量:确保输入节点和边的数量一致,并且没有重复的边或节点;检查权重:确保所有边的权重为非负值。
(2)使用初始化检查进行调试,起点验证:确保选择的起点在图中存在。距离数组初始化:起点到自身的距离应为0,其他所有节点的距离为无穷大。访问数组初始化:所有节点均初始化为未访问。
(3)算法步骤逐步检查调试,选择未访问节点中距离最小的节点:使用优先队列来有效地选择节点。更新相邻节点的距离:对于每个选择的相邻节点,检查其所有相邻节点,如果通过当前节点到相邻节点的距离更短,则更新距离。标记节点为已访问:防止重复处理。
(4)边界条件处理调试: 孤立节点:如果图中存在孤立节点,确保这些节点在结果中的距离保持为无穷大。空图:如果输入为空图,确保算法能够处理并返回合理的错误或结果。单节点图:如果图中只有一个节点,确保算法返回的距离数组正确。
测试结果
首先初始化一个包含5个顶点、6条边的有向图,输入图的顶点为:a、b、c、d、e共5个顶点,6条边为:a->b,权重为1;a->c,权重为2;b->d,权重为3;c->d,权重为4;d->e,权重为5;e->b,权重为6,其余边权重皆为无穷大。
完成输入后,成功得到了相应的邻接矩阵。随后设置a为源节点,通过调用单源最短路径Dijkstra算法,并成功指出起点a到其余4个节点间的最短路径如下:
起点a到所有顶点的最短路径:
a-1->b -总开销:1
a-2->c -总开销:2
a-1->b-3->d -总开销:4
a-1->b-3->d-5->e -总开销:9