题目地址:
https://www.lintcode.com/problem/cheapest-flights-within-k-stops/description
给定一个非负权有向图,再给定一个源点 s s s,一个目标点 d d d,和一个整数 K K K,问从 s s s到 d d d中转站个数(其实就是路径去掉起点和终点后剩余的点数)不超过 K K K的最短路径长度。若不存在则返回 − 1 -1 −1。
法1:Bellman-Ford算法。一共更新 K + 1 K+1 K+1轮,每轮都对每条边进行松弛操作。具体代码实现方面要注意,在进行第 i i i轮松弛的时候,需要用第 i − 1 i-1 i−1轮松弛的结果,而不能实时更新,否则会得到错误答案。代码如下:
import java.util.*;
public class Solution {
/**
* @param n: a integer
* @param flights: a 2D array
* @param src: a integer
* @param dst: a integer
* @param K: a integer
* @return: return a integer
*/
public int findCheapestPrice(int n, int[][] flights, int src, int dst, int K) {
// write your code here
if (flights == null || flights.length == 0 || flights[0].length == 0) {
return 0;
}
// 用邻接表建图。key是边的出发点,value的下标0存边的到达点,下标1存边权
Map<Integer, List<int[]>> graph = buildGraph(flights);
// dis存编号为i的点到源点的最短路距离,先初始化为正无穷。源点自己初始化为0
int[] dis = new int[n];
Arrays.fill(dis, Integer.MAX_VALUE);
dis[src] = 0;
// pass表示更新的轮数。当一遍循环结束时,就找到了边的数量不超过pass的最短路的长度;
// K是中转站个数,也就等于边数减1
for (int pass = 1; pass <= K + 1; pass++) {
// 这里为了防止在某次相同循环里出现用更新过的dis再去更新别的dis的错误情况,需要对前一轮pass的dis做备份
int[] backup = Arrays.copyOf(dis, dis.length);
// 枚举图的每个点
for (int i = 0; i < n; i++) {
// 更新其出边
if (graph.containsKey(i)) {
for (int[] next : graph.get(i)) {
int nextPoint = next[0], disFromI = next[1];
// 注意,当某个点的dis是正无穷的时候,其出边是不能更新的
if (backup[i] != Integer.MAX_VALUE) {
dis[nextPoint] = Math.min(dis[nextPoint], backup[i] + disFromI);
}
}
}
}
}
// 如果dis是正无穷说明到不了,返回-1;否则返回那个距离
return dis[dst] == Integer.MAX_VALUE ? -1 : dis[dst];
}
private Map<Integer, List<int[]>> buildGraph(int[][] g) {
Map<Integer, List<int[]>> graph = new HashMap<>();
for (int[] tuple : g) {
graph.putIfAbsent(tuple[0], new ArrayList<>());
graph.get(tuple[0]).add(new int[]{tuple[1], tuple[2]});
}
return graph;
}
}
时间复杂度 O ( K E ) O(KE) O(KE),空间 O ( V + E ) O(V+E) O(V+E)。
法2:SPFA。在Bellman-Ford算法基础上进行优化,用一个队列,每次只有当松弛操作会产生更优路线的时候才会将被松弛的边的那个到达点加入队列中,进行下面的优化。同时,开一个数组记录源点到其余顶点的最短路的长度,在每次松弛的时候,都更新之;而一旦发现某个点更新完后其最短路长度已经达到了 K + 1 K+1 K+1了,也不能将其加入队列,因为用其更新别的点的最短路只会产生路径长度更长的最短路,这是不符合要求的。代码如下:
import java.util.*;
public class Solution2 {
/**
* @param n: a integer
* @param flights: a 2D array
* @param src: a integer
* @param dst: a integer
* @param K: a integer
* @return: return a integer
*/
public int findCheapestPrice(int n, int[][] flights, int src, int dst, int K) {
// write your code here
if (flights == null || flights.length == 0 || flights[0].length == 0) {
return 0;
}
Map<Integer, List<int[]>> graph = buildGraph(flights);
int[] dis = new int[n];
Arrays.fill(dis, Integer.MAX_VALUE);
dis[src] = 0;
// 记录当前算出的到该顶点的最短路的边的数量
int[] count = new int[n];
// 记录当前顶点是否已经存在于队列
boolean[] state = new boolean[n];
Queue<Integer> queue = new LinkedList<>();
queue.offer(src);
while (!queue.isEmpty()) {
int cur = queue.poll();
state[cur] = false;
if (graph.containsKey(cur)) {
for (int[] next : graph.get(cur)) {
int nextPoint = next[0], disFromCur = next[1];
if (dis[nextPoint] > dis[cur] + disFromCur) {
dis[nextPoint] = dis[cur] + disFromCur;
count[nextPoint] = count[cur] + 1;
// 判断一下nextPoint的最短路的边的数量是小于等于K的,如果不满足也不能加入队列
if (!state[nextPoint] && count[nextPoint] <= K) {
queue.offer(nextPoint);
state[nextPoint] = true;
}
}
}
}
}
return dis[dst] == Integer.MAX_VALUE ? -1 : dis[dst];
}
private Map<Integer, List<int[]>> buildGraph(int[][] g) {
Map<Integer, List<int[]>> graph = new HashMap<>();
for (int[] tuple : g) {
graph.putIfAbsent(tuple[0], new ArrayList<>());
graph.get(tuple[0]).add(new int[]{tuple[1], tuple[2]});
}
return graph;
}
}
时间复杂度 O ( K E ) O(KE) O(KE),空间 O ( V + E ) O(V+E) O(V+E),虽然最差情况一样,但在大多数情况下还是要比Bellman-Ford算法快很多。