117. 软件构建
题目描述
某个大型软件项目的构建系统拥有 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
cpp
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
// 邻接表存图
// edges[i] 表示从节点 i 出发能到达的所有节点
vector<vector<int>> edges(n);
// inDegree[i] 表示节点 i 的入度
// 入度就是有多少条边指向这个节点
vector<int> inDegree(n, 0);
// 读入 m 条有向边
for (int i = 0; i < m; i++) {
int s, t;
cin >> s >> t;
// s 指向 t
edges[s].push_back(t);
// t 的入度加 1
inDegree[t]++;
}
queue<int> que;
// res 用来保存拓扑排序结果
vector<int> res;
// 将所有入度为 0 的节点加入队列
// 入度为 0,说明该节点没有前置依赖,可以先处理
for (int i = 0; i < n; i++) {
if (inDegree[i] == 0) {
que.push(i);
}
}
// 开始拓扑排序
while (!que.empty()) {
// 取出当前入度为 0 的节点
int cur = que.front();
que.pop();
// 加入结果数组
res.push_back(cur);
// 遍历 cur 指向的所有节点
for (int i : edges[cur]) {
// 删除 cur 对 i 的影响
// 相当于去掉 cur -> i 这条边
inDegree[i]--;
// 如果 i 的入度变成 0
// 说明 i 的所有前置节点都已经处理完
if (inDegree[i] == 0) {
que.push(i);
}
}
}
// 如果结果中节点数量等于 n
// 说明所有节点都被处理,图中没有环
if (res.size() == n) {
for (int i = 0; i < res.size() - 1; i++) {
cout << res[i] << " ";
}
cout << res.back() << endl;
}
// 否则说明图中存在环,无法完成拓扑排序
else {
cout << -1 << endl;
}
return 0;
}
总结
1. 核心思路
这段代码使用的是 BFS 入度法 实现拓扑排序。
核心思想是:
每次选择入度为 0 的节点加入结果
入度为 0 表示这个节点没有前置依赖,可以直接处理。
处理完这个节点后,就把它指向的节点入度减一。
如果某个节点的入度也变成 0,说明它的前置依赖已经全部处理完,也可以加入队列。
2. 关键数组
vector<vector<int>> edges(n);
用来存储有向图。
如果有一条边:
s -> t
就表示 s 是 t 的前置节点。
vector<int> inDegree(n, 0);
用来记录每个节点的入度。
入度越大,说明它依赖的前置节点越多。
3. 代码流程
读入有向图
↓
统计每个节点的入度
↓
把入度为 0 的节点加入队列
↓
不断取出队头节点
↓
删除它对后续节点的影响
↓
新的入度为 0 的节点继续入队
↓
判断是否处理完所有节点
4. 本质理解
拓扑排序本质上是在解决依赖关系。
它每次做的事情就是:
先处理没有依赖的节点
处理完一个节点后,它对后面节点的限制就消失了。
所以后面节点的入度需要减一。
如果最后所有节点都能被处理,说明依赖关系合法。
如果有节点始终无法处理,说明图中存在环。
5. 如何判断有环
代码中通过这一句判断是否有环:
if (res.size() == n)
如果 res.size() == n,说明所有节点都进入了拓扑序,图中没有环。
如果 res.size() < n,说明还有节点没有被处理。
这些节点的入度始终无法变成 0,说明它们之间存在循环依赖,也就是有环。
6. 复杂度
每个节点最多入队一次,每条边最多遍历一次。
所以时间复杂度是:
O(n + m)
邻接表、入度数组和结果数组需要额外空间。
空间复杂度是:
O(n + m)
47. 参加科学大会(第六期模拟笔试)
题目描述
小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。
小明的起点是第一个车站,终点是最后一个车站。然而,途中的各个车站之间的道路状况、交通拥堵程度以及可能的自然因素(如天气变化)等不同,这些因素都会影响每条路径的通行时间。
小明希望能选择一条花费时间最少的路线,以确保他能够尽快到达目的地。
输入描述
第一行包含两个正整数,第一个正整数 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
cpp
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
// 定义一个较大的数,表示无穷大
const int INF = 1e8;
// 邻接矩阵存图
// edges[i][j] 表示从 i 到 j 的边权
// 如果 edges[i][j] == INF,说明 i 到 j 没有直接连边
vector<vector<int>> edges(n + 1, vector<int>(n + 1, INF));
// minDist[i] 表示从起点到节点 i 的当前最短距离
vector<int> minDist(n + 1, INF);
// visited[i] 表示节点 i 是否已经被确定最短路径
vector<bool> visited(n + 1, false);
// 读入 m 条有向边
for (int i = 0; i < m; i++) {
int s, t, v;
cin >> s >> t >> v;
// s 指向 t,边权为 v
edges[s][t] = v;
}
// 起点为 1,终点为 n
int start = 1, end = n;
// 起点到自己的距离为 0
minDist[start] = 0;
// 最多需要确定 n 个节点的最短路径
for (int i = 1; i <= n; i++) {
int cur = -1;
int minv = INF + 5;
// 1. 在所有没有访问过的节点中
// 找到距离起点最近的节点
for (int j = 1; j <= n; j++) {
if (!visited[j] && minDist[j] < minv) {
minv = minDist[j];
cur = j;
}
}
// 如果没有找到可访问的节点,直接结束
if (cur == -1) break;
// 标记该节点已经确定最短路径
visited[cur] = true;
// 2. 用 cur 节点更新其他节点的最短距离
for (int j = 1; j <= n; j++) {
// 如果 j 没有被访问过
// 并且 cur 到 j 有边
// 并且经过 cur 到达 j 的距离更短
if (!visited[j] &&
edges[cur][j] != INF &&
minDist[cur] + edges[cur][j] < minDist[j]) {
minDist[j] = minDist[cur] + edges[cur][j];
}
}
}
// 如果终点距离仍然是 INF,说明无法到达
if (minDist[end] == INF) {
cout << -1 << endl;
}
// 否则输出从 1 到 n 的最短距离
else {
cout << minDist[end] << endl;
}
return 0;
}
总结
1. 核心思路
Dijkstra 算法用来求单源最短路。
也就是:
从一个起点出发,到其他所有点的最短距离
这段代码中,起点是 1,终点是 n。
核心思想是:
每次选择当前距离起点最近、且还没有确定最短路的节点
选中这个节点后,就用它去更新其他节点的最短距离。
2. 关键数组
vector<vector<int>> edges(n + 1, vector<int>(n + 1, INF));
用邻接矩阵存图。
其中:
edges[i][j] 表示 i 到 j 的边权
如果 edges[i][j] == INF,说明 i 到 j 没有直接边。
vector<int> minDist(n + 1, INF);
表示起点到每个节点的当前最短距离。
比如:
minDist[i] 表示从 1 到 i 的最短距离
vector<bool> visited(n + 1, false);
表示某个节点的最短路径是否已经确定。
如果:
visited[i] == true
说明节点 i 的最短距离已经不会再被更新。
3. 代码流程
初始化图
↓
起点距离设为 0
↓
每次找到未访问节点中距离最小的点
↓
标记该点已经访问
↓
用该点更新其他节点的距离
↓
重复 n 次
↓
输出 1 到 n 的最短距离
4. 本质理解
Dijkstra 的本质是一个不断"扩展最短路范围"的过程。
一开始只知道:
起点到起点的距离是 0
然后每次找出离起点最近的点。
因为边权都是非负数,所以这个点一旦被选中,它的最短距离就确定了。
之后再通过这个点,尝试更新其他点的距离。
也就是:
先确定最近的点,再用最近的点更新更远的点
5. 为什么不能有负权边
Dijkstra 依赖一个前提:
当前选出来的最短距离,一定不会再变小
这个前提只有在边权非负时才成立。
如果存在负权边,后面可能会通过一条负权边,把已经确定的距离变得更小。
所以 Dijkstra 不能处理负权边。
6. 复杂度
这份代码使用的是邻接矩阵,并且每次都要遍历所有节点来找最小值。
所以时间复杂度是:
O(n²)
邻接矩阵需要存储所有点对之间的边。
空间复杂度是:
O(n²)