今天继续图论章节,主要是拓扑排序和dijkstra算法。
还是举例说明。
题目:软件构建
117. 软件构建 (kamacoder.com)
题目描述
某个大型软件项目的构建系统拥有 N 个文件,文件编号从 0 到 N - 1,在这些文件中,某些文件依赖于其他文件的内容,这意味着如果文件 A 依赖于文件 B,则必须在处理文件 A 之前处理文件 B (0 <= A, B <= N - 1)。请编写一个算法,用于确定文件处理的顺序。
输入描述
第一行输入两个正整数 N, M。表示 N 个文件之间拥有 M 条依赖关系。
后续 M 行,每行两个正整数 S 和 T,表示 T 文件依赖于 S 文件。
输出描述
输出共一行,如果能处理成功,则输出文件顺序,用空格隔开。
如果不能成功处理(相互依赖),则输出 -1。
输入示例
5 4
0 1
0 2
1 3
2 4
输出示例
0 1 2 3 4
题目分析:
他的图是这样的
可以发现,他的结果其实不止一个。
拓扑排序
主要是解决依赖问题,就是将有向图转为一条线性排序关系。
回到本题来看,因为存在依赖关系,所以执行计划是有先后顺序的,这是一题很明显的拓扑排序问题。
对于拓扑排序来说,
例如 先上A课,才能上B课,上了B课才能上C课,上了A课才能上D课,等等一系列这样的依赖顺序。 问给规划出一条 完整的上课顺序。
拓扑排序在文件处理上也有应用,我们在做项目安装文件包的时候,经常发现 复杂的文件依赖关系, A依赖B,B依赖C,B依赖D,C依赖E 等等。
如果给出一条线性的依赖顺序来下载这些文件呢?
所以就需要使用拓扑排序的方式,将所有的结点排成一个线性的。
那我们来看看拓扑排序是怎样的过程。
实现拓扑排序的算法有两种:卡恩算法(BFS)和DFS,一般来说我们只需要掌握 BFS (广度优先搜索)就可以了。他的具体思路是这样的
首先,选出起点,这个起点是没有依赖的,一眼选中0,那么0有什么特征呢?入度为0,这很重要,意味着我们每次选取的应该是入度为0的结点。
现在把0加入结果中,那么剩下的结点,那些结点的入度为0呢?1和2,没错,在1和2中随便选一个加入结果集中,继续寻找入度为0的结点。直到没有入度为0的结点。
如果,结果集的长度和结点数相同,则表示我们可以将有向图转为线性排序,反之,则不能,为什么呢?因为有环,这一点是用在判断有向图是否有环中的
来看看具体的代码实现。
#include<iostream>
#include<vector>
#include<unordered_map>
#include <queue>
using namespace std;
int main(){
int n,m;
int s,t;
cin>>n>>m;
vector<int> inDegree(n, 0); // 记录每个文件的入度
unordered_map<int,vector<int>> umap;//记录所有与节点相连的节点
vector<int> result;
while(m--){
cin>>s>>t;
inDegree[t]++;//s是t的依赖文件
umap[s].push_back(t);
}
queue<int> que;//存放所有入度为0的节点
for(int i=0;i<n;i++){
if(inDegree[i]==0)//入度为0
{
que.push(i);
}
}
while(que.size()){
int cur = que.front(); // 当前选中的节点
que.pop();
result.push_back(cur);//加入结果集
vector<int> temp=umap[cur];//与当前节点相连的其他节点
if(temp.size()){
for(int i=0;i<temp.size();i++){ //遍历相连节点,将他们的入度减一
inDegree[temp[i]]--;
if(inDegree[temp[i]]==0){//入度为0,加入队列
que.push(temp[i]);
}
}
}
}
if(result.size()==n){//结果集长度等于节点数,无环
for (int i = 0; i < n - 1; i++) cout << result[i] << " ";
cout << result[n - 1];
}
else cout << -1 << endl;
}
题目:参加科学大会
47. 参加科学大会(第六期模拟笔试) (kamacoder.com)
题目描述
小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。
小明的起点是第一个车站,终点是最后一个车站。然而,途中的各个车站之间的道路状况、交通拥堵程度以及可能的自然因素(如天气变化)等不同,这些因素都会影响每条路径的通行时间。
小明希望能选择一条花费时间最少的路线,以确保他能够尽快到达目的地。
输入描述
第一行包含两个正整数,第一个正整数 N 表示一共有 N 个公共汽车站,第二个正整数 M 表示有 M 条公路。
接下来为 M 行,每行包括三个整数,S、E 和 V,代表了从 S 车站可以单向直达 E 车站,并且需要花费 V 单位的时间。
输出描述
输出一个整数,代表小明从起点到终点所花费的最小时间。
输入示例
7 9
1 2 1
1 3 4
2 3 2
2 4 5
3 4 2
4 5 3
2 6 4
5 7 4
6 7 9
输出示例
12
提示信息
能够到达的情况:
如下图所示,起始车站为 1 号车站,终点车站为 7 号车站,绿色路线为最短的路线,路线总长度为 12,则输出 12。
不能到达的情况:
如下图所示,当从起始车站不能到达终点车站时,则输出 -1。
题目分析:
到这里我们就开始接触最短寻路了,
dijkstra
主要是解决有向加权图的最短路径问题。在给定的加权图中,从起点到终点的最短路径。
他的思路,也是贪心的策略,每次选代价最小的结点。这一点和prim算法很像,但是有又区别,这一点之后再说。
他的具体思路大概是这样的,寻找离原点最近的未被访问过的结点,加入结果集,更新其他结点。那么这个最近结点怎么找呢,我们用一个mindist数组来记录,每一个结点到原点的最近距离。这一点需要很清楚。所以,对于dijkstra算法来说,他的结果表示的是每一个结点到原点的最短距离,而非只有终点。
来看看他的过程
minDist数组数值初始化为int最大值。
这里在强点一下 minDist数组的含义:记录所有节点到源点的最短路径,那么初始化的时候就应该初始为最大值,这样才能在后续出现最短路径的时候及时更新。
1、选源点到哪个节点近且该节点未被访问过
源点距离源点最近,距离为0,且未被访问。
2、该最近节点被标记访问过
标记源点访问过
3、更新非访问节点到源点的距离(即更新minDist数组) ,如图:
更新 minDist数组,即:源点(节点1) 到 节点2 和 节点3的距离。
- 源点到节点2的最短距离为1,小于原minDist[2]的数值max,更新minDist[2] = 1
- 源点到节点3的最短距离为4,小于原minDist[3]的数值max,更新minDist[3] = 4
再强调一下 minDist[2] 的含义,它表示源点到节点2的最短距离,那么目前我们得到了 源点到节点2的最短距离为1,小于默认值max,所以更新。 minDist[3]的更新同理
1、选源点到哪个节点近且该节点未被访问过
未访问过的节点中,源点到节点2距离最近,选节点2
2、该最近节点被标记访问过
节点2被标记访问过
3、更新非访问节点到源点的距离(即更新minDist数组) ,如图:
更新 minDist数组:
- 源点到节点6的最短距离为5,小于原minDist[6]的数值max,更新minDist[6] = 5
- 源点到节点3的最短距离为3,小于原minDist[3]的数值4,更新minDist[3] = 3
- 源点到节点4的最短距离为6,小于原minDist[4]的数值max,更新minDist[4] = 6
顺着这个循环执行下去,你就可以得到
这个时候我们的最短路径也就求出来了,看看具体的代码实现
#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;
// 存储从源点到每个节点的最短距离
std::vector<int> minDist(n + 1, INT_MAX);
// 记录顶点是否被访问过
std::vector<bool> visited(n + 1, false);
minDist[start] = 0; // 起始点到自身的距离为0
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;
}
}
visited[cur] = true; // 2、标记该节点已被访问
// 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];
}
}
}
if (minDist[end] == INT_MAX) cout << -1 << endl; // 不能到达终点
else cout << minDist[end] << endl; // 到达终点最短路径
}
区别
之前提到了prim和dijkstra的区别,现在具体看看,两者的步骤都很类似,寻找最近,加入结果,更新数组。而他们之间的区别,主要在Mindist数组中。
prim算法的数组表示的是结点到最小生成树的距离,而这个判断的边界是一直在改变的。
dijkstra算法的数组表示结点到原点的最短距离,而这个原点是不变的。所以在更新数组的逻辑上两者的代码不一样。
对于更详细的解析与其他语言的代码块,可以去代码随想录上查看。
代码随想录 (programmercarl.com)