【算法入门&图论】【模板】拓扑排序|【模板】单源最短路2 |最小生成树
✅作者简介:热爱后端语言的大学生,CSDN内容合伙人
✨精品专栏:C++面向对象
🔥系列专栏:算法百炼成神
文章目录
- 🔥前言
- 1、AB13 【模板】拓扑排序
-
- 1.1、解题思路
- 1.2、代码实现与注释
- 2、AB14 最小生成树
-
- 2.1、解题思路
- 2.2、代码实现与注释
- 3、AB15 单源最短路2
-
- 3.1、解题思路
- 3.2、代码实现与注释
🔥前言
本专栏收录的均为牛客网的算法题目,内含链表、双指针、递归、动态规划、基本数据结构等算法思想的具体运用。牛客网不仅有大量的经典算法题目,也有大厂的面试真题,面试、找工作完全可以来这里找机会。此外,网站内的编码主题多样化,调试功能可运用性强,可谓是非常注重用户体验。这么好的免费刷题网站还不快入手吗,快去注册开启算法百炼成神之路吧!
1、AB13 【模板】拓扑排序
学会使用邻接表解决图论问题,巧妙利用vector
容器
题目链接:拓朴排序
1.1、解题思路
解决拓扑排序之前要先认识什么是拓扑排序:
对一个有向无环图(Directed Acyclic Graph简称
DAG
)图G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u
和v
,若边∈E(G)
,则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。
解决步骤:
- 使用邻接表将顶点联系起来,辅助数组
inDegree
表示每个顶点的入度。 - 借助队列和计数器变量来判断该有向图是否有环:
- 将入度为零的顶点入队(也就是拓扑图第一个顶点)
- 取队首,遍历与之相邻的顶点,若该顶点入度减一后为零就将其入队
- 只要队列非空就循环操作,计算器循环加一,与顶点数比较是否相等
- 本题末尾也不能输出空格,因此输出拓扑序列时要加限制条件
1.2、代码实现与注释
本题源码:
#include#include#include#define M 200001using namespace std;int main() { int n, m; cin >> n >> m; vector<int> adjList[M]; // 模拟邻接表 int inDegree[M] = { 0 };// 记录每个顶点的入度 int a, b; for (int i = 0; i < m; i++) { cin >> a >> b; adjList[a].push_back(b); inDegree[b]++; } queue<int> que; // 将初始入度为零的顶点入队 for (int i = 1; i <= n; i++) { if (inDegree[i] == 0) que.push(i); } int cnt = 0; // 用来计数,判断改图是否有环 vector<int> res; // 用来输出顶点序列 while (!que.empty()) { int u = que.front(); que.pop(); res.push_back(u); for (int i = 0; i < adjList[u].size(); i++) { // 遍历u的相邻顶点 int v = adjList[u][i]; if (--inDegree[v] == 0) que.push(v); } cnt++; } // 若计数器与顶点数相同则图无环,存在拓扑排序 if (cnt == n) { for (int i = 0; i < res.size(); i++) { cout << res[i]; // 限制输出空格的条件 if (i != res.size() - 1) { cout << " "; } } } else { cout << -1; } return 0;}
重要注释:
adjList
数组是vector
类型的,用来模拟邻接表- 使用每个元素为一个数组的
vector
容器模拟邻接表进行建图 vector[a]
所对应的数组中存储着该顶点所指向的其他顶点inDegree
数组代表每一个顶点的入度情况
- 使用每个元素为一个数组的
- 使用一个队列,初始时将所有入度为0的顶点全部入队,之后采用
BFS
的思想:- 依次取出队头元素并存入结果数组中,然后在邻接表中遍历该队头元素所指向的其他顶点
- 将这些顶点的入度全部减一,若减一后某顶点的入度变为0,则将该顶点进行入队操作,
重复此步骤直至队列为空为止。
- 设置一个用于判断图中是否存在环(是否可以得到拓扑序列)的计数器,在弹出队头元素后要将计数器加一,最后队列为空后,若计数器的值与顶点数相同,则说明图不存在环,可以得到拓扑序列。
2、AB14 最小生成树
题目链接:最小生成树
2.1、解题思路
本题要求在最小花费下将 n 户人家连接起来,很显然是最小生成树的问题,我采用prim
算法:
- 将二维数组
cost
按权升序排序,那么cost[0][2]
就是最小的一个权值 - 将连接这条边的两个顶点放入
unordered_set
容器:unordered_set
容器的元素是无序的,可以用来给顶点去重- 内置的
find
方法也很好用
- 接下来遍历
cost
二维数组,直到所有顶点全部放入容器中,遍历结束 - 将遍历中权值的和返回,程序结束
2.2、代码实现与注释
本题源码:
class Solution { public: /** * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 * * 返回最小的花费代价使得这n户人家连接起来 * @param n int n户人家的村庄 * @param m int m条路 * @param cost intvector<vector> 一维3个参数,表示连接1个村庄到另外1个村庄的花费的代价 * @return int */ // 自定义排序规则:按权递增 static bool cmp(vector<int>& x, vector<int>& y) { return x[2] < y[2]; } int miniSpanningTree(int n, int m, vector<vector<int> >& cost) { unordered_set<int> points; // 记录不重复的点 int res = 0; sort(cost.begin(), cost.end(), cmp); res += cost[0][2]; // 此时res 为最小权值 // 将最小边加入 points.insert(cost[0][0]); points.insert(cost[0][1]); while (1) { if (points.size() == n) break; // 所有的点连同后退出循环 // 遍历剩余的边 for (auto it = cost.begin(); it != cost.end(); it++) { // 如果边仅有一个点在集合内就加入 if ((points.find((*it)[0]) != points.end() && points.find((*it)[1]) == points.end()) || (points.find((*it)[1]) != points.end() && points.find((*it)[0]) == points.end())) { res += (*it)[2]; points.insert((*it)[0]); points.insert((*it)[1]); cost.erase(it); // 删除该边 break; } } } return res; }};
重要注释:
cmp
是自定义的一个按权递增的排序函数,配合sort
函数来将cost
排序- 相关知识点可以参考我的博文:自定义排序规则
auto
关键字可以自动推导表达式类型,在这里就相当于vector<vector>::iterator
if
的条件很长,但其实就是将有且仅有一个顶点在points
中的边找到:- 获取该边的权值并求和,将另一顶点插入到
points
中 - 将该边删除,重新遍历
cost
,直到全部顶点被插入到points
中
- 获取该边的权值并求和,将另一顶点插入到
- 最终的
res
就是该题的结果,即最小花费。
3、AB15 单源最短路2
题目链接:单源最短路2
3.1、解题思路
使用Dijkstra
算法(即不断从未处理集合中找当前距离源点最近的顶点以添加到已处理集合中,直至未处理集合为空的算法思想)
- 对于无向图,采用邻接矩阵表示点与点的连接关系以及距离
- 使用数组
dist
记录每个顶点与源点的距离:- 本题中就是记录顶点1与其他顶点的距离
- 用一个布尔类型的数组记录顶点的处理情况:
- 初始状态全部设为
false
,一经处理就设为true
- 初始状态全部设为
- 最终
dist[n]
就是顶点n
到源点的最短距离
3.2、代码实现与注释
本题源码
#include#include // 使用INT_MAX所需要引入的头文件using namespace std;const int N = 5000; // 注意题干,图的点数是固定值5000int main() { int G[N + 1][N + 1]; // 用于模拟邻接矩阵进行建图 for (int i = 1; i <= N; i++) { for (int j = 1; j <= N; j++) { G[i][j] = INT_MAX; // 先将邻接矩阵全部初始化为无穷大 } } int n, m; cin >> n >> m; int u, v, w; for (int i = 1; i <= m; i++) { cin >> u >> v >> w; G[u][v] = w; G[v][u] = w; // 需要关于主对角线对称,因此两边都需要存储 } int dist[N + 1]; // 用于存储每个顶点当前与源点的最短距离 bool flag[N + 1]; // 用于记录每个顶点是否已经完成与源点最短距离的计算处理 for (int i = 1; i <= N; i++) { dist[i] = G[1][i]; // 初始设置为邻接矩阵中源点所在 行 的权值 flag[i] = false; } dist[1] = 0; flag[1] = true; // 将源点加入已处理集合 for (int i = 2; i <= N; i++) { int tmp = INT_MAX, index = 1; for (int j = 1; j <= N; j++) { // 遍历寻找与源点的最短距离 if (flag[j] == false && dist[j] < tmp) { tmp = dist[j]; index = j; } } if (index != 1) { flag[index] = true; // 找到后将其加入已处理集合 } for (int j = 2; j <= N; j++) { if (flag[j] == false && G[index][j] != INT_MAX) { if (G[index][j] + dist[index] < dist[j]) { dist[j] = G[index][j] + dist[index]; // 更新最短路径 } } } } if (dist[n] != INT_MAX) { cout << dist[n]; } else { cout << -1; } return 0;}
重要注释:
- 二维数组
G
是具化出的邻接矩阵,将元素值全部初始化为无穷大:INT_MAX
u
,v
记录两个顶点,w
记录顶点间距离,全部存入G
中dist
数组用来存放各顶点到源点的距离,flag
数组记录顶点是否被处理过index
代表着与源点连接且距离最短的未经处理的顶点:- 随后将该顶点设为已处理
- 然后
index
寻找与之相连且未经处理的顶点:dist[index]
是index
到源点的最短距离,如果加上与之相连的顶点的距离较小,那么就更新最短路径
- 最终的
dist
数组的值就是各点到源点的最短路径
当自己动手去做的时候才发现原来我以为的难题,不过如此,将这份自信传达给大家,奋斗下去!