图算法详解:最短路径、拓扑排序与关键路径
图是数据结构中的重要概念,在实际应用中有着广泛的用途。本文将深入探讨图的三个核心算法:最短路径算法、拓扑排序和关键路径算法,帮助读者理解其原理、实现和应用场景。
文章目录
- 一、最短路径算法
- 1.1 Dijkstra算法 - 单源最短路径
- 1.2 Floyd算法 - 全源最短路径
- 二、拓扑排序算法
- 2.1 基于邻接矩阵的实现
- 2.2 基于邻接表的改进实现
- 三、关键路径算法
- 3.1 算法实现
- 四、算法应用与总结
- 4.1 应用场景对比
- 4.2 算法选择建议
- 4.3 性能优化提示
一、最短路径算法
最短路径问题是图论中的经典问题,旨在寻找图中两点间路径权值和最小的路径。根据求解范围的不同,可以分为单源最短路径和全源最短路径问题。
1.1 Dijkstra算法 - 单源最短路径
Dijkstra算法用于解决从某个源点到图中其他所有顶点的最短路径问题,适用于非负权值的有向图或无向图。
算法核心思想:
- 维护一个距离数组D[],记录源点到各顶点的最短距离
- 使用贪心策略,每次选择当前距离最小且未访问的顶点
- 通过该顶点更新其邻接顶点的距离值
float D[n]; // 存放各条最短路径的长度
int p[n], s[n]; // p[]记录前驱节点,s[]标记是否已访问void Dijkstra(int v, float dist[][n]) {int i, j, k, v1, min, max = 10000;v1 = v;// 初始化for(i = 0; i < n; i++) {D[i] = dist[v1][i];if(D[i] != max)p[i] = v1 + 1;else p[i] = 0;s[i] = 0;}s[v1] = 1; // 源点标记为已访问// 主循环:找到n-1个最短路径for(i = 0; i < n-1; i++) {min = max + 1;// 找到距离最小的未访问顶点for(j = 0; j < n; j++) {if((!s[j]) && (D[j] <= min)) {min = D[j];k = j;}}s[k] = 1; // 标记为已访问// 更新通过顶点k可达的其他顶点的距离for(j = 0; j < n; j++) {if((!s[j]) && D[j] > D[k] + dist[k][j]) {D[j] = D[k] + dist[k][j];p[j] = k + 1;}}}
}
复杂度分析:
- 时间复杂度:O(n²)
- 空间复杂度:O(n)
1.2 Floyd算法 - 全源最短路径
Floyd算法能够求出图中任意两个顶点之间的最短路径,采用动态规划的思想实现。
算法核心思想:
- 通过一个中间顶点k,判断经由k的路径是否比直接路径更短
- 逐个尝试每个顶点作为中间节点,更新所有顶点对之间的最短距离
int path[n][n]; // 路径矩阵,记录路径信息void Floyd(float A[][n], float dist[][n]) {int i, j, k, max = 1000;// 初始化路径长度矩阵和路径矩阵for(i = 0; i < n; i++) {for(j = 0; j < n; j++) {if(dist[i][j] != max)path[i][j] = i + 1;elsepath[i][j] = 0;A[i][j] = dist[i][j];}}// 三重循环:k为中间顶点for(k = 0; k < n; k++) {for(i = 0; i < n; i++) {for(j = 0; j < n; j++) {if(A[i][j] > A[i][k] + A[k][j]) {A[i][j] = A[i][k] + A[k][j]; // 更新距离path[i][j] = path[k][j]; // 更新路径}}}}
}
复杂度分析:
- 时间复杂度:O(n³)
- 空间复杂度:O(n²)
二、拓扑排序算法
拓扑排序是针对**有向无环图(DAG)**的一种排序方法,用于确定顶点的一个线性序列,使得对于图中每条有向边(u,v),顶点u都出现在顶点v之前。
应用场景:
- 课程安排(先修课程关系)
- 项目调度
- 编译器中的依赖分析
2.1 基于邻接矩阵的实现
void TopoSortA(Graph *g, int n) {int i, j, k, t, v, D[n];for(i = 0; i < n; i++)D[i] = 0; // 标记数组初始化v = 1; // 序号计数器for(k = 0; k < n; k++) {// 寻找入度为0的顶点(全0列)for(j = 0; j < n; j++) {if(D[j] == 0) {t = 1;for(i = 0; i < n; i++) {if(g->arcs[i][j] == 1) {t = 0;break;}}if(t == 1) {m = j;break;}}}if(j != n) {D[m] = v; // 分配新序号printf("%d\t", g->vex[m]);for(i = 0; i < n; i++)g->arcs[m][i] = 0; // 删除该顶点的所有出边v++;} else break;}if(v < n)printf("\n图中存在环路\n");
}
2.2 基于邻接表的改进实现
typedef struct {int adjvex; // 邻接点struct node *next;
} EdgeNode;typedef struct {int vertex; // 顶点信息int id; // 入度EdgeNode *link; // 边表头指针
} VexNode;void TopoSortB(VexNode ga[]) {int i, j, k, m = 0, top = -1;EdgeNode *p;// 建立入度为0的顶点栈for(i = 0; i < n; i++) {if(ga[i].id == 0) {ga[i].id = top;top = i;}}while(top != -1) {j = top;top = ga[top].id; // 出栈printf("%d\t", ga[j].vertex);m++;p = ga[j].link;while(p) {k = p->adjvex;ga[k].id--; // 入度减1if(ga[k].id == 0) {ga[k].id = top;top = k; // 新的零入度顶点入栈}p = p->next;}}if(m < n)printf("\n图中存在环路\n");
}
复杂度对比:
- 邻接矩阵实现:O(n³)
- 邻接表实现:O(n + e)
三、关键路径算法
关键路径算法用于解决**AOE网络(Activity On Edge)**中的项目调度问题,寻找决定整个项目完成时间的关键活动序列。
核心概念:
- 事件:用顶点表示,代表项目中的某个状态
- 活动:用边表示,代表需要消耗时间的任务
- 关键路径:从起点到终点的最长路径
- 关键活动:位于关键路径上的活动
3.1 算法实现
typedef struct node {int adjvex; // 邻接点int dur; // 活动持续时间struct node *next;
} EdgeNode;typedef struct {char vertex; // 顶点信息int id; // 入度EdgeNode *link; // 边表头指针
} VexNode;int CriticalPath(VexNode digl[]) {int i, j, k, m;int front = -1, rear = -1; // 队列指针int tpord[n], ve[n], vl[n];int l[maxsize], e[maxsize];EdgeNode *p;// 初始化事件最早发生时间for(i = 0; i < n; i++)ve[i] = 0;// 拓扑排序,计算ve[]for(i = 0; i < n; i++) {if(digl[i].id == 0)tpord[++rear] = i;}m = 0;while(front != rear) {front++;j = tpord[front];m++;p = digl[j].link;while(p) {k = p->adjvex;digl[k].id--;// 更新最早发生时间if(ve[j] + p->dur > ve[k])ve[k] = ve[j] + p->dur;if(digl[k].id == 0)tpord[++rear] = k;p = p->next;}}if(m < n) {printf("AOE网络中存在环路\n");return 0;}// 初始化事件最迟发生时间for(i = 0; i < n; i++)vl[i] = ve[n-1];// 按逆拓扑序列计算vl[]for(i = n-2; i >= 0; i--) {j = tpord[i];p = digl[j].link;while(p) {k = p->adjvex;if((vl[k] - p->dur) < vl[j])vl[j] = vl[k] - p->dur;p = p->next;}}// 计算活动的最早开始时间e[]和最迟开始时间l[]i = 0;for(j = 0; j < n; j++) {p = digl[j].link;while(p) {k = p->adjvex;e[++i] = ve[j];l[i] = vl[k] - p->dur;printf("活动<%d,%d> e=%d l=%d 松弛时间=%d\t", digl[j].vertex, digl[k].vertex, e[i], l[i], l[i] - e[i]);if(l[i] == e[i])printf("关键活动");printf("\n");p = p->next;}}return 1;
}
算法步骤:
- 通过拓扑排序计算事件的最早发生时间ve[]
- 按逆拓扑序列计算事件的最迟发生时间vl[]
- 计算每个活动的最早开始时间e[]和最迟开始时间l[]
- 找出松弛时间为0的活动(e[i] = l[i]),即关键活动
复杂度分析:
- 时间复杂度:O(n + e)
- 空间复杂度:O(n + e)
四、算法应用与总结
4.1 应用场景对比
算法 | 适用场景 | 典型应用 |
---|---|---|
Dijkstra | 单源最短路径,非负权值 | GPS导航,网络路由 |
Floyd | 全源最短路径,允许负权值 | 距离矩阵计算,传递闭包 |
拓扑排序 | 有向无环图排序 | 课程安排,依赖分析 |
关键路径 | 项目调度优化 | 工程管理,资源分配 |
4.2 算法选择建议
- 图规模较小且需要全源最短路径:选择Floyd算法
- 图规模较大且只需单源最短路径:选择Dijkstra算法
- 存在负权边但无负权环:使用Bellman-Ford算法(Floyd的变种)
- 需要判断依赖关系或检测环路:使用拓扑排序
- 项目管理和时间优化:使用关键路径算法
4.3 性能优化提示
- Dijkstra算法优化:使用优先队列(堆)可将时间复杂度降至O((n+e)logn)
- 拓扑排序优化:邻接表存储比邻接矩阵更高效
- 关键路径优化:可以结合并行计算技术处理大规模项目网络
这三个算法构成了图论算法的重要基础,掌握它们不仅有助于理解图的性质,更能为解决实际问题提供有力工具。在实际应用中,需要根据具体问题的特点选择合适的算法,并考虑数据规模和性能要求进行相应的优化。