1631. 最小体力消耗路径

1631. 最小体力消耗路径

链接:https://leetcode-cn.com/problems/path-with-minimum-effort/

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

示例 2:

输入:heights = [[1,2,3],[3,8,4],[5,3,5]]
输出:1
解释:路径 [1,2,3,4,5] 的相邻格子差值绝对值最大为 1 ,比路径 [1,3,5,3,5] 更优。

示例 3:

输入: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]]
输出:0
解释:上图所示路径不需要消耗任何体力。

 

提示:

  • rows == heights.length
  • columns == heights[i].length
  • 1 <= rows, columns <= 100
  • 1 <= heights[i][j] <= 10^6

思路1:最朴素的做法当然就是遍历出每一条路径,找出最终路径。我们尝试做一个剪枝,如果说我们设定一个阈值上限,当查找路径时发现当前边权值大于上限,就可以直接剪去当前的遍历;当整个图中存在一条满足阈值要求的边时,就可以认为该阈值可以满足,我们的任务就变成了找到一条满足这个阈值的最小值。因为1 <= heights[i][j] <= 10^6,所以题目所求的阈值上限不会超过10^6,这就是直接搜索答案的算法。我们可以加一个二分优化,设阈值A>B,若存在一条路径,使得路径上所有的边的权值都满足<=A,自然也就可以满足<=B。路径的搜索可以用DFS或BFS,当然BFS的遍历次数更少,更推荐BFS。不知道会不会有读者有疑惑,因为BFS是一种层次扩展的方式,可能在失败的路径上存在我们需要使用的边,直接在BFS过程中剪枝会不会丢掉可能满足要求的边呢,我们看一个图便知:

在上图中,A-B为红边,表示超过的阈值不满足要求,但是从A->B可以走满足要求的A-C-D-B,也就是说,从B出发到终点(因为能到达B,说明前面的路径都满足要求了)的路径可以被考虑,自然也就不会漏掉任何一个可能路径了。

class Solution {
public:

    bool visited[110][110];
    typedef pair<int,int> Node;

    bool BFS(vector<vector<int>>& heights,int n,int m,int M){
        memset(visited,0,sizeof(visited));
        queue<pair<int,int>> Que;
        Node node = Node{0,0};
        visited[0][0] = true;
        int i, j;
        Que.push(node);
        while(!Que.empty()){
            node = Que.front();
            Que.pop();
            i = node.first, j = node.second;

            if(i == n - 1 && j == m - 1) return true;

            if(i > 0 && !visited[i - 1][j] && abs(heights[i - 1][j] - heights[i][j]) <= M ){
                visited[i - 1][j] = true;
                Que.push(Node{i - 1,j});
            }
            if(i + 1 < n && !visited[i + 1][j] && abs(heights[i + 1][j] - heights[i][j]) <= M ){
                visited[i + 1][j] = true;
                Que.push(Node{i + 1,j});
            }
            if(j > 0 && !visited[i][j - 1] && abs(heights[i][j - 1] - heights[i][j]) <= M ){
                visited[i][j - 1] = true;
                Que.push(Node{i,j - 1});
            }
            if(j + 1 < m && !visited[i][j + 1] && abs(heights[i][j + 1] - heights[i][j]) <= M ){
                visited[i][j + 1] = true;
                Que.push(Node{i,j + 1});
            }
        }
        return false;
    }

    int minimumEffortPath(vector<vector<int>>& heights) {
        int n = heights.size(), m = heights[0].size(), i, j, L = 0, R = 1000000, M, ans = -1;
        while(L <= R){
            M = (L + R) >> 1;
            if(BFS(heights, n, m, M)){
                ans = M;
                R = M - 1;
            }else{
                L = M + 1;
            }
        }
        return ans;
    }
};

思路2:贪心思路构图。题意要求路径上的权值尽可能地小,我们就尽可能地多用小边;将所有的边权按照大小排序,并依次加入并查集中,当边权为value的边加入并查集后,发现左上角与右下角连通,则value为答案(因为我们加入的边权是单调非减的)。构建边集合的时候,我们只需要从左上遍历到右下,考虑右和下的边即可,而不需要对每个点都考虑上下左右(防止重复运算)。

class Solution {
public:

    int Father[10010], Index[110][110];

