题目
你准备参加一场远足活动。给你一个二维 rows x columns 的地图 heights ,其中 heights[row][col] 表示格子 (row, col) 的高度。一开始你在最左上角的格子 (0, 0) ,且你希望去最右下角的格子 (rows-1, columns-1) (注意下标从 0 开始编号)。你每次可以往 上,下,左,右 四个方向之一移动,你想要找到耗费 体力 最小的一条路径。
一条路径耗费的 体力值 是路径上相邻格子之间 高度差绝对值 的 最大值 决定的。
请你返回从左上角走到右下角的最小 体力消耗值 。
示例 1:
输入:heights = [[1,2,2],[3,8,2],[5,3,5]]
输出:2
解释:路径 [1,3,5,3,5] 连续格子的差值绝对值最大为 2 。
这条路径比路径 [1,2,2,2,5] 更优,因为另一条路径差值最大值为 3 。
回溯算法
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
Main main = new Main();
int [][] heights = {{1,2,1,1,1}, {1,2,1,2,1}, {1,2,1,2,1}, {1,2,1,2,1}, {1,1,1,2,1}};
main.minimumEffortPath(heights);
}
int m = 0;
int n = 0;
int [][] heights;
boolean [][] visited;
int min = Integer.MAX_VALUE;
public int minimumEffortPath(int[][] heights) {
this.m = heights.length;
this.n = heights[0].length;
this.heights = heights;
this.visited = new boolean[m][n];
visited[0][0] = Boolean.TRUE;
huisu(0, 0, heights[0][0], 0);
return min;
}
public void huisu(int row, int col, int preValue, int preMax) {
if (preMax > this.min) {
return;
}
// 到达最后一格
if (row == m - 1 && col == n - 1) {
int thisValue = heights[row][col];
int thisCost = Math.abs(thisValue - preValue);
preMax = Math.max(preMax, thisCost);
this.min = Math.min(preMax, min);
return;
}
// 选择列表
List<int []> optionList = getOptionList(row, col);
for (int [] option:optionList) {
visited[row][col] = Boolean.TRUE;
int thisValue = heights[row][col];
int thisCost = Math.abs(thisValue - preValue);
huisu(option[0], option[1], thisValue, Math.max(thisCost, preMax));
// 撤销选择
visited[row][col] = Boolean.FALSE;
}
}
public List<int []> getOptionList(int row, int col) {
List<int []> optionList = new ArrayList<>();
int [] rowOffset = {1, -1, 0, 0};
int [] colOffset = {0, 0, 1, -1};
for (int i =0; i < 4; i ++) {
int targetRow = row + rowOffset[i];
int targetCol = col + colOffset[i];
if (targetRow < 0 || targetRow >= m || targetCol < 0 || targetCol >= n || visited[targetRow][targetCol]) {
continue;
}
optionList.add(new int[] {targetRow, targetCol});
}
return optionList;
}
}
基本思路
-
基本的递归全遍历回溯算法, 每到一个格子计算当前格子和上一格子的差值, 和当前路径的的最大值进行比较, 保留最大值.
-
每条路径走到最后一个格子后, 比较和保留, 保留最小值, 当所有路径走完, 得到的最小值, 就是答案
缺点
-
当格子数量多了以后, 效率很差, 需要遍历完所有可能, 会超时
-
这种思路就是认为最小值我不知道, 然后去寻找那种哪种可能是最小值
二分查找法
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class Main {
public static void main(String[] args) {
Main main = new Main();
int [][] heights = {{1, 1000000}};
main.minimumEffortPath(heights);
}
public int minimumEffortPath(int[][] heights) {
int m = heights.length;
int n = heights[0].length;
int left = 0;
int right = 999999;
int result = 0;
int [] rowOffset = {1, -1, 0, 0};
int [] colOffset = {0, 0, 1, -1};
while (left <= right) {
int mid = (left + right) / 2;
Queue<int[]> queue = new LinkedList<int[]>();
queue.offer(new int[]{0, 0});
boolean [][] visited = new boolean[m][n];
visited[0][0] = Boolean.TRUE;
while (!queue.isEmpty()) {
// 利用二分查找法 从0-999999中找一个数字A, 使得在边权重小于A的情况下,能从图的左上走到右下
int [] positon = queue.poll();
int row = positon[0];
int col = positon[1];
for (int i = 0; i < 4; i ++) {
int targetRow = row + rowOffset[i];
int targetCol = col + colOffset[i];
if (targetRow >= 0 && targetRow < m && targetCol >= 0 && targetCol < n && !visited[targetRow][targetCol] && Math.abs(heights[targetRow][targetCol] - heights[row][col]) <= mid) {
queue.offer(new int [] {targetRow, targetCol});
visited[targetRow][targetCol] = Boolean.TRUE;
}
}
}
if (visited[m - 1][n - 1]) {
result = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
return result;
}
}
基本思路
-
因为元素差值是有最大值限制的, 所以我可以假设两个相邻元素的最大值为999999, 然后去数组中只要找到一条路径(这里选用的BFS去探索路径), 能够从左上角到达右下角, 就说明, 相邻元素差值的最大值是小于等于999999的, 然后就改变目标值为499999(根据二分法加速选值的过程), 如果发现仍然可以找到一条路径, 继续缩小目标值, 直到某个目标值例如10, 发现无法在满足目标值的情况下, 从左上角到达右下角, 说明目标值比10大, 应该增大目标值, 最后会发现一个最小的目标值, 就是题目答案
-
外层二分查找法负责加速选择目标值, 内层的BFS负责在当前目标值的情况下, 探索一条左上到右下的路径, 根据探索成功与否调整二分查找的左右端点.
优点
-
已知目标值的情况下, 只需探索一条路径即可结束, 大大减少了探索的复杂度
-
无需考虑走的具体是哪条路径, 只要经过的格子是能够连通到重点即可
-
已知目标值之后, 遇见大于目标值的情况就可以直接排除