代码随想录算法训练营五十二天|图论part10

倒计时两天!!!

城市间货物运输I(SPFA也叫Bellman_ford队列优化算法)

题目描述

某国为促进城市间经济交流,决定对货物运输提供补贴。共有 n 个编号为 1 到 n 的城市,通过道路网络连接,网络中的道路仅允许从某个城市单向通行到另一个城市,不能反向通行。

网络中的道路都有各自的运输成本和政府补贴,道路的权值计算方式为:运输成本 - 政府补贴。

权值为正表示扣除了政府补贴后运输货物仍需支付的费用;权值为负则表示政府的补贴超过了支出的运输成本,实际表现为运输过程中还能赚取一定的收益。

请找出从城市 1 到城市 n 的所有可能路径中,综合政府补贴后的最低运输成本。

如果最低运输成本是一个负数,它表示在遵循最优路径的情况下,运输过程中反而能够实现盈利。

城市 1 到城市 n 之间可能会出现没有路径的情况,同时保证道路网络中不存在任何负权回路。

负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。

输入描述

第一行包含两个正整数,第一个正整数 n 表示该国一共有 n 个城市,第二个整数 m 表示这些城市中共有 m 条道路。

接下来为 m 行,每行包括三个整数,s、t 和 v,表示 s 号城市运输货物到达 t 号城市,道路权值为 v(单向图)。

输出描述

如果能够从城市 1 到连通到城市 n, 请输出一个整数,表示运输成本。如果该整数是负数,则表示实现了盈利。如果从城市 1 没有路径可达城市 n,请输出 "unconnected"。

输入示例

6 7
5 6 -2
1 2 1
5 3 1
2 5 2
2 4 -3
4 6 4
1 3 5

输出示例

1

提示信息

示例中最佳路径是从 1 -> 2 -> 5 -> 6,路上的权值分别为 1 2 -2,最终的最低运输成本为 1 + 2 + (-2) = 1。

示例 2:

4 2
1 2 -1
3 4 -1

在此示例中,无法找到一条路径从 1 通往 4,所以此时应该输出 "unconnected"。

数据范围:

1 <= n <= 1000;
1 <= m <= 10000;

-100 <= v <= 100;

Bellman_ford算法每次都是对所有的边进行松弛,其实是多做了一些无用功。实际上只需要对上一次松弛的时候更新过的节点作为出发节点所连接的边进行松弛就够了

我们来模拟一下过程:

初始化,起点为节点1, 起点到起点的最短距离为0,所以minDist[1] 为 0。 将节点1 加入队列 (下次松弛从节点1开始)

从队列里取出节点1,松弛节点1 作为出发点连接的边(节点1 -> 节点2)和边(节点1 -> 节点3)

边:节点1 -> 节点2,权值为1 ,minDist[2] > minDist[1] + 1 ,更新 minDist[2] = minDist[1] + 1 = 0 + 1 = 1 。

边:节点1 -> 节点3,权值为5 ,minDist[3] > minDist[1] + 5,更新 minDist[3] = minDist[1] + 5 = 0 + 5 = 5。

将节点2、节点3 加入队列,如图:

从队列里取出节点2,松弛节点2 作为出发点连接的边(节点2 -> 节点4)和边(节点2 -> 节点5)

边:节点2 -> 节点4,权值为1 ,minDist[4] > minDist[2] + (-3) ,更新 minDist[4] = minDist[2] + (-3) = 1 + (-3) = -2 。

边:节点2 -> 节点5,权值为2 ,minDist[5] > minDist[2] + 2 ,更新 minDist[5] = minDist[2] + 2 = 1 + 2 = 3 。

将节点4,节点5 加入队列,如图:

从队列里出去节点3,松弛节点3 作为出发点连接的边。

因为没有从节点3作为出发点的边,所以这里就从队列里取出节点3就好,不用做其他操作。

从队列中取出节点4,松弛节点4作为出发点连接的边(节点4 -> 节点6)

边:节点4 -> 节点6,权值为4 ,minDist[6] > minDist[4] + 4,更新 minDist[6] = minDist[4] + 4 = -2 + 4 = 2 。

将节点6加入队列

如图:

从队列中取出节点5,松弛节点5作为出发点连接的边(节点5 -> 节点3),边(节点5 -> 节点6)

