最小体力消耗

147 阅读4分钟

题目

你准备参加一场远足活动。给你一个二维 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;

    }

}

基本思路

  1. 基本的递归全遍历回溯算法, 每到一个格子计算当前格子和上一格子的差值, 和当前路径的的最大值进行比较, 保留最大值.

  2. 每条路径走到最后一个格子后, 比较和保留, 保留最小值, 当所有路径走完, 得到的最小值, 就是答案

缺点

  1. 当格子数量多了以后, 效率很差, 需要遍历完所有可能, 会超时

  2. 这种思路就是认为最小值我不知道, 然后去寻找那种哪种可能是最小值

二分查找法

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;

    }

}

基本思路

  1. 因为元素差值是有最大值限制的, 所以我可以假设两个相邻元素的最大值为999999, 然后去数组中只要找到一条路径(这里选用的BFS去探索路径), 能够从左上角到达右下角, 就说明, 相邻元素差值的最大值是小于等于999999的, 然后就改变目标值为499999(根据二分法加速选值的过程), 如果发现仍然可以找到一条路径, 继续缩小目标值, 直到某个目标值例如10, 发现无法在满足目标值的情况下, 从左上角到达右下角, 说明目标值比10大, 应该增大目标值, 最后会发现一个最小的目标值, 就是题目答案

  2. 外层二分查找法负责加速选择目标值, 内层的BFS负责在当前目标值的情况下, 探索一条左上到右下的路径, 根据探索成功与否调整二分查找的左右端点.

优点

  1. 已知目标值的情况下, 只需探索一条路径即可结束, 大大减少了探索的复杂度

  2. 无需考虑走的具体是哪条路径, 只要经过的格子是能够连通到重点即可

  3. 已知目标值之后, 遇见大于目标值的情况就可以直接排除