代码随想录算法训练营 Day50 | 图论 part08

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

就表示 st 的前置节点。

复制代码
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,说明 ij 没有直接边。

复制代码
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²)
相关推荐
aini_lovee3 小时前
多目标粒子群优化(MOPSO)双适应度函数MATLAB实现
人工智能·算法·matlab
yong99903 小时前
图像融合与拼接:完整MATLAB工具箱
算法·计算机视觉·matlab
春风不语5054 小时前
深入理解主成分分析(PCA)
算法
apollowing4 小时前
启发式算法WebApp实验室:从搜索策略到群体智能的能力进阶(二十二)
算法·启发式算法·web app
晚枫歌F4 小时前
最小堆定时器
数据结构·算法
Lumos_7774 小时前
Linux -- 线程
java·jvm·算法
七颗糖很甜5 小时前
“十五五”气象发展规划:聚焦五大核心任务
大数据·python·算法
科研前沿5 小时前
镜像视界浙江科技有限公司的关键技术突破有哪些?
大数据·人工智能·科技·算法·音视频·空间计算
嫩萝卜头儿5 小时前
2 - 复杂度收尾 + 链表经典OJ
数据结构·算法·链表·复杂度