一、DAG算法(基于拓扑排序计算单源最短路径)
- 原理
- DAG 是一种有向图,其中不存在任何有向环。这一特性使得在 DAG 上进行一些操作(如拓扑排序)成为可能。
- 在 DAG 中,我们可以利用拓扑排序的结果来高效地解决一些问题。例如,如果我们要计算从一个源节点到其他节点的最长或最短路径,可以按照拓扑排序的顺序依次处理节点。因为拓扑排序保证了对于任何一条边 (u, v),u 在排序中的顺序在 v 之前。
- 应用
- 任务调度:在项目管理中,不同的任务之间存在先后顺序关系,并且这种关系可以用 DAG 表示。例如,建筑工程中的各个施工步骤,某些步骤必须在其他步骤完成之后才能开始。
- 编译系统中的依赖关系:编译程序时,源文件之间可能存在依赖关系,如头文件与源文件的关系。这种依赖关系构成了一个 DAG,编译系统需要按照正确的顺序编译文件以避免错误。
- 示例
// 假设图以邻接表形式存储,graph为图结构,s为源节点 // 拓扑排序辅助函数,使用深度优先搜索 void topologicalSortUtil(int v, bool visited[], stack<int>& st, Graph graph) { visited[v] = true; AdjListNode* ptr = graph.adjList[v].head; while (ptr!= NULL) { int i = ptr->dest; if (!visited[i]) { topologicalSortUtil(i, visited, st, graph); } ptr = ptr->next; } st.push(v); } // 拓扑排序 void topologicalSort(stack<int>& st, Graph graph) { bool* visited = new bool[graph.V]; for (int i = 0; i < graph.V; i++) { visited[i] = false; } for (int i = 0; i < graph.V; i++) { if (visited[i] == false) { topologicalSortUtil(i, visited, st, graph); } } delete[] visited; } // DAG算法计算单源最短路径 void dagShortestPath(Graph graph, int s) { stack<int> st; topologicalSort(st, graph); int dist[graph.V]; for (int i = 0; i < graph.V; i++) { dist[i] = INT_MAX; } dist[s] = 0; while (st.empty() == false) { int u = st.top(); st.pop(); if (dist[u]!= INT_MAX) { AdjListNode* ptr = graph.adjList[u].head; while (ptr!= NULL) { int v = ptr->dest; if (dist[u] + ptr->weight < dist[v]) { dist[v] = dist[u] + ptr->weight; } ptr = ptr->next; } } } for (int i = 0; i < graph.V; i++) { if (dist[i] == INT_MAX) { printf("从 %d 到 %d 的距离为: INF\n", s, i); } else { printf("从 %d 到 %d 的距离为: %d\n", s, i, dist[i]); } } }
二、Dijkstra 算法(用于带非负权边的图)
- 原理
- Dijkstra 算法用于解决带权有向图或无向图(所有边的权重为非负)中的单源最短路径问题。
- 算法维护一个集合 S,表示已经确定了最短路径的节点集合。初始时,S 只包含源节点。同时,为每个节点 v 维护一个距离值 d [v],表示从源节点到 v 的当前最短距离估计。
- 在每一步中,算法从不在 S 中的节点中选择距离值最小的节点 u,将 u 加入 S,并更新与 u 相邻的节点的距离值。具体来说,对于每个与 u 相邻的节点 v,新的距离估计 d [v]=min (d [v], d [u]+w (u, v)),其中 w (u, v) 是边 (u, v) 的权重。
- 重复这个过程,直到所有节点都被加入到 S 中。
- 应用
- 路由算法:在计算机网络中,当需要找到从一个源路由器到其他路由器的最短路径时,Dijkstra 算法可以被用来计算。例如,在一个企业网络中,找到从总部路由器到各个部门路由器的最优路径。
- 导航系统:在地图导航中,计算从用户当前位置(源点)到目的地的最短路径(假设道路的权重可以是路程、时间等)。
- 示例
// 假设图以邻接矩阵形式存储在graph中,V为节点数,s为源节点 void dijkstra(int graph[][V], int s) { int dist[V]; bool sptSet[V]; for (int i = 0; i < V; i++) { dist[i] = INT_MAX; sptSet[i] = false; } dist[s] = 0; for (int count = 0; count < V - 1; count++) { int u = -1; for (int i = 0; i < V; i++) { if (!sptSet[i] && (u == -1 || dist[i] < dist[u])) { u = i; } } sptSet[u] = true; for (int v = 0; v < V; v++) { if (!sptSet[v] && graph[u][v]!= INT_MAX && dist[u]!= INT_MAX && dist[u] + graph[u][v] < dist[v]) { dist[v] = dist[u] + graph[u][v]; } } } for (int i = 0; i < V; i++) { if (dist[i] == INT_MAX) { printf("从 %d 到 %d 的距离为: INF\n", s, i); } else { printf("从 %d 到 %d 的距离为: %d\n", s, i, dist[i]); } } }
三、Bellman - ford 算法(用于带权有向图,可处理负权边但不能有负权环)
- 原理
- Bellman - ford 算法也用于解决带权有向图中的单源最短路径问题,但它能够处理包含负权重边的图(只要图中不存在负权重环)。
- 算法通过对所有边进行多次松弛操作来逐渐逼近最短路径。对于一个有 n 个节点的图,最多需要进行 n - 1 次松弛操作。
- 松弛操作是指:对于边 (u, v),如果 d [v]>d [u]+w (u, v),则更新 d [v]=d [u]+w (u, v)。在 n - 1 次松弛操作之后,如果还能对某些边进行松弛,就说明图中存在负权重环。
- 应用
- 金融领域中的套利检测:在货币兑换网络中,不同货币之间的汇率可以看作是图中的边权重。如果存在一个货币兑换循环,使得通过这个循环可以无风险地获得利润(即存在负权重环),Bellman - ford 算法可以用来检测这种情况。
- 动态规划问题的建模:一些可以被转化为图上最短路径问题的动态规划问题,当图中可能存在负权重边时,可以使用 Bellman - ford 算法求解。
- 示例
// 假设图以边列表形式存储在edges数组中,E为边数,V为节点数,s为源节点 void bellmanFord(struct Edge edges[], int E, int V, int s) { int dist[V]; for (int i = 0; i < V; i++) { dist[i] = INT_MAX; } dist[s] = 0; for (int i = 0; i < V - 1; i++) { for (int j = 0; j < E; j++) { int u = edges[j].src; int v = edges[j].dest; int weight = edges[j].weight; if (dist[u]!= INT_MAX && dist[u] + weight < dist[v]) { dist[v] = dist[u] + weight; } } } for (int j = 0; j < E; j++) { int u = edges[j].src; int v = edges[j].dest; int weight = edges[j].weight; if (dist[u]!= INT_MAX && dist[u] + weight < dist[v]) { printf("图中存在负权环\n"); return; } } for (int i = 0; i < V; i++) { if (dist[i] == INT_MAX) { printf("从 %d 到 %d 的距离为: INF\n", s, i); } else { printf("从 %d 到 %d 的距离为: %d\n", s, i, dist[i]); } } }
四、异同点
(一)相同点
- 目标相同
- 三者都用于解决图中的最短路径问题,特别是单源最短路径问题,即找到从一个源节点到图中其他节点的最短路径。
(二)不同点
- 图的适用类型
- DAG 算法:专门用于有向无环图。
- Dijkstra 算法:适用于带非负权边的有向图或无向图。如果图中存在负权边,可能导致错误结果。
- Bellman - ford 算法:适用于带权有向图,能处理负权边,但图中不能存在负权环,否则结果无意义。
- 算法复杂度
- DAG 算法:如果拓扑排序使用深度优先搜索实现,时间复杂度为
(V 是节点数,E 是边数),计算最短路径时间复杂度为
,总体复杂度为
。
- Dijkstra 算法:如果使用二叉堆实现优先队列,时间复杂度为
;如果使用邻接矩阵实现,时间复杂度为
(
) 。
- Bellman - ford 算法:时间复杂度为
。
- DAG 算法:如果拓扑排序使用深度优先搜索实现,时间复杂度为
- 算法思想
- DAG 算法:基于拓扑排序的顺序进行距离更新。
- Dijkstra 算法:基于贪心策略,每次选择当前距离源节点最近的节点进行扩展。
- Bellman - ford 算法:基于多次对所有边进行松弛操作来逼近最短路径。