    struct Edge{
        int u, v, w;
        bool operator < (const Edge edge) const {
            return w < edge.w;
        }
    };

    Edge edge[20010];

    int LocateToIndex(int x,int y,int n,int m){
        return x * m + y;
    }

    int Find(int x){
        if(x == Father[x]) return x;
        return Father[x] = Find(Father[x]);
    }

    void Union(int A,int B){
        A = Find(A);
        B = Find(B);
        if(A != B){
            Father[A] = B;
        }
    }

    int minimumEffortPath(vector<vector<int>>& heights) {
        memset(Father,0,sizeof(Father));
        int n = heights.size(), m = heights[0].size(), i, j, cnt = 0, s, o;
        for(i = 0; i < n; ++ i){
            for(j = 0; j < m; ++ j){
                Index[i][j] = LocateToIndex(i, j, n, m);
                Father[Index[i][j]] = Index[i][j];
            }
        }
        for(i = 0; i < n; ++ i){
            for(j = 0; j < m; ++ j){
                if(i + 1 < n) edge[cnt ++] = Edge{Index[i][j], Index[i + 1][j], abs(heights[i][j] - heights[i + 1][j])};
                if(j + 1 < m) edge[cnt ++] = Edge{Index[i][j], Index[i][j + 1], abs(heights[i][j] - heights[i][j + 1])};
            }
        }
        sort(edge, edge + cnt);
        s = 0, o = n * m - 1;
        for(i = 0; i < cnt; ++ i){
            Union(edge[i].u, edge[i].v);
            if(Find(s) == Find(o)) return edge[i].w;
        }
        return 0;
    }
};

思路3:其实本题是对最短路的定义进行的更改,最短路的长度定义为路径上的边的最大长度。那如果我们使用最短路算法呢?单源最短路的经典算法Dijkstra,维护一个从源点S到当前点v的最短路径长度数组dist[v],每次基于已有的最短路径对其他路径进行更新。Dijkstra基于这样一种想法,每次dist数组内部最小的值对应的点v(不考虑已经成为过中继点的点),从S到它的最短距离是确定为dist[v]。

i123456
dist[i]152634
visited[i]yesnoyesnoyesno

在如上表格中,可以确定源点到1、3、5,当前要称为中继点的点是5,2、4、6已经分别使用经过1、3考虑,现在需要考虑的是是否能够通过5使得他们的路径长度变小。

这一想法的成立基础是更新逻辑保持单点不减,也就是说,考虑更多的点,答案不会变小。而对于max操作,也是一样的,考虑更多的点,答案也不会变小,因此Dijkstra可以解决这个问题。我们使用带单调队列优化的Dijkstra:

class Solution {
public:

    struct Dist{
        int i, j, w; // (i,j):到达点 w:dist[v]
        bool operator < (const Dist dist) const{
            return w > dist.w;
        }
    };

    bool visited[110][110]; // 已经确定dist[v]的点

    int minimumEffortPath(vector<vector<int>>& heights) {
        memset(visited,0,sizeof(visited));
        int n = heights.size(), m = heights[0].size(), i, j, cnt = 0, w, v, o = n * m - 1, index;
        priority_queue<Dist> pri_que;
        Dist dist;
        pri_que.push(Dist{0, 0, 0});
        while(!pri_que.empty()){
            dist = pri_que.top();
            pri_que.pop();
            i = dist.i, j = dist.j, w = dist.w;
            visited[i][j] = true;

            if(i == n - 1 && j == m - 1) return w;

            if(i > 0 && !visited[i - 1][j]){
                pri_que.push(Dist{i - 1, j, max(w, abs(heights[i][j] - heights[i - 1][j]) )});
            }

            if(i + 1 < n && !visited[i + 1][j]){
                pri_que.push(Dist{i + 1, j, max(w, abs(heights[i][j] - heights[i + 1][j]) )});
            }

            if(j > 0 && !visited[i][j - 1]){
                pri_que.push(Dist{i, j - 1, max(w, abs(heights[i][j] - heights[i][j - 1]) )});
            }

            if(j + 1 < m && !visited[i][j + 1]){
                pri_que.push(Dist{i, j + 1, max(w, abs(heights[i][j] - heights[i][j + 1]) )});
            }

        }
        return 0;
    }
};

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值