报名明年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