首页 > 编程语言 >「代码随想录算法训练营」第五十一天 | 图论 part9

「代码随想录算法训练营」第五十一天 | 图论 part9

时间:2024-09-01 11:39:02浏览次数:3  
标签:val int 随想录 minDist que 第五十一 part9 include 节点

目录

Bellman_ford算法

Bellman_ford算法解决的问题和dijkstra朴素版要解决的问题类似,唯一不同的是dijkstra解决的问题中边的权值不能为负数,而Bellman_ford算法要解决的问题是带负权值的单源最短路问题。

Bellman_ford算法的核心思想是对所有边进行松弛n-1次操作(n为节点数量),从而求得目标最短路

松弛的概念就是当当前节点的minDist值大于从其他节点过来的权值,则更新当前节点的minDist值,如下面的代码:

if(minDist[B] > minDist[A] + value) minDist[B] = minDist[A] + value;

其中value表示从A节点到B节点中的权值,上面代码的意思就是A到B的路径所需要的权值要小于B当前的权值,因此要更新B的权值。

模拟过程

注意:这个模拟过程仅仅是将具有改变的过程展现出来,而有些节点在当前松弛阶段并没有改变其minDist值,比如节点5到节点6……,这里并没有将这些过程展现出来。

  1. 加入节点1,更新minDist数组。

image

  1. 进行一次松弛计算:

节点1到节点2:minDist[2] > minDist[1] + 1,因此更新minDist[2]。

image

节点2到节点5:minDist[5] > minDIst[2] + 2,因此更新minDist[5]。

image

节点2到节点4:minDist[4] > minDist[2] + (-3),因此更新minDist[4]。

image

节点4到节点6:minDist[6] > minDist[4] + 4,因此更新minDist[6]。

image

节点1到节点3:minDist[3] > minDist[1] + 5,因此更新minDist[3]。

image

  1. 上面只是一次松弛的结果,相当于计算起点到达与起点一条边相连的节点的最短距离。而从起点到终点最多有n-1条边,因此需要进行n-1次松弛。

题目:94. 城市间货物运输I

题目链接:https://kamacoder.com/problempage.php?pid=1152
文章讲解:https://www.programmercarl.com/kamacoder/0094.城市间货物运输I.html
题目状态:看题解

思路:

使用Bellman_ford算法。

代码:

#include <iostream>
#include <vector>
#include <list>
#include <climits>

using namespace std;

int main()
{
    int n, m, p1, p2, val;
    cin >> n >> m;

    vector<vector<int>> grid;

    // 将所有边保存起来
    for(int i = 0; i < m; ++i)
    {
        cin >> p1 >> p2 >> val;
        grid.push_back({p1, p2, val});
    }

    int start = 1; // 起点
    int end = n; // 终点

    vector<int> minDist(n + 1, INT_MAX);
    minDist[start] = 0;
    
    // 对所有边松弛n-1次
    for(int i = 1; i < n; ++i)
    {
        // 每一次松弛,都是对所有边进行松弛
        for(vector<int> &side : grid)
        {
            int from = side[0]; // 边的出发点
            int to = side[1]; // 边的到达点
            int price = side[2]; // 边的权值
            // 松弛操作
            // minDist[from] != INT_MAX 防止从未计算过的节点出发
            if(minDist[from] != INT_MAX && minDist[to] > minDist[from] + price)
            {
                minDist[to] = minDist[from] + price;
            }
        }
        cout << "对所有边松弛" << i << "次" << endl;
        for(int k = 1; k <= n; ++k)
        {
            cout << minDist[k] << " ";
        }
        cout << endl;
    }
    if(minDist[end] == INT_MAX) cout << "unconnected" << endl;
    // 不能到达终点
    else cout << minDist[end] << endl; // 到达终点最短路径
}

Bellman_ford队列优化算法(又名SPFA)

在上一节的Bellman_ford算法中的松弛阶段是对每一个节点都进行松弛,而有些节点在上一个节点没有被松弛的情况下进行松弛是没必要的。

比如当前节点5的minDist[5]为INT_MAX,而节点5到节点6需要判断minDist[6]是否大于minDist[5] + value,显然是不必要的。

而队列优化版本则只需要对上一次松弛的时候更新过的节点作为出发节点所连接的边进行松弛就够了

模拟过程

  1. 将节点1压入队列,并更新minDist数组。

image

  1. 从队列中取出节点1,以节点1为起点,压入与其有边的节点2和节点3,并更新minDist数组。

image

  1. 从队列中取出节点2,以节点2为起点,压入与其有边的节点4和节点5,并更新minDist数组。

image

  1. 从队列中取出节点3,以节点3为起点,压入与其有边的节点(该例中没有),并更新minDist数组。

image

  1. 从队列中取出节点4,以节点4为起点,压入与其有边的节点6,并更新minDist数组。

