24. 最短路径
一、最短路径问题
图的最短路径问题是指在加权图中寻找两个顶点之间权重之和最小的路径。
二、单源最短路径问题
单源最短路径问题是在加权图中寻找从一个特定源节点到所有其他节点的最短路径。这里,我们按照 递增 的顺序依次找出各个顶点的最短路径。
2.1、无权图的单源最短路径
无权图的单源最短路径问题,即图中的每条边没有权重,寻找从一个特定源节点到所有其他节点的最短路径。在这种情况下,由于没有权重的区别,最短路径通常指的是边数最少的路径。解决这类问题的算法通常比处理有权图的算法简单,因为边的权重都相同,所以路径的长度只与边的数量有关。最常用的算法是 广度优先搜索(Breadth-First Search, BFS) ,它特别适合于无权图的单源最短路径问题。
/**
* @brief 无权图的最短路径
*
* @param G 图
* @param v 第一个访问的邻接点
*/
void Unweighted(Graph G, int v)
{
PQueue Q = CreateQueue();
int u = 0, w = 0;
int dist[G->VertexNum]; // 存放顶点V到其它的最短距离
int path[G->VertexNum]; // 存放顶点V到其它顶点的路径
for (int i = 0; i < G->VertexNum; i++)
{
dist[i] = -1;
path[i] = -1;
}
dist[v] = 0; // 自身的最短路径为0
Enqueue(Q, v); // 将v入队
while (Q->Front != NULL)
{
u = Dequeue(Q); // 取出队头结点下标
for (w = GetFirstNeighbor(G, u); w != -1; w = GetNextNeighbor(G, u, w)) // 遍历u的邻接点
{
if (dist[w] == -1) // 如果w没有被访问过
{
dist[w] = dist[u] + 1; // w到v的距离为u到v的距离+1
path[w] = u; // u为w到v的路径
Enqueue(Q, w);
}
}
}
PrintShortestPath(G, path, dist, v);
}
#define MAX_PATH_LENGTH 1024
/**
* @brief 打印图中一个顶点到其它顶点的最短路径
*
* @param G 图
* @param path 存放路径的数组
* @param dist 存放距离的数组
* @param v 开始的顶点
*/
void PrintShortestPath(Graph G, int path[], int dist[], int v)
{
Stack S = CreateStack();
for (int i = 0; i < G->VertexNum; i++)
{
if (dist[i] == MAX_PATH_LENGTH)
{
continue;
}
if (i != v)
{
int j = i;
Push(S, G->Vertex[i]);
while (path[j] != -1)
{
Push(S, G->Vertex[path[j]]);
j = path[j];
}
printf("%c到%c的最短路径长度为:%d,路径为:", G->Vertex[v], G->Vertex[i], dist[i]);
while (S->Next != NULL)
{
printf("%c ", Pop(S));
}
printf("\n");
}
}
}
2.2、有权图的单源最短路径
有权图的单源最短路径问题涉及到寻找从一个给定的源节点到图中所有其他节点的最短路径,这里的 “最短” 是指路径上边的权重之和最小。解决这一问题的经典算法包括 Dijkstra 算法。
对于任一未收录的顶点 v,定义 dist[v] 为 s 到 v 的最短路径长度,但该路径是 仅通过 s 中的顶点的最短路径。随着顶点的不断被收录到 dist[] 中,dist[v] 的值不断变小,直到最后,全部的顶点被收录到 dist[] 中,dist[v] 称为真正的最短路径。这里,要求路径是按 递增 的顺序生成的。
#include <limits.h>
/**
* @brief 有权图的单源最短路径
*
* @param G 图
* @param v 开始的顶点
*/
void Dijkstra(Graph G, int v)
{
int u = 0, w = 0;
int dist[G->VertexNum]; // 存放顶点V到其它的最短距离
int path[G->VertexNum]; // 存放顶点V到其它顶点的路径
int collected[G->VertexNum]; // 存放顶点是否被收录的状态
MinHeap H = CreateMinHeap(G->VertexNum);
EdgeNode minItem = {0};
for (int i = 0; i < G->VertexNum; i++)
{
dist[i] = INT_MAX;
collected[i] = 0;
path[i] = -1;
}
dist[v] = 0;
minItem.V1 = v;
minItem.V2 = v;
minItem.Weight = dist[v];
Insert(H, minItem); // 将v入堆
while (H->Size != 0)
{
minItem = DeleteMin(H); // 未收录顶点中dist中的最小值
u = minItem.V1;
collected[u] = 1;
for (w = GetFirstNeighbor(G, u); w != -1; w = GetNextNeighbor(G, u, w)) // 遍历u的邻接点
{
if (collected[w] == 0 && (dist[u] + G->Matrix[u][w] < dist[w])) // 如果w没有被收录且v到w的距离大于v到u的距离加上u到w的距离
{
dist[w] = dist[u] + G->Matrix[u][w];
path[w] = u;
minItem.V1 = w;
minItem.V2 = u;
minItem.Weight = dist[w];
Insert(H, minItem);
}
}
}
PrintShortestPath(G, path, dist, v);
}
这里,我们使用最小堆的方式来获取未收录顶点中 dist 中的最小值。
typedef struct HeapStruct
{
EdgeNode * Data;
int Size;
int Capacity;
} HeapStruct, * MinHeap;
/**
* @brief 最小堆的创建
*
* @param MaxSize 最大容量
* @return MinHeap 指向最小堆的指针
*/
MinHeap CreateMinHeap(int MaxSize)
{
MinHeap H = (MinHeap)malloc(sizeof(HeapStruct));
H->Data = (EdgeNode *)malloc((MaxSize + 1) * sizeof(EdgeNode));
H->Size = 0;
H->Capacity = MaxSize;
// 定义哨兵为小于堆中所有可能元素的值,便于后序操作
H->Data[0].Weight = INT_MIN;
return H;
}
/**
* @brief 插入元素
*
* @param H 最小堆
* @param X 插入的元素
*/
void Insert(MinHeap H, EdgeNode X)
{
int i = 0;
if (H->Size == H->Capacity)
{
printf("堆已满,无法插入元素\n");
return;
}
// i指向插入后堆中的最后一个元素的位置
i = ++H->Size;
// 从最后一个元素开始,依次向上比较,直到找到合适位置
for (; H->Data[i / 2].Weight > X.Weight; i /= 2)
{
H->Data[i] = H->Data[i / 2];
}
// 插入元素
H->Data[i] = X;
}
/**
* @brief 删除元素
*
* @param H 最小堆
* @return EdgeNode 最短的边
*/
EdgeNode DeleteMin(MinHeap H)
{
int Parent = 0, Child = 0;
EdgeNode temp = {0}, MinItem = {0};
if (H->Capacity == 0)
{
printf("堆为空,无法删除元素\n");
return MinItem;
}
MinItem = H->Data[1];
// 用最小堆中最后一个元素从根结点开始向下过滤下层结点
temp = H->Data[H->Size--];
// Parent指向当前结点,Parent * 2 指向左子结点
for (Parent = 1; Parent * 2 <= H->Size ; Parent = Child)
{
Child = Parent * 2;
// Child != H->Size,意味着有右子结点,Child指向左右子结点的较小者
if (Child != H->Size && (H->Data[Child].Weight > H->Data[Child + 1].Weight))
{
Child++;
}
if (temp.Weight <= H->Data[Child].Weight)
{
break; // 找到合适位置,退出循环
}
else
{
H->Data[Parent] = H->Data[Child]; // 移动temp元素到下一层
}
}
H->Data[Parent] = temp;
return MinItem;
}
如果我们直接扫描所有收录顶点,则时间复杂度为 ,如果我们采用最小堆的方式,时间复杂度为 。
三、多源最短路径问题
多源最短路径问题是指在加权图中寻找多个源节点到所有其他节点的最短路径,即求任意两顶点见的最短路径。
这里,使用邻接矩阵 D[][]
表示图中各顶点对之间的直接距离,如果两个顶点间没有直接连接,则距离设为无穷大(通常表示为 INF
)。D[i][j]
初始时为从顶点 i
到顶点 j
的直接距离(如果存在边)或无穷大(如果不存在边)。
Floyd 算法会使用三层循环。外层循环遍历图中的每一个顶点 k
,作为可能的中转点。中层循环遍历图中的每一个顶点 i
,作为起始点。内层循环遍历图中的每一个顶点 j
,作为终点。在每次内层循环中,算法检查是否通过顶点 k
中转从 i
到 j
的路径比已知的直接路径更短。如果是,则更新 D[i][j]
的值。
经过三层循环后,矩阵 D[][]
中的每个元素 D[i][j]
将存储从顶点 i
到顶点 j
的最短路径长度。如果某条路径不存在,则对应的距离值保持为无穷大。
/**
* @brief 多源最短路径
*
* @param G 图
*/
void Floyd(Graph G)
{
int D[G->VertexNum][G->VertexNum];
int path[G->VertexNum][G->VertexNum];
// 初始化距离矩阵和路径矩阵,复用图G的邻接矩阵
for (int i = 0; i < G->VertexNum; i++)
{
for (int j = 0; j < G->VertexNum; j++)
{
D[i][j] = (i != j && G->Matrix[i][j] == 0) ? MAX_PATH_LENGTH : G->Matrix[i][j];
path[i][j] = -1;
}
}
for (int k = 0; k < G->VertexNum; k++) // 逐步更新距离矩阵,尝试通过中间顶点k缩短路径长度
{
for (int i = 0; i < G->VertexNum; i++)
{
for (int j = 0; j < G->VertexNum; j++)
{
if (D[i][k] + D[k][j] < D[i][j]) // 如果通过顶点k可以缩短路径,则更新距离矩阵和路径矩阵
{
D[i][j] = D[i][k] + D[k][j]; // 更新最短路径长度
path[i][j] = k; // 更新路径信息
}
}
}
}
// 输出所有顶点对之间的最短路径和长度
for (int v = 0; v < G->VertexNum; v++)
{
PrintShortestPath(G, path[v], D[v], v);
printf("\n");
}
}
如果我们将单源最短路算法调用 遍,时间复杂度为 ,如果采用 Floyd 算法,则时间复杂度为 。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)