老规矩上题号。 1102 743 1129 1514 1631 787, 1654. 其中787和1654比较复杂,推荐最后一个练习。
还有个1334(这题是用floyd算法解决的,因为他不是单源最短,是多源最短,我认为过于textbook了这种题,背了floyd就是秒杀)
Leetcode上涉及Dijsktra算法变形的题还是不少的,我是很讨厌背算法的人,因为我发现不管我再怎么背,面试紧张起来我还是会忘掉,因为面试的时候我会下意识去相信我背的东西而不是我所理解的东西,万一背错了,那就跪了。你可能会问Dijkstra算法理解了不就是很小儿科的算法么,这点我同意。但是使用BFS这种更general的算法解决问题而不是通过特定的算法解决问题的方式,我个人比较喜欢吧,而且要背的话,BFS比Dijkstra更好背^_^。
这种单源最短的BFS基本框架几乎一模一样,一个记录数组,一个Queue做BFS(可以用PriorityQueue加快算法速度)。
先来个简单的1631做例子
class Step { //这个类用来记录当前的状态
int x;
int y;
int effort;
public Step(int _x, int _y, int _effort) {
x = _x;
y = _y;
effort = _effort;
}
}
class Solution {
public int minimumEffortPath(int[][] heights) {
int m = heights.length, n = heights[0].length;
int[][] miniEffort = new int[m][n]; //这里是我们的记录数组
for(int i = 0; i < m; i++) { //记录数组的初始化
for(int j = 0; j < n; j++) {
miniEffort[i][j] = Integer.MAX_VALUE;
}
}
miniEffort[0][0] = 0;
int[][] moves = new int[][]{{-1, 0}, {1, 0}, {0, 1}, {0, -1}};
PriorityQueue<Step> queue = new PriorityQueue<>((s1, s2) -> s1.effort - s2.effort); //这是我们BFS存储记录的Queue
queue.add(new Step(0, 0, 0));
while(queue.size() > 0) {
Step cur = queue.poll();
for(int[] move : moves) {
int nextx = cur.x + move[0], nexty = cur.y + move[1];
if (nextx >= 0 && nextx < m && nexty >= 0 && nexty < n) {
int diff = Math.abs(heights[nextx][nexty] - heights[cur.x][cur.y]);
int nextDiff = Math.max(diff, cur.effort);
if (nextDiff < miniEffort[nextx][nexty]) {
miniEffort[nextx][nexty] = nextDiff;
queue.add(new Step(nextx, nexty, nextDiff));
}
}
}
}
return miniEffort[m - 1][n - 1];
}
}
这道题里,它规定 A route's effort is the maximum absolute difference in heights between two consecutive cells of the route. 然后最终目标是最右边最底下那个点的最小effort。刚开始我想的是动态规划,然后发现东南西北4个方向都可以,那显然就不行了。于是开始搞套路,既然最后是单点结果,那么就用这个BFS方法咯。
1. 记录数组用来存储当前搜索情况下,到达每一个点的minimum effort(初始情况设置为正无穷)。
2. 那么到达一个点的状态,我们需要记录当前点的坐标,和不管如何到达的,到达当前点的minimum effort(选择路径不一样,这个effort就会不一样)。
3. 当我们开始走下一步的时候,对比走下一步所导致的minimum effort和记录数组中的minimum effort。如果比记录数组中的小,代表我们不知道怎样,找到了一个更好的路径,能够带来更好的收益,那么这条路径应该继续尝试下去。如果相同或者大于,我们可以保证,走这条路至少不会带来更好的收益,那么这条路就可以中断了,不用再试了(这也是我们避免重复搜索和走回原路最重要的一环)。同时,在我们继续尝试的时候,我们应该更新记录数组的值,因为我们找到了更好的路径,带来了更高的收益。
4. 考虑搜索的收敛性。因为我们加入搜索的判断条件,所以记录数组中的值永远只会越来越小,而最终的答案是确定的,所以当这个值和最终答案相同时,搜索过程终结,不会陷入无限循环。
这四步,你把general的idea提出来,跟着我列出的题号进行练习,很快就能掌握,我把这个BFS称为懒人做法,套路过于明显。使用PriorityQueue的原因是,让那些可能导致最优结果的路径优先被搜索,这样我们就可以避免搜索那些在逻辑上本来就不可能达到最优结果的路径。
下面Po出787的代码,这题是真挺难的,但是能够用我的方法解决代表你已经掌握了此类BFS的要点。
public int findCheapestPrice(int n, int[][] flights, int src, int dst, int K) {
if(src == dst) {
return 0;
}
HashMap<Integer, HashMap<Integer, Integer>> flightsInfo = new HashMap<>();
for(int[] flight : flights) {
if (flightsInfo.get(flight[0]) == null) {
flightsInfo.put(flight[0], new HashMap<>());
}
flightsInfo.get(flight[0]).put(flight[1], flight[2]);
}
if (K == 0) {
if (flightsInfo.get(src) != null) {
return flightsInfo.get(src).getOrDefault(dst, -1);
}
return -1;
}
int[][2] cheapest = new int[n][2];
PriorityQueue<int[]> queue = new PriorityQueue<>((t1, t2) -> t1[1] - t2[1]);
queue.add(new int[]{src, 0, 0});
int ret = Integer.MAX_VALUE;
while(queue.size() > 0) {
int[] cur = queue.poll();
int node = cur[0], cost = cur[1], stops = cur[2];
HashMap<Integer, Integer> neighbors = flightsInfo.get(node);
if (neighbors == null) {
continue;
}
if (cheapest[node][0] != 0 && node != src && cost > cheapest[node][0]) {
continue;
}
for(Map.Entry<Integer, Integer> neighbor : neighbors.entrySet()) {
int nextCity = neighbor.getKey(), nextCost = cost + neighbor.getValue();
int nextStops = stops + 1;
if (nextCity == dst) {
ret = Math.min(ret, nextCost);
continue;
}
if (nextStops > K) {
continue;
}
if (cheapest[nextCity][0]== 0 || nextCost < cheapest[nextCity][0] || nextCost == cheapest[nextCit]) {
queue.add(new int[]{nextCity, nextCost, nextStops});
cheapest[nextCity] = nextCost;
}
}
}
return ret == Integer.MAX_VALUE ? -1 : ret;
我这个方法,未必是最快的,但是我认为是最适合面试的,因为套路很明显,熟练了以后就是傻瓜式的照搬。而且时间复杂度分析起来特别容易,就是每一个点需要和记录数组中的值最多比较多少次 × 逻辑上点的数目。