边:节点5 -> 节点3,权值为1 ,minDist[3] > minDist[5] + 1 ,更新 minDist[3] = minDist[5] + 1 = 3 + 1 = 4

边:节点5 -> 节点6,权值为-2 ,minDist[6] > minDist[5] + (-2) ,更新 minDist[6] = minDist[5] + (-2) = 3 - 2 = 1

如图,将节点3加入队列,因为节点6已经在队列里,所以不用重复添加。

所以我们在加入队列的过程可以有一个优化,用visited数组记录已经在队列里的元素,已经在队列的元素不用重复加入

从队列中取出节点6,松弛节点6 作为出发点连接的边。

节点6作为终点,没有可以出发的边。

同理从队列中取出节点3,也没有可以出发的边

所以直接从队列中取出,如图:

代码如下:

import java.util.*;

class Edge {
    int t;
    int v;

    public Edge(int t, int v) {
        this.t = t;
        this.v = v;
    }
}

public class Main {
    private static List<List<Edge>> graph = new ArrayList<>();

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        int m = in.nextInt();
        int[] minDist = new int[n + 1];
        boolean[] visited = new boolean[n + 1];
        for (int i = 2; i <= n; i++) minDist[i] = Integer.MAX_VALUE;
        for (int i = 0; i <= n; i++) graph.add(new ArrayList<>());
        for (int i = 0; i < m; i++) {
            int s = in.nextInt();
            int t = in.nextInt();
            int v = in.nextInt();
            graph.get(s).add(new Edge(t, v));
        }
        Queue<Integer> q = new LinkedList<>();
        q.offer(1);
        visited[1] = true;
        while (!q.isEmpty()) {
            int cur = q.poll();
            visited[cur] = false;
            for (Edge edge : graph.get(cur)) {
                if (minDist[edge.t] > minDist[cur] + edge.v) {
                    minDist[edge.t] = minDist[cur] + edge.v;
                    if (visited[edge.t] == false) {
                        q.offer(edge.t);
                        visited[edge.t] = true;
                    }
                }
            }
        }
        if (minDist[n] != Integer.MAX_VALUE) System.out.println(minDist[n]);
        else System.out.println("unconnected");
    }
}

城市间货物运输II(Bellman_ford之判断负权回路)

【题目描述】

某国为促进城市间经济交流,决定对货物运输提供补贴。共有 n 个编号为 1 到 n 的城市,通过道路网络连接,网络中的道路仅允许从某个城市单向通行到另一个城市,不能反向通行。

网络中的道路都有各自的运输成本和政府补贴,道路的权值计算方式为:运输成本 - 政府补贴。权值为正表示扣除了政府补贴后运输货物仍需支付的费用;

权值为负则表示政府的补贴超过了支出的运输成本,实际表现为运输过程中还能赚取一定的收益。

然而,在评估从城市 1 到城市 n 的所有可能路径中综合政府补贴后的最低运输成本时,存在一种情况:图中可能出现负权回路

负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。

为了避免货物运输商采用负权回路这种情况无限的赚取政府补贴,算法还需检测这种特殊情况。

请找出从城市 1 到城市 n 的所有可能路径中,综合政府补贴后的最低运输成本。同时能够检测并适当处理负权回路的存在。

城市 1 到城市 n 之间可能会出现没有路径的情况

【输入描述】

第一行包含两个正整数,第一个正整数 n 表示该国一共有 n 个城市,第二个整数 m 表示这些城市中共有 m 条道路。

接下来为 m 行,每行包括三个整数,s、t 和 v,表示 s 号城市运输货物到达 t 号城市,道路权值为 v。

【输出描述】

如果没有发现负权回路,则输出一个整数,表示从城市 1 到城市 n 的最低运输成本(包括政府补贴)。

如果该整数是负数,则表示实现了盈利。如果发现了负权回路的存在,则输出 "circle"。如果从城市 1 无法到达城市 n,则输出 "unconnected"。

输入示例

4 4
1 2 -1
2 3 1
3 1 -1 
3 4 1

输出示例

circle

提示信息

路径中存在负权回路,从 1 -> 2 -> 3 -> 1,总权值为 -1,理论上货物运输商可以在该回路无限循环赚取政府补贴,所以输出 "circle" 表示已经检测出了该种情况。

