「代码随想录算法训练营」第五十天 | 图论 part8
拓扑排序
拓扑排序概括来说就是给出一个有向无环图,把这个有向无环图转成线性的排序,就叫拓扑排序。
使用广度优先搜索(BFS)即可。
如上图,当我们做拓扑排序的时候,优先找入度为0的节点,只有入度为0,它才是出发节点。
拓扑排序的过程:
- 找到入度为0的节点,加入结果集。
- 将该节点从图中移除。
循环以上两步,直到所有节点都在图中被移除了。
结果集的顺序,就是我们想要的拓扑排序顺序(结果集里顺序可能不唯一)
模拟过程:
判断有环:
这个图,我们只能将入度为0的节点0接入结果集。
之后,节点1、2、3、4形成了环,找不到入度为0的节点了,所以此时结果集里只有一个元素。
那么如果我们发现结果集元素个数不等于图中节点个数,我们就可以认定图中一定有有向环!
题目:117. 软件构建
题目链接:https://kamacoder.com/problempage.php?pid=1191
文章讲解:https://www.programmercarl.com/kamacoder/0117.软件构建.html
题目状态:看题解
思路:
使用拓扑排序。
代码:
#include <iostream> #include <vector> #include <queue> #include <unordered_map> using namespace std; int main() { int m, n, s, t; cin >> n >> m; vector<int> isDegree(n, 0); // 记录每个文件的入度 unordered_map<int, vector<int>> umap; // 记录文件依赖关系 vector<int> result; // 结果集 while(m--) { // s->t,先有s才能有t cin >> s >> t; isDegree[t]++; // t的入度加一 umap[s].push_back(t); // 记录s指向哪些文件 } queue<int> que; for(int i = 0; i < n; ++i) { // 入度为0的文件,可以作为开头,先加入队列 if(isDegree[i] == 0) que.push(i); } while(que.size()) { int cur = que.front(); // 当前选中的文件 que.pop(); result.push_back(cur); vector<int> files = umap[cur]; // 获取该文件指向的文件 if(files.size()) // cur有后续文件 { for(int i = 0; i < files.size(); ++i) { isDegree[files[i]]--; // cur的指向的文件入度-1 if(isDegree[files[i]] == 0) que.push(files[i]); } } } if(result.size() == n) { for(int i = 0; i < n - 1; ++i) cout << result[i] << " "; cout << result[n - 1]; } else cout << -1 << endl; return 0; }
dijkstra(朴素版)
dijkstra算法:在有权图(权值非负数)中求从起点到其他节点的最短路径算法。
需要注意两点:
- dijkstra算法可以同时求起点到所有节点的最短路径。
- 权值不能为负数。
dijkstra算法和prim算法的计算过程非常类似:
- 第一步,选源点到哪个节点近且该节点未被访问过
- 第二步,该最近节点被标记访问过
- 第三步,更新非访问节点到源点的距离(即更新minDist数组)
其中minDist数组用来记录每一个节点距离源点的最小距离。
模拟过程:
题目:47. 参加科学大会
题目链接:https://kamacoder.com/problempage.php?pid=1047
文章讲解:https://www.programmercarl.com/kamacoder/0047.参会dijkstra朴素.html
题目状态:看题解
思路:
dijkstra算法。
代码:
#include <iostream> #include <vector> #include <climits> using namespace std; int main() { int n, m, p1, p2, val; cin >> n >> m; vector<vector<int>> grid(n + 1, vector<int>(n + 1, INT_MAX)); for(int i = 0; i < m; ++i) { cin >> p1 >> p2 >> val; grid[p1][p2] = val; } int start = 1; int end = n; // 存储从源点到每个节点的最短距离 vector<int> minDist(n + 1, INT_MAX); // 记录顶点是否被访问过 vector<bool> visited(n + 1, false); minDist[start] = 0; // 起始点到自身的距离为0 // 初始化路径数组 vector<int> parent(n + 1, -1); // 遍历所有节点 for(int i = 1; i <= n; ++i) { int minVal = INT_MAX; int cur = 1; // 1.选距离源点最近且未访问过的节点 for(int v = 1; v <= n; ++v) { if(!visited[v] && minDist[v] < minVal) { minVal = minDist[v]; cur = v; } } // 2.标记该节点已被访问 visited[cur] = true; // 3.更新非访问节点到源点的距离(即更新minDist数组) for(int v = 1; v <= n; ++v) { if(!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { minDist[v] = minDist[cur] + grid[cur][v]; parent[v] = cur; // 记录边 } } } // 不能到达终点 if(minDist[end] == INT_MAX) cout << -1 << endl; // 到达终点最短路径 else cout << minDist[end] << endl; // 输出最短情况 for(int i = 1; i <= n; ++i) cout << parent[i] << "->" << i << endl; return 0; }
dijkstra算法和prim算法的区别
- prim是求非访问节点到最小生成树的最小距离。
- dijkstra是求非访问节点到源点的最小距离。
dijkstra(堆优化版)
上一节的dijkstra算法是从节点的角度来遍历的,这一节的dijkstra算法我们从边的角度来遍历分析,且采用邻接表来存储图。邻接表表示一个有向有权图如下:
其中:
- 节点1指向节点3,权值为1
- 节点1指向节点5,权值为2
- 节点2指向节点4,权值为7
- 节点2指向节点3,权值为6
- 节点2指向节点5,权值为3
- 节点3指向节点4,权值为3
- 节点5指向节点1,权值为10
dijkstra算法思路三部曲:
- 第一步,选源点到哪个节点近且该节点未被访问过
- 第二步,该最近节点被标记访问过
- 第三步,更新非访问节点到源点的距离(即更新minDist数组)
其中第一步我们要选择距离源点近的节点(即:该边的权值最小),所以我们需要一个小顶堆来帮我们对边的权值排序,每次从小顶堆堆顶去边就是权值最小的边。
因此在堆优化版本的dijkstra算法的三部曲中:
- 第一步:不用for循环去遍历,直接取堆顶元素
- 第二步:将节点做访问标记
- 第三步:和朴素dijkstra算法一样
题目:47. 参加科学大会
题目链接:https://kamacoder.com/problempage.php?pid=1047
文章讲解:https://www.programmercarl.com/kamacoder/0047.参会dijkstra朴素.html
题目状态:看题解
思路:
使用堆优化版本的dijkstra算法。
代码:
#include <iostream> #include <vector> #include <list> #include <queue> #include <climits> using namespace std; // 小顶堆 class mycomparison { public: bool operator()(const pair<int, int> &lhs, const pair<int, int> &rhs) { return lhs.second > rhs.second; } }; // 定义一个结构体来表示带权重的边 struct Edge { int to; // 邻接顶点 int val; // 边的权重 Edge(int t, int w): to(t), val(w) {} // 构造函数 }; int main() { int n, m, p1, p2, val; cin >> n >> m; vector<list<Edge>> grid(n + 1); for(int i = 0; i < m; ++i) { cin >> p1 >> p2 >> val; // p1指向p2,权值为val grid[p1].push_back(Edge(p2, val)); } int start = 1; // 起点 int end = n; // 终点 // 存储从源点到每个节点的最短距离 vector<int> minDist(n + 1, INT_MAX); // 记录顶点是否被访问过 vector<bool> visited(n + 1, false); // 优先级队列中存放pair<节点, 源点到该节点的权值> priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pq; // 初始化队列,源点到源点的距离为0,所以初始为0 pq.push(pair<int, int>(start, 0)); // 起始点到自身的距离为0 minDist[start] = 0; while(!pq.empty()) { // 1.第一步,选源点到哪个节点近且该节点未被访问过(通过优先级队列来实现) // <节点, 源点到该节点的距离> pair<int, int> cur = pq.top(); pq.pop(); if(visited[cur.first]) continue; // 2.第二步,该最近节点被标记访问过 visited[cur.first] = true; // 3.第三步,更新非访问节点到源点的距离(即更新minDist数组) for(Edge &edge : grid[cur.first]) { // 遍历cur指向的节点,cur指向的节点为edge // cur指向的节点edge.to,这条边的权值为edge.val if(!visited[edge.to] && minDist[cur.first] + edge.val < minDist[edge.to]) { // 更新minDist minDist[edge.to] = minDist[cur.first] + edge.val; pq.push(pair<int, int>(edge.to, minDist[edge.to])); } } } // 不能到达终点 if(minDist[end] == INT_MAX) cout << -1 << endl; // 到达终点最短路径 else cout << minDist[end] << endl; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?