最小体力消耗路径

**题意:**给定一个整形二维数组,求从左上角到右下角的最小路径。这里的路径指的是所有路径中相邻元素差的最小值,而对于单条路径代表的结果值指的是该条路径上相邻元素之间差的最大值。

输入:heights = [[1,2,2],[3,8,2],[5,3,5]]
输出:2
解释:路径 [1,3,5,3,5] 连续格子的差值绝对值最大为 2 。
这条路径比路径 [1,2,2,2,5] 更优,因为另一条路径差值最大值为 3 。

来源:力扣(LeetCode)
链接:leetcodehttps://leetcode-cn.com/problems/path-with-minimum-effort
对于本题目的做法比较多,最容易想到的是直接进行深度优先搜索或者宽度优先搜索从(0, 0)位置到(m-1, n-1)位置的所有路径,并得出结果。但是这样做会超时。
深度优先搜索如下所示:

public class Main {
     
	//这里实际上可以使用一个数组就可以表示了
	int[] a = new int[]{
     0, 0, -1, 1};
	int[] b = new int[]{
     -1, 1, 0, 0};
	int res = Integer.MAX_VALUE;
	int curRes = Integer.MIN_VALUE;
	public int minimumEffortPath(int[][] heights) {
     
		if(heights == null || heights.length == 0){
     
			return 0;
		}
		int m = heights.length;
		int n = heights[0].length;
		if(m == 1 && n == 1){
     
			return 0;
		}
		boolean[][] vis = new boolean[m][n];
		dfs(0, 0, heights, vis);
		return res;
	}
	public void dfs(int x, int y, int[][] heights, boolean[][] vis){
     
		if(x == heights.length - 1 && y == heights[0].length - 1){
     
			res = Math.min(res, curRes);
		}else{
     
			for(int i = 0; i < 4; i++){
     
				int x1 = x + b[i];
				int y1 = y + a[i];
				if(tryPos(x1, y1, vis)){
     
					vis[x1][y1] = true;
					int cur = curRes;
					curRes = Math.max(curRes, Math.abs(heights[x1][y1] - heights[x][y]));
					dfs(x1, y1, heights, vis);
					curRes = cur;
					vis[x1][y1] = false;
				}
			}
		}
	}
	public boolean tryPos(int x, int y, boolean[][] vis){
     
		if(x < 0 || x >= vis.length || y < 0 || y >= vis[0].length){
     
			return false;
		}
		if(vis[x][y]){
     
			return false;
		}
		return true;
	}

    public static void main(String[] args) {
     
		int[][] heights = new int[][]{
     {
     4,3,4,10,5,5,9,2},{
     10,8,2,10,9,7,5,6},{
     5,8,10,10,10,7,4,2},{
     5,1,3,1,1,3,1,9},{
     6,4,10,6,10,9,4,6}};
		Main main = new Main();
		int res = main.minimumEffortPath(heights);
		System.out.println(res);
    }
}

下面介绍另外一种方法,话不多说了,尽在代码中:

import java.util.*;

public class UnionFindExample1 {
     
    public int minimumEffortPath(int[][] heights) {
     
        //算法思想是:将相邻元素之间的差值的绝对值作为它们之间的边的权值
        //由于当前是用二维数组进行存储,节点所在的位置为(i, j),那么我们
        //如何存储表示节点的编号呢?可以采用i×n+j的方式表示,其中n为列的长度
        //当然也可以使用j×m+i的方式表示,其中m是行的长度。
        if(heights == null || heights.length == 0){
     
            return 0;
        }
        int m = heights.length;
        int n = heights[0].length;
        if(m == 1 && n == 1){
     
            return 0;
        }
        List<int[]> edges = new ArrayList<>();
        for(int i = 0; i < m; i++){
     
            for(int j = 0; j < n; j++){
     
                int id = i * n + j;//节点编号计算
                if(i > 0){
     
                    //表示的上下顶点之间的边
                    edges.add(new int[]{
     id - n, id, Math.abs(heights[i][j] - heights[i-1][j])});
                }
                if(j > 0){
     
                    //表示左右顶点之间的边
                    edges.add(new int[]{
     id - 1, id, Math.abs(heights[i][j] - heights[i][j-1])});
                }
            }
        }
        Collections.sort(edges, new Comparator<int[]>(){
     
            public int compare(int[] edge1, int[] edge2){
     
                return edge1[2] - edge2[2];//表示以边的权值升序排序
            }
        });
        UnionFind uf = new UnionFind(m * n);
        int res = 0;
        for(int[] edge : edges){
     
            uf.union(edge[0], edge[1]);
            if(uf.connected(0, m*n - 1)){
     
                res = edge[2];
                break;
            }
        }
        return res;
    }
    private class UnionFind{
     