image

  1. 从队列中取出节点5,以节点5为起点,压入与其有边的节点6和节点3(因为节点6还在队列中,因此不用重复压入),并更新minDist数组。

image

  1. 分别从队列中取出节点6和节点3,因为其都没有下一个节点,因此没有压入操作,而需要更新minDist数组。

image

完成。

题目:94. 城市间货物运输I

题目链接:https://kamacoder.com/problempage.php?pid=1152
文章讲解:https://www.programmercarl.com/kamacoder/0094.城市间货物运输I.html
题目状态:看题解

思路:

Bellman_ford算法队列优化版本

代码:

#include <iostream>
#include <vector>
#include <queue>
#include <list>
#include <climits>

using namespace std;

// 邻接表
struct Edge
{
    int to; // 链接的节点
    int val; // 边的权值

    Edge(int t, int w): to(t), val(w) {} // 构造函数
};

int main()
{
    int n, m, p1, p2, val;
    cin >> n >> m;

    vector<list<Edge>> grid(n + 1);

    // 加入优化,已经在队列里的元素不用重复添加
    vector<bool> isInQueue(n + 1);

    // 将所有边保存起来
    for(int i = 0; i < m; ++i)
    {
        cin >> p1 >> p2 >> val;
        grid[p1].push_back(Edge(p2, val));
    }
    int start = 1; // 起点
    int end = n; // 终点
    
    vector<int> minDist(n + 1, INT_MAX);
    minDist[start] = 0;

    queue<int> que;
    que.push(start);

    while(!que.empty())
    {
        int node = que.front(); que.pop();
        // 从队列中取出的时候,要取消标记,我们只保证已经在队列里的元素不用重复加入
        isInQueue[node] = false;
        for(Edge edge : grid[node])
        {
            int from = node;
            int to = edge.to;
            int value = edge.val;
            // 开始松弛
            if(minDist[to] > minDist[from] + value)
            {
                minDist[to] = minDist[from] + value;
                if(isInQueue[to] == false)
                {
                    // 已经在队列里的元素不用重复添加
                    que.push(to);
                    isInQueue[to] = true;
                }
            }
        }
    }

    // 不能到达终点
    if(minDist[end] == INT_MAX) cout << "unconnected" << endl;
    // 到达终点最短路径
    else cout << minDist[end] << endl;
}

Bellman_ford算法之判断负权回路

上面两节都是没有回路的情况,而在本节的示例中出现了负权回路的情况,也就是说一直在这个负权回路中转到话,其minDist会一直变小,因此需要判断是否存在负权回路。

前两节都是进行n-1次松弛,因为n-1次和n次是相同的,但在本节中是不一样的,因为有负权回路的存在,n次和n-1次的结果是不同的,因此可以根据这个条件来判断是否存在负权回路。

题目:95. 城市间货物运输II

题目链接:https://kamacoder.com/problempage.php?pid=1153
文章讲解:https://www.programmercarl.com/kamacoder/0095.城市间货物运输II.html
题目状态:看题解

思路一:

在Bellman_ford算法基础上增加负权回路判断。

代码一:

#include <iostream>
#include <vector>
#include <list>
#include <climits>

using namespace std;

int main()
{
    int n, m, p1, p2, val;
    cin >> n >> m;

    vector<vector<int>> grid;

    for(int i = 0; i < m; ++i)
    {
        cin >> p1 >> p2 >> val;
        grid.push_back({p1, p2, val});
    }
    int start = 1; // 起点
    int end = n; // 终点

    vector<int> minDist(n + 1, INT_MAX);
    minDist[start] = 0;
    bool flag = false;
    // 松弛n次,最后一次判断负权回路
    for(int i = 1; i <= n; ++i)
    {
        for(vector<int> &side : grid)
        {
            int from = side[0];
            int to = side[1];
            int price = side[2];
            if(i < n)
            {
                if(minDist[from] != INT_MAX && minDist[to] > minDist[from] + price)
                    minDist[to] = minDist[from] + price;
            }
            else
            {
                // 多加一次松弛判断负权回路
                if(minDist[from] != INT_MAX && minDist[to] > minDist[from] + price) flag = true;
            }
        }
    }

    if(flag) cout << "circle" << endl;
    else if(minDist[end] == INT_MAX) cout << "unconnected" << endl;
    else cout << minDist[end] << endl;

    return 0;
}

思路二:

在Bellman_ford的队列优化版本中加入负权回路判断。

代码二:

#include <iostream>
#include <vector>
#include <queue>
#include <list>
#include <climits>

using namespace std;

// 邻接表
struct Edge
{
    int to; // 链接的节点
    int val; // 边的权重
    Edge(int t, int w): to(t), val(w) {} // 构造函数
};