数据范围:

1 <= n <= 1000;
1 <= m <= 10000;

-100 <= v <= 100;

在Bellman_ford算法中,对所有边进行n-1次松弛我们得到了源点到目的地的最短路径,因为一个有n个节点的图最多有n-1条边。也就是说,进行n次松弛和进行n-1次松弛的结果是一样的(如果图中不存在负权回路)。但是如果一个图中存在负权回路,那么每松弛一次,都会更新最短路径,所以结果会一直变化。

所以本题的思路很简单,直接看松弛n-1和松弛n次的结果是不是一样的,如果一样,说明没有负权回路;反之说明存在负权回路。

代码如下:

import java.util.*;

class Edge {
    int s;
    int t;
    int v;

    public Edge(int s, int t, int v) {
        this.s = s;
        this.t = t;
        this.v = v;
    }
}

public class Main {
    private static List<Edge> edges = new ArrayList<>();

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        int m = in.nextInt();
        int[] minDist = new int[n + 1];
        for (int i = 2; i <= n; i++) minDist[i] = Integer.MAX_VALUE;
        for (int i = 0; i < m; i++) {
            int s = in.nextInt();
            int t = in.nextInt();
            int v = in.nextInt();
            edges.add(new Edge(s, t, v));
        }
        for (int i = 1; i <= n - 1; i++) {
            for (Edge edge : edges) {
                if (minDist[edge.s] != Integer.MAX_VALUE
                        && minDist[edge.t] > minDist[edge.s] + edge.v) {
                    minDist[edge.t] = minDist[edge.s] + edge.v;
                }
            }
        }
        int[] tmp = new int[n + 1];
        for (int i = 0; i <= n; i++) tmp[i] = minDist[i];
        for (Edge edge : edges) {
            if (minDist[edge.s] != Integer.MAX_VALUE
                    && minDist[edge.t] > minDist[edge.s] + edge.v) {
                minDist[edge.t] = minDist[edge.s] + edge.v;
            }
        }
        for(int i=0;i<n;i++){
            if(tmp[i]!=minDist[i]){
                System.out.println("circle");
                return;
            }
        }
        if(minDist[n]!=Integer.MAX_VALUE)System.out.println(minDist[n]);
        else System.out.println("unconnected");
    }
}

城市间货物运输III(Bellman_ford之单源有限最短路)

【题目描述】

某国为促进城市间经济交流,决定对货物运输提供补贴。共有 n 个编号为 1 到 n 的城市,通过道路网络连接,网络中的道路仅允许从某个城市单向通行到另一个城市,不能反向通行。

网络中的道路都有各自的运输成本和政府补贴,道路的权值计算方式为:运输成本 - 政府补贴。

权值为正表示扣除了政府补贴后运输货物仍需支付的费用;

权值为负则表示政府的补贴超过了支出的运输成本,实际表现为运输过程中还能赚取一定的收益。

请计算在最多经过 k 个城市的条件下,从城市 src 到城市 dst 的最低运输成本。

【输入描述】

第一行包含两个正整数,第一个正整数 n 表示该国一共有 n 个城市,第二个整数 m 表示这些城市中共有 m 条道路。

接下来为 m 行,每行包括三个整数,s、t 和 v,表示 s 号城市运输货物到达 t 号城市,道路权值为 v。

最后一行包含三个正整数,src、dst、和 k,src 和 dst 为城市编号,从 src 到 dst 经过的城市数量限制。

【输出描述】

输出一个整数,表示从城市 src 到城市 dst 的最低运输成本,如果无法在给定经过城市数量限制下找到从 src 到 dst 的路径,则输出 "unreachable",表示不存在符合条件的运输方案。

输入示例

6 7
1 2 1
2 4 -3
2 5 2
1 3 5
3 5 1
4 6 4
5 6 -2
2 6 1

输出示例

0

提示信息

从 2 -> 5 -> 6 中转一站,运输成本为 0。 

1 <= n <= 1000; 

1 <= m <= 10000; 

-100 <= v <= 100;

在Bellman_ford算法中对所有边松弛一次,相当于计算起点到达与起点一条边相连的节点的最短距离。本题中从src走到dst最多经过k个城市,那么就是最多经过k+1条边。