        int size[];//元素所属的连通块的大小,如果不定义的话,在合并的时候不知道让哪个连通块作为主导
        int parent[];//元素的父结点
        int setcount;
        public UnionFind(int n){
     
            size = new int[n];
            parent = new int[n];
            Arrays.fill(size, 1);
            for(int i = 0; i < n; i++) parent[i] = i;
            setcount = n;//初始化连通块的数量
        }
        private boolean connected(int x, int y){
     //判断两个连通块是否是相通的
            return findSet(x) == findSet(y);
        }
        private void union(int x, int y){
     //合并x和y所在的连通块
            x = findSet(x);
            y = findSet(y);
            if(x == y) return ;
            if(size[x] < size[y]){
     
                int temp = x;
                x = y;
                y = temp;
            }
            parent[y] = x;//将小的连通块合并到大的连通块
            --setcount;//连通块的数量减少一个
            size[x] += size[y];//改变大的连通块中的元素数量
        }
        private int findSet(int x){
     
            //如果当前元素的所属的连通块的节点编号就是它本身,直接返回就可以
            //如果不是,则需要层层向上去寻找它所在的连通块的编号
            //貌似使用parent[x] = findSet(parent[x])的好处是把所有属于同一个连通块的元素的x的paret[x]值
            //赋值为其所在的连通块的编号
            return parent[x] == x ? x : (parent[x] = findSet(parent[x]));
        }
    }

    public static void main(String[] args) {
     
        int[][] heights = new int[][]{
     {
     4,3,4,10,5,5,9,2},{
     10,8,2,10,9,7,5,6},{
     5,8,10,10,10,7,4,2},{
     5,1,3,1,1,3,1,9},{
     6,4,10,6,10,9,4,6}};
        UnionFindExample1 s = new UnionFindExample1();
        int res = s.minimumEffortPath(heights);
        System.out.println(res);
    }
}

该题还可以使用二分方法和Dijkstra方法。其中二分法是二分数组元素的值,对于每次二分给出的mid,要求所求的路径上相邻元素的绝对差值最大为当前的mid,如果找到一条路径,则right = mid - 1,继续下去直到二分结束。 后一种方法指的是对于「最短路径」的定义不是其经过的所有边权的和,而是其经过的所有边权(相邻元素的绝对插值)的最大值
Dijkstra方法代码如下:(来源于leetcode)

class Solution {
     
    int[][] dirs = {
     {
     -1, 0}, {
     1, 0}, {
     0, -1}, {
     0, 1}};

    public int minimumEffortPath(int[][] heights) {
     
        int m = heights.length;
        int n = heights[0].length;
        PriorityQueue<int[]> pq = new PriorityQueue<int[]>(new Comparator<int[]>() {
     
            public int compare(int[] edge1, int[] edge2) {
     
                return edge1[2] - edge2[2];
            }
        });
        pq.offer(new int[]{
     0, 0, 0});

        int[] dist = new int[m * n];
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[0] = 0;
        boolean[] seen = new boolean[m * n];

        while (!pq.isEmpty()) {
     
            int[] edge = pq.poll();
            int x = edge[0], y = edge[1], d = edge[2];
            int id = x * n + y;
            if (seen[id]) {
     
                continue;
            }
            if (x == m - 1 && y == n - 1) {
     
                break;
            }
            seen[id] = true;
            for (int i = 0; i < 4; ++i) {
     
                int nx = x + dirs[i][0];
                int ny = y + dirs[i][1];
                if (nx >= 0 && nx < m && ny >= 0 && ny < n && Math.max(d, Math.abs(heights[x][y] - heights[nx][ny])) < dist[nx * n + ny]) {
     
                    dist[nx * n + ny] = Math.max(d, Math.abs(heights[x][y] - heights[nx][ny]));
                    pq.offer(new int[]{
     nx, ny, dist[nx * n + ny]});
                }
            }
        }
        
        return dist[m * n - 1];
    }
}

你可能感兴趣的:(最小体力消耗路径)