int main()
{
    int n, m, p1, p2, val;
    cin >> n >> m;

    vector<list<Edge>> grid(n + 1); // 邻接表

    // 将所有边保存起来
    for(int i = 0; i < m; ++i)
    {
        cin >> p1 >> p2 >> val;
        grid[p1].push_back(Edge(p2, val));
    }

    int start = 1; // 起点
    int end = n; // 终点

    vector<int> minDist(n + 1, INT_MAX);
    minDist[start] = 0;

    queue<int> que;
    que.push(start); // 队列里放入起点

    // 记录节点加入队列几次
    vector<int> count(n + 1, 0);
    count[start]++;

    bool flag = false;
    while(!que.empty())
    {
        int node = que.front(); que.pop();

        for(Edge &edge : grid[node])
        {
            int from = node;
            int to = edge.to;
            int value = edge.val;
            // 开始松弛
            if(minDist[to] > minDist[from] + value)
            {
                minDist[to] = minDist[from] + value;
                que.push(to);
                count[to]++;
                // 如果加入队列次数超过n-1次就说明该图有负权回路
                if(count[to] == n)
                {
                    flag = true;
                    while(!que.empty()) que.pop();
                    break;
                }
            }
        }
    }
    if(flag) cout << "circle" << endl;
    else if(minDist[end] == INT_MAX) cout << "unconnected" << endl;
    else cout << minDist[end] << endl;

    return 0;
}

Bellman_ford算法之单源有限最短路

这节是在第一节的基础上加的内容,主要是限制两个节点中间经过的节点数。

比如,题目要求最多只能经过k个节点,那么也就意味着起始位置到终止位置中间最多只能有k+1条边。

我们只需要限制Bellman_ford算法的松弛次数即可,即从第一节中的n-1变为k+1。但是要注意在每次计算minDist数组的时候要基于所有边上一次松弛的minDist数值进行计算

题目:96. 城市间货物运输III

题目链接:https://kamacoder.com/problempage.php?pid=1154
文章讲解:https://www.programmercarl.com/kamacoder/0096.城市间货物运输III.html
题目状态:看题解

思路一:

在Bellman_ford算法的基础上修改。

代码一:

#include <iostream>
#include <vector>
#include <list>
#include <climits>

using namespace std;

int main()
{
    int src, dst, k, p1, p2, val, m, n;
    cin >> n >> m;
    vector<vector<int>> grid;
    for(int i = 0; i < m; ++i)
    {
        cin >> p1 >> p2 >> val;
        grid.push_back({p1, p2, val});
    }

    cin >> src >> dst >> k;

    vector<int> minDist(n + 1, INT_MAX);
    minDist[src] = 0;
    vector<int> minDist_copy(n + 1); // 用来记录上一次遍历的结果
    for(int i = 1; i <= k + 1; ++i)
    {
        minDist_copy = minDist; // 获取上一次计算的结果
        for(vector<int> &side : grid)
        {
            int from = side[0];
            int to = side[1];
            int price = side[2];
            // 注意使用minDist_copy来计算minDist
            if(minDist_copy[from] != INT_MAX && minDist[to] > minDist_copy[from] + price)
                minDist[to] = minDist_copy[from] + price;
        }
    }
    // 不能到达终点
    if(minDist[dst] == INT_MAX) cout << "unreachable" << endl;
    // 到达终点最短路径
    else cout << minDist[dst] << endl;
}

思路二:

在Bellman_ford的队列优化版本的基础上修改。

代码二:

#include <iostream>
#include <vector>
#include <queue>
#include <list>
#include <climits>
using namespace std;

struct Edge { //邻接表
    int to;  // 链接的节点
    int val; // 边的权重

    Edge(int t, int w): to(t), val(w) {}  // 构造函数
};


int main() {
    int n, m, p1, p2, val;
    cin >> n >> m;

    vector<list<Edge>> grid(n + 1); // 邻接表

    // 将所有边保存起来
    for(int i = 0; i < m; i++){
        cin >> p1 >> p2 >> val;
        // p1 指向 p2,权值为 val
        grid[p1].push_back(Edge(p2, val));
    }
    int start, end, k;
    cin >> start >> end >> k;

    k++;

    vector<int> minDist(n + 1 , INT_MAX);
    vector<int> minDist_copy(n + 1); // 用来记录每一次遍历的结果

    minDist[start] = 0;

    queue<int> que;
    que.push(start); // 队列里放入起点

    int que_size;
    while (k-- && !que.empty()) {

        minDist_copy = minDist; // 获取上一次计算的结果
        que_size = que.size(); // 记录上次入队列的节点个数
        while (que_size--) { // 上一轮松弛入队列的节点,这次对应的边都要做松弛
            int node = que.front(); que.pop();
            for (Edge edge : grid[node]) {
                int from = node;
                int to = edge.to;
                int price = edge.val;
                if (minDist[to] > minDist_copy[from] + price) {
                    minDist[to] = minDist_copy[from] + price;
                    que.push(to);
                }
            }

        }
    }
    if (minDist[end] == INT_MAX) cout << "unreachable" << endl;
    else cout << minDist[end] << endl;

}

