<蓝桥杯软件赛>零基础备赛20周--第19周--最短路

报名明年4月蓝桥杯软件赛的同学们,如果你是大一零基础,目前懵懂中,不知该怎么办,可以看看本博客系列:备赛20周合集
20周的完整安排请点击:20周计划
每周发1个博客,共20周。
在QQ群上交流答疑:

在这里插入图片描述

第19周:最短路

  最短路问题是最广为人知的图论问题,也是蓝桥考核最多的图论问题。
  在“第十四周 BFS”中提到BFS也是一种很不错的最短路算法。不过它只适合一种场景:任意的相邻两点之间距离相等,一般把这个距离看成1,称为“1跳”,从起点到终点的路径长度就是多少个“跳数”。在这种场景下,查找一个起点到一个终点的最短距离,BFS是最优的最短路径算法,计算复杂度是O(n),n是图上点的数量。
  在更多的应用场景中,需要用不同的算法来解决, 有这些通用的最短路径算法:Floyd、Dijkstra、Bellman-ford、SPFA算法。
  Floyd算法是最简单的最短路径算法,代码仅有4行且非常易懂。它的效率不高,不能用于大图,但是在某些场景下也有自己的优势,难以替代。Floyd算法是一种“多源”最短路算法,一次计算能得到图中每一对结点之间(多对多)的最短路径。
  Bellman-Ford、Dijkstra、SPFA算法都是“单源”最短路径算法,一次计算能得到一个起点到其他所有点(一对多)的最短路径。
  蓝桥杯以前经常考最短路,考过Floyd、BFS、Bellman-Ford、Dijkstra,不过这2年没怎么出题,是不是因为以前考多了,累了。例如2023年省赛的几十道题中只考了一题最短路,用到Dijkstra。
  下面介绍Floyd、Bellman-Ford、Dijkstra。Floyd、Bellman-Ford都非常简单,代码短,存图的数据结构简单。Dijkstra难一些,初学者可能比较费劲。

1. Floyd算法

  求图上两点i、j之间的最短距离,可以按“从小图到全图”的步骤,在逐步扩大图的过程中计算和更新最短路,这是动态规划的思路。定义状态为dp[k][i][j],i、j、k是点的编号,范围1 ~ n。状态dp[k][i][j]表示在包含1 ~ k点的子图上,点对i、j之间的最短路。当从子图1 ~ k-1扩展到子图1 ~ k时,状态转移方程这样设计:
    dp[k][i][j] = min(dp[k-1][i][j], dp[k-1][i][k] + dp[k-1][k][j])
  计算过程如下图所示,虚线圆圈内是包含了1 ~ k-1点的子图。方程中的dp[k-1][i][j]是虚线子图内的点对i、j的最短路;dp[k-1][i][k] + dp[k-1][k][j]是经过k点的新路径的长度,即这条路径从i出发,先到k,再从k到终点j。比较不经过k的最短路径dp[k-1][i][j]和经过k的新路径,较小者就是新的dp[k][i][j]。每次扩展一个新点k时,都能用到1 ~ k-1的结果,从而提高了效率。这就是动态规划的方法。
在这里插入图片描述
      图1 从子图1~k-1扩展到1~k

  当k从1逐步扩展到n时,最后得到的dp[n][i][j]是点对i、j之间的最短路径长度。由于i和j是图中所有的点对,所以能得到所有点对之间的最短路。
  初值dp[0][i][j],若i、j是直连的,就是它们的边长;若不直连,赋值为无穷大。
  由于i、j是任意点对,所以计算结束后得到了所有点对之间的最短路。
  下面是代码,仅有4行。这里把dp[][][]缩小成了dp[][],用到了滚动数组,因为dp[k][][]只和dp[k-1][][]有关,所以可以省掉k这一维。由于k是动态规划的子问题的“阶段”,即k是从点1开始逐步扩大到n的,所以k循环必须放在i、j循环的外面。三重循环,复杂度 O ( n 3 ) O(n^3) O(n3)

