最短路
最短路
引入
在图中,求出某一点到任意一点的最短路径
单源最短路
Dijkstra
定义
松弛
在理解
我们尝试用
简单来说就是选择更短的路径
对于边
过程
将结点分成两个集合:
- 已确定最短路长度的点集(记为
集合)。 - 未确定最短路长度的点集(记为
集合)。
一开始所有的点都属于
初始化
然后重复这些操作:
- 从
集合中,选取一个最短路长度最小的结点,移到 集合中。 - 对那些刚刚被加入
集合的结点的所有出边执行松弛操作。
直到
若
时间复杂度
对于操作
采取不同方法有着不同的时间复杂度,以下给出两种常用的方法及总时间复杂度:
- 暴力:不使用任何数据结构进行维护,每次
操作执行完毕后,直接在 集合中暴力寻找最短路长度最小的结点。 操作总时间复杂度为 , 操作总时间复杂度为 ,全过程的时间复杂度为 。 - 优先队列:每成功松弛一条边
,就将 加入优先队列中,如果同一个点的最短路被更新多次,但对于优先队列前面的元素不能删除也不能修改,只能留在队列中,因此优先队列内的元素个数是 的,总时间复杂度为
实现(采用邻接表存图)
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Scanner;
public class Main {
static class edge {
int v;
long w;
public edge(int v, long w) {
this.v = v;
this.w = w;
}
}
static LinkedList<edge>[] adj;
//dis[i]表示 S -> i 的最短路长度
static long[] dis;
//vis[i]判断i号节点是否被选择
static boolean[] vis;
// n个点,m条边
static int n, m;
//不要赋值为MAX,程序可能会进行加法等操作导致溢出
static final long INF = (long) 1e12;
// 求以start为源点的单源最短路
static void Dijkstra(int start) {
Arrays.fill(dis, INF);
Arrays.fill(vis, false);
//自己到自己的最短路为0
dis[start] = 0;
for (int i = 1; i <= n; ++i) {
//操作1:在未确定的集合T中选择最短路长度最小的结点
int u = 0;
long min = INF;
for (int j = 1; j <= n; ++j)
if (!vis[j] && dis[j] < min) {
u = j;
min = dis[j];
}
//如果u为初始值,则说明所有节点都被选择了
if (u == -1) return;
//将其加入集合S中
vis[u] = true;
//操作2:对加入的结点的所有出边进行松弛操作
for (edge e : adj[u])
//松弛操作
if (dis[e.v] > dis[u] + e.w)
dis[e.v] = dis[u] + e.w;
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
//点的标号从1开始,即1~n
adj = new LinkedList[n + 1];
for (int i = 1; i <= n; ++i) adj[i] = new LinkedList<>();
dis = new long[n + 1];
vis = new boolean[n + 1];
// start为源点
int start = sc.nextInt();
for (int i = 0; i < m; ++i) {
int u = sc.nextInt(), v = sc.nextInt();
long w = sc.nextLong();
// 加边(有向边)
adj[u].add(new edge(v, w));
}
Dijkstra(start);
for (int i = 1; i <= n; ++i) System.out.printf("%d到%d的最短路径长度为%d\n", start, i, dis[i]);
}
}
import java.util.Arrays;
import java.util.LinkedList;
import java.util.PriorityQueue;
import java.util.Scanner;
public class Main {
static class edge {
int v;
long w;
public edge(int v, long w) {
this.v = v;
this.w = w;
}
}
static LinkedList<edge>[] adj;
//dis[i]表示 S -> i 的最短路长度
static long[] dis;
//vis[i]判断i号节点是否被选择
static boolean[] vis;
// n个点,m条边
static int n, m;
//不要赋值为MAX,程序可能会进行加法等操作导致溢出
static final long INF = (long) 1e12;
// 求以start为源点的单源最短路
static void Dijkstra(int start) {
Arrays.fill(dis, INF);
Arrays.fill(vis, false);
class node {
int u;
long dis;
public node(int u, long dis) {
this.u = u;
this.dis = dis;
}
}
PriorityQueue<node> q = new PriorityQueue<>(((o1, o2) -> (int) (o1.dis - o2.dis)));
//自己到自己的最短路为0
dis[start] = 0;
q.add(new node(start, 0));
while (!q.isEmpty()) {
//操作1:选择最短路长度最小的结点
int u = q.poll().u;
//如果已经在集合中,则选择下一个节点
if (vis[u]) continue;
vis[u] = true;
//操作2:对加入的结点的所有出边进行松弛操作
for (edge e : adj[u])
//松弛操作
if (dis[e.v] > dis[u] + e.w) {
dis[e.v] = dis[u] + e.w;
q.add(new node(e.v, dis[e.v]));
}
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
//点的标号从1开始,即1~n
adj = new LinkedList[n + 1];
for (int i = 1; i <= n; ++i) adj[i] = new LinkedList<>();
dis = new long[n + 1];
vis = new boolean[n + 1];
// start为源点
int start = sc.nextInt();
for (int i = 0; i < m; ++i) {
int u = sc.nextInt(), v = sc.nextInt();
long w = sc.nextLong();
// 加边(有向边)
adj[u].add(new edge(v, w));
}
Dijkstra(start);
for (int i = 1; i <= n; ++i) System.out.printf("%d到%d的最短路径长度为%d\n", start, i, dis[i]);
}
}
测试数据
输入:
4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4
输出:
1到1的最短路径长度为0
1到2的最短路径长度为2
1到3的最短路径长度为4
1到4的最短路径长度为3
输出路径
每次更新最短路径时,通过数组将 来结点(前驱节点)记录下来
以下给出使用优先队列的输出路径
import java.util.Arrays;
import java.util.LinkedList;
import java.util.PriorityQueue;
import java.util.Scanner;
public class Main {
static class edge {
int v;
long w;
public edge(int v, long w) {
this.v = v;
this.w = w;
}
}
static LinkedList<edge>[] adj;
static long[] dis;
static boolean[] vis;
static int n, m;
static final long INF = (long) 1e12;
static int[] pre;
static void Dijkstra(int start) {
Arrays.fill(dis, INF);
Arrays.fill(vis, false);
class node {
int u;
long dis;
public node(int u, long dis) {
this.u = u;
this.dis = dis;
}
}
PriorityQueue<node> q = new PriorityQueue<>(((o1, o2) -> (int) (o1.dis - o2.dis)));
dis[start] = 0;
q.add(new node(start, 0));
while (!q.isEmpty()) {
int u = q.poll().u;
if (vis[u]) continue;
vis[u] = true;
for (edge e : adj[u])
if (dis[e.v] > dis[u] + e.w) {
dis[e.v] = dis[u] + e.w;
q.add(new node(e.v, dis[e.v]));
pre[e.v] = u;
}
}
}
static String _findPathTool(int now) {
String ans = "";
//如果还有前驱节点
if (pre[now] != 0) ans += _findPathTool(pre[now]);
ans += now + "->";
return ans;
}
static String findPath(int start, int end) {
if (start == end) return "自身到自身";
if (pre[end] == 0) return "无最短路";
return _findPathTool(pre[end]) + end;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
adj = new LinkedList[n + 1];
for (int i = 1; i <= n; ++i) adj[i] = new LinkedList<>();
dis = new long[n + 1];
vis = new boolean[n + 1];
pre = new int[n + 1];
int start = sc.nextInt();
for (int i = 0; i < m; ++i) {
int u = sc.nextInt(), v = sc.nextInt();
long w = sc.nextLong();
adj[u].add(new edge(v, w));
}
Dijkstra(start);
for (int i = 1; i <= n; ++i)
System.out.printf("%d到%d的最短路径长度为%d,最短路径为:%s\n", start, i, dis[i], findPath(start, i));
}
}
测试数据:
输入:
4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4
输出:
1到1的最短路径长度为0,最短路径为:自身到自身
1到2的最短路径长度为2,最短路径为:1->2
1到3的最短路径长度为4,最短路径为:1->2->3
1到4的最短路径长度为3,最短路径为:1->2->4
例题
实现
import java.io.*;
import java.util.Arrays;
import java.util.LinkedList;
public class Main {
static class edge {
int v;
int w;
public edge(int v, int w) {
this.v = v;
this.w = w;
}
}
static LinkedList<edge>[] adj;
static int[] dis;
static boolean[] vis;
static int n, m;
static final int INF = Integer.MAX_VALUE;
static void Dijkstra(int start) {
Arrays.fill(dis, INF);
Arrays.fill(vis, false);
//自己到自己的最短路为0
dis[start] = 0;
for (int i = 1; i <= n; ++i) {
//操作1:在未确定的集合T中选择最短路长度最小的结点
int u = 0;
long min = INF;
for (int j = 1; j <= n; ++j)
if (!vis[j] && dis[j] < min) {
u = j;
min = dis[j];
}
//如果u为初始值,则说明所有节点都被选择了
if (u == -1) return;
//将其加入集合S中
vis[u] = true;
//操作2:对加入的结点的所有出边进行松弛操作
for (edge e : adj[u])
//松弛操作
if (dis[e.v] > (long) dis[u] + e.w)
dis[e.v] = dis[u] + e.w;
}
}
static StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter out = new PrintWriter(System.out);
static int get() throws IOException {
in.nextToken();
return (int) in.nval;
}
public static void main(String[] args) throws IOException {
n = get();
m = get();
//点的标号从1开始,即1~n
adj = new LinkedList[n + 1];
for (int i = 1; i <= n; ++i) adj[i] = new LinkedList<>();
dis = new int[n + 1];
vis = new boolean[n + 1];
// start为源点
int start = get();
for (int i = 0; i < m; ++i) {
int u = get(), v = get(), w = get();
// 加边(有向边)
adj[u].add(new edge(v, w));
}
Dijkstra(start);
for (int i = 1; i <= n; ++i) out.print(dis[i] + " ");
out.close();
}
}
实现
import java.io.*;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.PriorityQueue;
public class Main {
static class edge {
int v, w;
public edge(int v, int w) {
this.v = v;
this.w = w;
}
}
static LinkedList<edge>[] adj;
static int[] dis;
static boolean[] vis;
static int n, m;
static final int INF = Integer.MAX_VALUE;
static void Dijkstra(int start) {
Arrays.fill(dis, INF);
Arrays.fill(vis, false);
class node {
int u, dis;
public node(int u, int dis) {
this.u = u;
this.dis = dis;
}
}
PriorityQueue<node> q = new PriorityQueue<>(((o1, o2) -> o1.dis - o2.dis));
//自己到自己的最短路为0
dis[start] = 0;
q.add(new node(start, 0));
while (!q.isEmpty()) {
//操作1:选择最短路长度最小的结点
int u = q.poll().u;
//如果已经在集合中,则选择下一个节点
if (vis[u]) continue;
vis[u] = true;
//操作2:对加入的结点的所有出边进行松弛操作
for (edge e : adj[u])
//松弛操作
if (dis[e.v] > (long) dis[u] + e.w) {
dis[e.v] = dis[u] + e.w;
q.add(new node(e.v, dis[e.v]));
}
}
}
static StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter out = new PrintWriter(System.out);
static int get() throws IOException {
in.nextToken();
return (int) in.nval;
}
public static void main(String[] args) throws IOException {
n = get();
m = get();
//点的标号从1开始,即1~n
adj = new LinkedList[n + 1];
for (int i = 1; i <= n; ++i) adj[i] = new LinkedList<>();
dis = new int[n + 1];
vis = new boolean[n + 1];
// start为源点
int start = get();
for (int i = 0; i < m; ++i) {
int u = get(), v = get(), w = get();
// 加边(有向边)
adj[u].add(new edge(v, w));
}
Dijkstra(start);
for (int i = 1; i <= n; ++i) out.print(dis[i] + " ");
out.close();
}
}
全源最短路
例题:P2910 Clear And Present Danger S - 洛谷
全源最短路可以通过单源最短路求得,即对任意两个点求单源最短路。
但有更简单的方法
Floyd
forforfor 求最短路
-
是用来求任意两个结点之间的最短路的。
-
复杂度比较高,但是常数小,容易实现。
-
适用于任何图,不管有向无向,边权正负,但是最短路必须存在。(不能有负环)
-
是一种动态规划算法,也称插点法(每次在两个点的路径间新增点)。
过程
定义
对于
- 路径不经过
点,则 - 路径经过
点,则
因此
注意,当计算第
初始化
- 当
时,由于自身到自身的最短路径长度为 ,则 - 当
且存在 直接相连的边时, ,其中 为 到 的边权 - 当
且存在 直接相连的边时,
优化
重看状态计算:
- 当路径不经过
点时,相当于直接将 层的值复制到 层 - 当路径经过
点时,第 层的 和 都不经过 点,可以直接投影到第 层的对应位置,即
因此,
性质
- 每新增一个点,就等于新增一个桥,尝试更新那些经过新桥的最短路径
- 当循环完
层后,会将有插点编号不超过 的最短路径都找出来,但是插点编号超过 的最短路径是找不出来的 - 当枚举完
层,所有的最短路径都找出来了 - 当
算法运行后,若 为初值 ,则无最短路径,即他们不可达
时间复杂度
显然,时间复杂度为
实现
import java.util.Scanner;
public class Main {
static int[][] dis;
static int n, m;
static final int INF = (int) 1e9;
static void Floyd() {
for (int k = 1; k <= n; ++k)
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
//dis[i][j] = Math.min(dis[i][j], dis[i][k] + dis[k][j]);
if (dis[i][j] > dis[i][k] + dis[k][j]) dis[i][j] = dis[i][k] + dis[k][j];
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
//点的标号从1开始,即1~n
dis = new int[n + 1][n + 1];
//初始化
for (int i = 1; i <= n; ++i) {
for (int j = 1; j < i; ++j) dis[i][j] = dis[j][i] = INF;
dis[i][i] = 0;
}
for (int i = 0; i < m; ++i) {
//有向边
int u = sc.nextInt(), v = sc.nextInt(), w = sc.nextInt();
dis[u][v] = w;
}
Floyd();
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j) {
if (dis[i][j] == INF) System.out.printf("%d不可达到%d\n", i, j);
else System.out.printf("%d到%d的最短路径长度为%d\n", i, j, dis[i][j]);
}
}
}
测试数据:
输入:
4 6
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4
输出:
1到1的最短路径长度为0
1到2的最短路径长度为2
1到3的最短路径长度为4
1到4的最短路径长度为3
2不可达到1
2到2的最短路径长度为0
2到3的最短路径长度为2
2到4的最短路径长度为1
3不可达到1
3不可达到2
3到3的最短路径长度为0
3到4的最短路径长度为3
4不可达到1
4不可达到2
4不可达到3
4到4的最短路径长度为0
输出路径
每次更新最短路径时,记录插点,即记录这次结果是从哪个中转点过来的
import java.util.Scanner;
public class Main {
static int[][] dis;
static int[][] path;
static int n, m;
static final int INF = (int) 1e9;
static void Floyd() {
for (int k = 1; k <= n; ++k)
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
if (dis[i][j] > dis[i][k] + dis[k][j]) {
dis[i][j] = dis[i][k] + dis[k][j];
path[i][j] = k;
}
}
static String _findPathTool(int x, int y) {
String ans = "";
int k = path[x][y];
// 如果 x 到 k 有插点
if (path[x][k] != 0) ans += _findPathTool(x, k);
// 中转点
ans += k + "->";
// 如果 k 到 y 有插点
if (path[k][y] != 0) ans += _findPathTool(k, y);
return ans;
}
static String findPath(int x, int y) {
if (x == y) return "自身到自身";
// 如果x到y间没有插点
if (path[x][y] == 0) return x + "->" + y;
return x + "->" + _findPathTool(x, y) + y;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
//点的标号从1开始,即1~n
dis = new int[n + 1][n + 1];
path = new int[n + 1][n + 1];
//初始化
for (int i = 1; i <= n; ++i) {
for (int j = 1; j < i; ++j) dis[i][j] = dis[j][i] = INF;
dis[i][i] = 0;
}
for (int i = 0; i < m; ++i) {
//有向边
int u = sc.nextInt(), v = sc.nextInt(), w = sc.nextInt();
dis[u][v] = w;
}
Floyd();
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j) {
if (dis[i][j] == INF) System.out.printf("%d不可达到%d\n", i, j);
else System.out.printf("%d到%d的最短路径长度为%d,最短路径为:%s\n", i, j, dis[i][j], findPath(i, j));
}
}
}
测试数据:
输入:
4 6
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4
输出:
1到1的最短路径长度为0,最短路径为:自身到自身
1到2的最短路径长度为2,最短路径为:1->2
1到3的最短路径长度为4,最短路径为:1->2->3
1到4的最短路径长度为3,最短路径为:1->2->4
2不可达到1
2到2的最短路径长度为0,最短路径为:自身到自身
2到3的最短路径长度为2,最短路径为:2->3
2到4的最短路径长度为1,最短路径为:2->4
3不可达到1
3不可达到2
3到3的最短路径长度为0,最短路径为:自身到自身
3到4的最短路径长度为3,最短路径为:3->4
4不可达到1
4不可达到2
4不可达到3
4到4的最短路径长度为0,最短路径为:自身到自身
例题
import java.util.Scanner;
public class Main {
static int[][] dis;
static int[] path;
static int n, m, ans = 0;
static void Floyd() {
for (int k = 1; k <= n; ++k)
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
if (dis[i][j] > dis[i][k] + dis[k][j])
dis[i][j] = dis[i][k] + dis[k][j];
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
//点的标号从1开始,即1~n
dis = new int[n + 1][n + 1];
path = new int[m + 1];
for (int i = 1; i <= m; ++i) path[i] = sc.nextInt();
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
dis[i][j] = sc.nextInt();
Floyd();
for (int i = 2; i <= m; ++i) ans += dis[path[i - 1]][path[i]];
System.out.println(ans);
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)