标签:val,int,随想录,minDist,que,第五十一,part9,include,节点
From: https://www.cnblogs.com/frontpen/p/18391009

相关文章

  • 代码随想录算法day5 - 哈希表1
    题目1242.有效的字母异位词给定两个字符串*s*和*t*,编写一个函数来判断*t*是否是*s*的字母异位词。字母异位词是通过重新排列不同单词或短语的字母而形成的单词或短语,通常只使用所有原始字母一次。示例1:输入:s="anagram",t="nagaram"输出:true示例2:......
  • 「代码随想录算法训练营」第五十天 | 图论 part8
    目录拓扑排序题目:117.软件构建dijkstra(朴素版)题目:47.参加科学大会dijkstra算法和prim算法的区别dijkstra(堆优化版)题目:47.参加科学大会拓扑排序拓扑排序概括来说就是给出一个有向无环图,把这个有向无环图转成线性的排序,就叫拓扑排序。使用广度优先搜索(BFS)即可。如上图,当我们......
  • 代码随想录算法训练营,8月31日 | 24. 两两交换链表中的节点,19.删除链表的倒数第N个节点
    24.两两交换链表中的节点题目链接:24.两两交换链表中的节点文档讲解︰代码随想录(programmercarl.com)视频讲解︰两两交换链表中的节点日期:2024-08-31做前思路:用上虚拟头指针,从头开始,先指向2再到1,再到3,但要注意保留原本的结点。Java代码如下:classSolution{publicListN......
  • 代码随想录day46 || 647 回文子串, 516 最长回文子序列
    647回文字串funccountSubstrings(sstring)int{ //动规五部曲 //dp[i][j]表示s[i:j+1]区间是否是一个回文 //ifs[i]==s[j]{ifi-j<=1||dp[i+1][j-1]==true{dp[i][j]==true}} //初始化为false //从下往上,从左往右 //print varcountint var......
  • 代码随想录算法day4 - 链表2
    题目124.两两交换链表中的节点给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。示例1:输入:head=[1,2,3,4]输出:[2,1,4,3]示例2:输入:head=[]输出:[]示例3:输入:head=[1]输出:[1]......
  • 代码随想录算法训练营,8月30日 | 203.移除链表元素, 707.设计链表, 206.反转链表
    链表理论基础1.单链表:数据域和指针域(指向下一个结点的位置)组成,头结点,结尾为空指针;双链表:多了一个指向前一个结点的指针;循环链表:结尾指向头结点。2.链表在内存中的存储不是顺序的,跟数组不同,要找一个数据只能通过前一个数据来找,所有这就导致链表的查询比数组麻烦,但是插入删除数据......
  • 「代码随想录算法训练营」第四十九天 | 图论 part7
    目录最小生成树的解题prim算法举例说明(来自代码随想录)题目:53.寻宝Kruskal算法举例说明(来自代码随想录)题目:53.寻宝最小生成树的解题最小生成树类型的题目主要用于解决所有节点的最小连通子图的问题,即:以最小的成本(边的权值)将图中所有节点链接到一起。最小生成树可以使用prim算......
  • 代码随想录day45 || 115 不同子序列, 583 两个字符串删除操作, 72 编辑距离
    115不同子序列funcnumDistinct(sstring,tstring)int{ //动态规划,思考一下判断连续和不连续的区别,如果相等都是左上角+1,如果不等,连续情况就是直接等于左上角,不连续情况直接归零 //dp[i][j]表示s[i]中存在t[j]结尾的的个数 //递推公式,不要求连续字串,所以,如果s[i......
  • 代码随想录算法day3 - 链表1
    题目1203.移除链表元素给你一个链表的头节点head和一个整数val,请你删除链表中所有满足Node.val==val的节点,并返回新的头节点。示例1:输入:head=[1,2,6,3,4,5,6],val=6输出:[1,2,3,4,5]示例2:输入:head=[],val=1输出:[]示例3:输入:head=[7,7,7,7],v......
  • 代码随想录算法训练营,29日 | 704. 二分查找,27. 移除元素,977.有序数组的平方,209.长度最
    数组基础文档讲解︰代码随想录(programmercarl.com)1.连续空间、相同类型元素2.元素只能覆盖3.二维数组的地址连续吗(C++连续,Java不连续)704.二分查找题目链接:704.二分查找文档讲解︰代码随想录(programmercarl.com)视频讲解︰二分查找日期:2024-08-29思路:第一反应是想到二分查......