算法日记48 day 图论(拓扑排序,dijkstra)

今天继续图论章节,主要是拓扑排序和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的结点。

如果,结果集的长度和结点数相同,则表示我们可以将有向图转为线性排序,反之,则不能,为什么呢?因为有环,这一点是用在判断有向图是否有环中的

来看看具体的代码实现。

cpp 复制代码
#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,小于原minDist2的数值max,更新minDist2 = 1
  • 源点到节点3的最短距离为4,小于原minDist3的数值max,更新minDist3 = 4

再强调一下 minDist2 的含义,它表示源点到节点2的最短距离,那么目前我们得到了 源点到节点2的最短距离为1,小于默认值max,所以更新。 minDist3的更新同理

1、选源点到哪个节点近且该节点未被访问过

未访问过的节点中,源点到节点2距离最近,选节点2

2、该最近节点被标记访问过

节点2被标记访问过

3、更新非访问节点到源点的距离(即更新minDist数组) ,如图:

更新 minDist数组:

  • 源点到节点6的最短距离为5,小于原minDist6的数值max,更新minDist6 = 5
  • 源点到节点3的最短距离为3,小于原minDist3的数值4,更新minDist3 = 3
  • 源点到节点4的最短距离为6,小于原minDist4的数值max,更新minDist4 = 6

顺着这个循环执行下去,你就可以得到

这个时候我们的最短路径也就求出来了,看看具体的代码实现

cpp 复制代码
#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)

已刷题目:144
相关推荐
江畔柳前堤5 小时前
大语言模型分布式训练:从并行策略到万卡工程的系统梳理
人工智能·分布式·深度学习·算法·目标检测·机器学习·语言模型
Forever Nore6 小时前
LeetCode 4 寻找两个正序数组的中位数 - 二分
算法·leetcode
罗西的思考7 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX8 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面9 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号9 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽9 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI9 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者11 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者11 小时前
【征程6】校准量化中HistogramObserver解析
算法