前言
以leetcode 743. Netword Delay Time为例。该题是要求一个有向图中,从某一个点开始,到达所有点的最短路径中的最大值。
图的最短路算法
单源最短路
不带负权边:Dijkstra
思路:确定一个出发点,每一个时间步,从所有没有到达过的点中,选择一个与出发点距离最短的点(如果不存在,则结束),此距离就是出发点到该点的最短距离。把该点设置为已到达,并将该点作为中间点,更新出发点和剩余未到达点的最短距离。注意两点: 1. 为什么"此距离就是出发点到该点的最短距离",因为如果存在另一条路径,使得出发点到该点距离更短,那首先需要满足的是:出发点到下一个点的距离一定是小于这个最短距离的,那如果是小于的,当前时间步为什么不选择这个点呢? 2. 如果存在负边,Dijkstra不成立。
代码(100%, 21.91%):
class Solution {
int[][] graph;
int MAX_VALUE = Integer.MAX_VALUE / 2;
public int networkDelayTime(int[][] times, int n, int k) {
graph = new int[n+1][n+1];
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
graph[i][j] = i == j ? 0 : MAX_VALUE;
}
}
for(int[] edge:times){
int u = edge[0];
int v = edge[1];
int w = edge[2];
graph[u][v] = w;
}
int[] dis_lst = graph[k]; //这样graph本身也会被改变,不过不影响
boolean[] vis = new boolean[n+1];
vis[k] = true;
for(int i = 1; i < n; i++){
int min_dis = MAX_VALUE;
int v = -1;
for(int j = 1; j <= n; j++){
if(!vis[j] && dis_lst[j] < min_dis){
min_dis = dis_lst[j];
v = j;
}
}
if(v == -1){
break;
}
vis[v] = true;
for(int j = 1; j <= n; j++){
if(!vis[j]){
dis_lst[j] = Math.min(dis_lst[j], dis_lst[v] + graph[v][j]);
}
}
}
int ans = 0;
for(int dis:dis_lst){
ans = Math.max(ans, dis);
}
return ans == MAX_VALUE ? -1 : ans;
}
}
算法复杂度:
- 时间:O(N^2+E)
- 空间:O(N^2)
带负权边:Bellman-Ford\SPFA
Bellman-Ford:
思路:最多需要循环n-1次,每一次遍历所有边,进行更新。第i次的更新,得到的是:出发点最多经过i条边,到达各点的最短路径。
两种优化方式:
- 某一次没有发生更新,直接停
- 每一次只选择特定的边进行更新,即:如果上一次的一条边u->v更新成功,则这一次v的出边是需要考虑用来进行更新的。(SPFA)
代码:(Bellman-Ford)(89.76%,91.12%)
class Solution {
int MAX_VALUE = Integer.MAX_VALUE / 2;
public int networkDelayTime(int[][] times, int n, int k) {
/* 不需要建图,直接使用边即可
graph = new int[n+1][n+1];
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
graph[i][j] = i == j ? 0 : MAX_VALUE;
}
}
for(int[] edge:times){
int u = edge[0];
int v = edge[1];
int w = edge[2];
graph[u][v] = w;
}
*/
int[] dis_lst = new int[n+1];
Arrays.fill(dis_lst, MAX_VALUE);
dis_lst[k] = 0;
dis_lst[0] = 0; //否则出错
for(int x = 0; x < n; x ++){
int flag = 0;
for(int[] edge:times){
int u = edge[0];
int v = edge[1];
int w = edge[2];
if(dis_lst[v] > dis_lst[u] + w){
dis_lst[v] = dis_lst[u] + w;
flag = 1;
}
}
if(flag == 0){
// 优化1:没有更新,直接结束
break;
}
}
int ans = 0;
for(int dis:dis_lst){
ans = Math.max(ans, dis);
}
return ans == MAX_VALUE? -1 : ans;
}
}
算法复杂度:
- 时间:O(N*E)
- 空间:O(E)
SPFA:思路,在bellman-ford的基础上,每一次只考虑特定的边,即优化2。使用队列来维护。
代码(89.76%,52.70%):
class Solution {
int[][] graph;
int MAX_VALUE = Integer.MAX_VALUE / 2;
public int networkDelayTime(int[][] times, int n, int k) {
graph = new int[n+1][n+1];
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
graph[i][j] = i == j ? 0 : MAX_VALUE;
}
}
for(int[] edge:times){
int u = edge[0];
int v = edge[1];
int w = edge[2];
graph[u][v] = w;
}
int[] dis_lst = new int[n+1];
Arrays.fill(dis_lst, MAX_VALUE);
dis_lst[0] = dis_lst[k] = 0;
Queue<Integer> q= new LinkedList<Integer>();
q.offer(k);
while(!q.isEmpty()){
int now = q.poll();
//遍历该点出发的所有边,进行更新
for(int i = 1; i <= n; i ++){
if(dis_lst[i] > dis_lst[now] + graph[now][i]){
dis_lst[i] = dis_lst[now] + graph[now][i];
q.offer(i);
}
}
}
int ans = 0;
for(int dis:dis_lst){
ans = Math.max(ans, dis);
}
return ans == MAX_VALUE? -1 : ans;
}
}
算法复杂度:
不会
多源最短路:Floyd
思路:对于G中任意两个顶点v、w,遍历除这两点以外的所有其它点,假设以该点为中间点,更新v和w之间的最短距离。
代码:(58%, 15%)
/*
返回出发点,到达所有点的最短路径中的最大值
*/
class Solution {
int[][] graph;
int MAX_VALUE = Integer.MAX_VALUE / 2; //防止下面graph[i][x] + graph[x][j]的时候,数值超过上限,反而变小
public int networkDelayTime(int[][] times, int n, int k) {
//建图
graph = new int[n+1][n+1];
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
graph[i][j] = i == j ? 0 : MAX_VALUE;
}
}
for(int[] edge:times){
int u = edge[0];
int v = edge[1];
int w = edge[2];
graph[u][v] = w;
}
//floyd
for(int x = 1; x <= n; x++){
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
graph[i][j] = Math.min(graph[i][j], graph[i][x] + graph[x][j]);
}
}
}
//最大的最短路径
int ans = 0;
for(int dis:graph[k]){
//这里要注意graph[k][0]这个位置的值,千万不要初始化为MAX_VALUE
ans = Math.max(ans, dis);
}
return ans == MAX_VALUE ? -1 : ans;
}
}
算法复杂度:
- 时间:O(N^3)
- 空间:O(N^2 + E)