for(int k=1; k<=n; k++)         //floyd的三重循环
    for(int i=1; i<=n; i++)
        for(int j=1; j<=n; j++)      // k循环在i、j循环外面
            dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]); //比较:不经过k、经过k

  Floyd算法的寻路极为盲目,几乎“毫无章法”,这是它的效率低于其他算法的原因。但是,这种“毫无章法”,在某些情况下却有优势。
  与其他最短路径算法相比,Floyd有以下特点。
  (1)能在一次计算后求得所有结点之间的最短距离,其他最短路径算法都做不到。
  (2)代码极其简单,是最简单的最短路算法。三重循环结束后,所有点对之间的最短路都得到了。
  (3)效率低下,计算复杂度是 O ( n 3 ) O(n^3) O(n3),只能用于n < 300的小规模的图。
  (4)存图用邻接矩阵dp[][]是最好最合理的,不用更省空间的邻接表。因为Floyd算法计算的结果是所有点对之间的最短路,本身就需要 n 2 n^2 n2的空间,用矩阵存储最合适。
  (5)能判断负圈。负圈是什么?若图中有权值为负的边,某个经过这个负边的环路,所有边长相加的总长度也是负数,这就是负圈。在这个负圈上每绕一圈,总长度就更小,从而陷入在负圈上兜圈子的死循环。Floyd算法很容易判断负圈,只要在算法运行过程出现任意一个dp[i][i] < 0就说明有负圈。因为dp[i][i]是从i出发,经过其他中转点绕一圈回到自己的最短路径,如果小于零,就存在负圈。
  下面的场景适用Floyd算法。
  (1)图的规模小,点数n < 400。计算复杂度 O ( n 3 ) O(n^3) O(n3)限制了图的规模。这种小图不需要用其他算法,其他算法的代码长,写起来麻烦。
  (2)问题的解决和中转点有关。这是Floyd算法的核心思想,算法用DP方法遍历中转点来计算最短路。
  (3)路径在“兜圈子”,一个点可能多次经过。这是Floyd算法的特长,其他路径算法都不行。
  (4)允许多次询问不同点对之间的最短路。这是Floyd算法的优势。


蓝桥公园
【题目描述】小明来到了蓝桥公园。已知公园有N个景点,景点和景点之间一共有M条道路。小明有Q个观景计划,每个计划包含一个起点st 和一个终点ed,表示他想从st去到ed。但是小明的体力有限,对于每个计划他想走最少的路完成,你可以帮帮他吗?
【输入描述】输入第一行包含三个正整数 N,M,Q。第2到M+1行每行包含三个正整数u,v,w,表示 u、v之间存在一条距离为w的路。第M+2到M+Q−1行每行包含两个正整数st,ed,其含义如题所述。
1≤N≤400,1≤M≤N×(N−1)/2,Q≤103,1≤u,v,st,ed≤n,1≤w≤109
【输出描述】输出共Q行,对应输入数据中的查询。若无法从st到达ed则输出−1。


  这一题简单演示了Floyd算法的基本应用。边数1 ≤ M ≤ N×(N−1)/2,说明是一个稠密图。当M = N×(N−1)/2时,任意两个点之间都有边。
  代码很简单,但是也有一些坑点,请仔细看注释。
c++代码

#include <bits/stdc++.h>
using namespace std;
const long long INF = 0x3f3f3f3f3f3f3f3fLL;  //这样定义INF的好处是: INF <= INF+x
const int N = 405;
long long dp[N][N];
int n,m,q;
void input(){
   
   
   // for(int i = 1; i <= n; i++)
   //     for(int j = 1; j <= n; j++)   dp[i][j] = INF;
    memset(dp,0x3f,sizeof(dp));                //初始化,和上面2行功能一样
    for(int i = 1; i <= m; i++){
   
   
        int u,v;long long w;
        cin >> u >> v >> w;
        dp[u][v]=dp[v][u] = min(dp[u][v] , w);   //防止有重边
    }
}
void floyd(){
   
   
    for(int k = 1; k <= n; k++)
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= n; j++)
                dp[i][j] = min(dp[i][j] , dp[i][k] + dp[k][j]);
}
void output(){
   
       
    while(q--){
   
   
        int s, t; cin >> s >>t;
        if(dp[s][t]==INF) cout << "-1" <<endl;
        else if(s==t) cout << "0" <<endl;      //如果不这样,dp[i][i]不等于0
        else          cout <<dp[s][t]<<endl;
    }
}
int main(){
   
   
    cin >> n>> m >> q;
    input();     floyd();    output();
    return 0;
}

java代码

import java.util.Arrays;
import java.util.Scanner;

public class Main {
   
   
    static final long INF = 0x3f3f3f3f3f3f3f3fL;
    static final int N = 405;
    static long[][] dp = new long[N][N];
    static int n, m, q;
    public static void main(String[] args) {
   
   
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        m = sc.nextInt();
        q = sc.nextInt();
        input(sc);
        floyd();
        output(sc);
    }

    static void input(Scanner sc) {
   
   
        for (int i = 1; i <= n; i++)  Arrays
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

罗勇军

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值