所以就相当于从src开始,对所有边松弛k+1次,得到minDist[dst]

这里需要注意一个问题:

城市间货物运输I,是不存在负权回路的,所以说松弛多少次,对结果都是没有影响的。也就是说,就算计算minDist是基于本次计算的minDist的,也不会有影响;

但是本题是存在负权回路的,如果多做了松弛,结果是会变得,而且本题对松弛次数是有要求的,所以计算minDist一定要基于上一次计算的minDist,所以我们需要一个minDistC数组来保存上一次minDist的计算结果。

代码如下:

import java.util.*;

class Edge{
    int s;
    int t;
    int v;
    public Edge(int s,int t,int v){
        this.s=s;
        this.t=t;
        this.v=v;
    }
}
public class Main{
    private static List<Edge> edges=new ArrayList<>();
    public static void main(String[] args){
        Scanner in=new Scanner(System.in);
        int n=in.nextInt();
        int m=in.nextInt();
        int[] minDist=new int[n+1];
        for(int i=0;i<=n;i++)minDist[i]=Integer.MAX_VALUE;
        for(int i=0;i<m;i++){
            int s=in.nextInt();
            int t=in.nextInt();
            int v=in.nextInt();
            edges.add(new Edge(s,t,v));
        }
        int src=in.nextInt();
        minDist[src]=0;
        int dst=in.nextInt();
        int k=in.nextInt();
        int[] minDistC=new int[n+1];
        for(int i=1;i<=k+1;i++){
            for(int j=0;j<=n;j++)minDistC[j]=minDist[j];
            for(Edge edge:edges){
                if(minDistC[edge.s]!=Integer.MAX_VALUE&&minDist[edge.t]>minDistC[edge.s]+edge.v){
                    minDist[edge.t]=minDistC[edge.s]+edge.v;
                }
            }
        }
        if(minDist[dst]!=Integer.MAX_VALUE)System.out.println(minDist[dst]);
        else System.out.println("unreachable");
    }
}
第二十二天的算法训练营主要涵盖了Leetcode题目中的三道题目,分别是Leetcode 28 "Find the Index of the First Occurrence in a String",Leetcode 977 "有序组的平方",和Leetcode 209 "长度最小的子组"。 首先是Leetcode 28题,题目要求在给定的字符串中找到第一个出现的字符的索引。思路是使用双指针来遍历字符串,一个指向字符串的开头,另一个指向字符串的结尾。通过比较两个指针所指向的字符是否相等来判断是否找到了第一个出现的字符。具体实现的代码如下: ```python def findIndex(self, s: str) -> int: left = 0 right = len(s) - 1 while left <= right: if s[left == s[right]: return left left += 1 right -= 1 return -1 ``` 接下来是Leetcode 977题,题目要求对给定的有序组中的元素进行平方,并按照非递减的顺序返回结果。这里由于组已经是有序的,所以可以使用双指针的方来解决问题。一个指针指向组的开头,另一个指针指向组的末尾。通过比较两个指针所指向的元素的绝对的大小来确定哪个元素的平方应该放在结果组的末尾。具体实现的代码如下: ```python def sortedSquares(self, nums: List[int]) -> List[int]: left = 0 right = len(nums) - 1 ans = [] while left <= right: if abs(nums[left]) >= abs(nums[right]): ans.append(nums[left ** 2) left += 1 else: ans.append(nums[right ** 2) right -= 1 return ans[::-1] ``` 最后是Leetcode 209题,题目要求在给定的组中找到长度最小的子组,使得子组的和大于等于给定的目标。这里可以使用滑动窗口的方来解决问题。使用两个指针来表示滑动窗口的左边界和右边界,通过移动指针来调整滑动窗口的大小,使得滑动窗口中的元素的和满足题目要求。具体实现的代码如下: ```python def minSubArrayLen(self, target: int, nums: List[int]) -> int: left = 0 right = 0 ans = float('inf') total = 0 while right < len(nums): total += nums[right] while total >= target: ans = min(ans, right - left + 1) total -= nums[left] left += 1 right += 1 return ans if ans != float('inf') else 0 ``` 以上就是第二十二天的算法训练营的内容。通过这些题目的练习,可以提升对双指针和滑动窗口等算法的理解和应用能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值