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

时间:2024-12-16 17:59:20

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

已刷题目:144