LeetCode 1631. Path With Minimum Effort【最小瓶颈路;二分+BFS或DFS;计数排序+并查集;最小生成树】1947

news2024/11/23 11:56:53

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

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

同类题目:

  • LeetCode 2812. Find the Safest Path in a Grid
  • LeetCode 778. Swim in Rising Water
  • LeetCode 1631. 最小体力消耗路径

这几道题,虽然物理问题不同,但是背后的数学模型几乎完全相同,唯一的区别在于:

  • 第 1631 题将相邻格子的高度差作为边的权重,是找最小化的 相邻格子最大绝对高度差。
  • 第 778 题将相邻两格子间高度的最大值作为边的权重,是找最小化的 最大高度
  • 第 2812 题将当前方格到最近小偷的距离作为边的权重,是找最大化的 最小距离

无向图 G G G x x x y y y最小瓶颈路是这样的一类简单路径,满足这条路径上的最大边权在所有 x x x y y y 的简单路径中是最小的。
根据最小生成树定义, x x x y y y 的最小瓶颈路上的最大边权等于最小生成树上 x x x y y y 路径上的最大边权。虽然最小生成树不唯一,但是每种最小生成树 x x x y y y 路径的最大边权相同且为最小值。也就是说,每种最小生成树上的 x x x y y y 的路径均为最小瓶颈路。

无向图 G G G x x x y y y最大瓶颈路是这样的一类简单路径,满足这条路径上的最小边权在所有 x x x y y y 的简单路径中是最大的。
根据最小生成树定义, x x x y y y 的最大瓶颈路上的最小边权等于最大生成树上 x x x y y y 路径上的最小边权。虽然最大生成树不唯一,但是每种最大生成树 x x x y y y 路径的最小边权相同且为最大值。也就是说,每种最大生成树上的 x x x y y y 的路径均为最大瓶颈路。

我们不必实际求出整个最小/大生成树,只需要求出最小/大生成树上 x x x y y y 路径上的最大/小边权

在看清楚物理问题背后的数学模型后,这几道题都会迎刃而解。

解法1 二分+BFS/DFS

这种做法是LeetCode 2812. Find the Safest Path in a Grid的完全简化版、在2812中需要先进行一次多源BFS、再来二分答案,也用在LeetCode 1631. 最小体力消耗路径中。

根据题目中的描述,给定了 h e i g h t s [ i ] [ j ] heights[i][j] heights[i][j] 的范围是 [ 1 , 1 0 6 ] [1, 10^6 ] [1,106] ,所以答案(绝对高度差)必然落在范围 [ 0 , 1 0 6 ] [0, 10^6] [0,106]

  • 如果允许消耗的体力值 h h h 越低,网格上可以越过的部分就越少,存在从左上角到右下角的一条路径的可能性越小
  • 如果允许消耗的体力值 h h h 越高,网格上可以游泳的部分就越多,存在从左上角到右下角的一条路径的可能性越大。

这是本问题具有的 单调性 。因此可以使用二分查找定位到最小体力消耗值。

假设最优解为 l l l 的话(恰好能到达右下角的体力消耗),那么小于 l l l 的体力消耗无法到达右下角,大于 l l l 的体力消耗能到达右下角,因此在以最优解 l l l 为分割点的数轴上具有二段性,可以通过「二分」找到分割点 l l l

注意:「二分」的本质是两段性,并非单调性。只要一段满足某个性质,另外一段不满足某个性质,就可以用「二分」。其中 33. 搜索旋转排序数组 是一个很好的说明例子。

具体来说:在区间 [ 0 , 1 e 6 ] [0,1e6] [0,1e6] 里猜一个整数,针对这个整数从起点(左上角)开始做一次DFS或BFS:

  • 当小于等于该数值时,如果存在一条从左上角到右下角的路径,说明答案可能是这个数值,也可能更小
  • 当小于等于该数值时,如果不存在一条从左上角到右下角的路径,说明答案一定比这个数值更大。
  • 按照这种方式不断缩小搜索的区间,最终找到最小体力消耗。
class Solution {
public:
    int minimumEffortPath(vector<vector<int>>& heights) {
        int n = heights.size(), m = heights[0].size();
        typedef pair<int, int> pii;
        int d[4][2] = {-1, 0, 1, 0, 0, -1, 0, 1};
        auto check = [&](int lim) {
            bool vis[n][m]; memset(vis, false, sizeof(vis));
            queue<pii> q;
            q.push(pii(0, 0)); vis[0][0] = true;
            while (!q.empty()) {
                pii p = q.front(); q.pop();
                int i = p.first, j = p.second;
                if (i == n - 1 && j == m - 1) return true;
                for (int k = 0; k < 4; ++k) {
                    int x = i + d[k][0], y = j + d[k][1];
                    if (x < 0 || x >= n || y < 0 || y >= m || 
                        abs(heights[x][y] - heights[i][j]) > lim || vis[x][y])
                        continue;
                    q.push(pii(x, y));
                    vis[x][y] = true;
                }
            }
            return vis[n - 1][m - 1];
        };
        int l = 1, r = 1e6 + 1;
        while (l < r) {
            int mid = (l + r) >> 1;
            if (check(mid)) r = mid;
            else l = mid + 1;
        }
        return l;
    }
};

复杂度分析:

  • 时间复杂度: O ( M N log ⁡ C ) O(MN \log C) O(MNlogC) 。 其中 N , M N,M N,M 是方格的边长。最差情况下进行 log ⁡ C   ( C = 1 e 6 ) \log C\ (C = 1e6) logC (C=1e6) 次二分查找,每一次二分查找最差情况下要遍历所有单元格一次,时间复杂度为 O ( M N ) O(MN) O(MN)
  • 空间复杂度: O ( M N ) O(MN) O(MN) 。 数组 v i s vis vis 的大小为 M N MN MN ,如果使用深度优先遍历,须要使用的栈的大小最多为 M N MN MN ,如果使用广度优先遍历,须要使用的队列的大小最多为 M N MN MN

解法2 最小瓶颈路模型+最小生成树(Prim+堆/Kruskal+边集数组)

根据题意,我们要找的是从左上角到右下角的最优路径,其中最优路径是指途径的边的最大权重值最小,然后输出最优路径中的最大权重值。此处的边权为途径节点间高度差绝对值

根据最小瓶颈路模型,我们可以使用Kruskal最小生成树算法:

  1. 先遍历所有的点,将所有的边加入集合,存储的格式为数组 [ a , b , w ] [a, b, w] [a,b,w] ,代表编号为 a a a 的点和编号为 b b b 的点之间的权重为 w w w(按照题意, w w w 为两者的高度绝对差)。
  2. 对边集集合进行排序,按照 w w w 进行从小到达排序
  3. 当我们有了所有排好序的候选边集合之后,对边从前往后处理,选择那些不会出现环的边加入最小生成树中。
  4. 每次加入一条边之后,使用并查集来查询左上角点和右下角点是否连通。如果连通,那么该边的权重即是答案
class Solution {
    private class UnionFind {
        private int[] parent;
        public UnionFind(int n) {
            parent = new int[n];
            for (int i = 0; i < n; ++i) parent[i] = i;
        }
        public int find(int x) {
            return x != parent[x] ? parent[x] = find(parent[x]) : x;
        }
        public boolean isConnected(int x, int y) { return find(x) == find(y); }
        public void union(int p, int q) {
            int rp = find(p), rq = find(q);
            if (rp == rq) return;
            parent[rp] = rq;
        }
    }
    public int minimumEffortPath(int[][] grid) {
        int n = grid.length, m = grid[0].length;
        int len = n * m;
        int[][] d = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
        UnionFind unionFind = new UnionFind(len);

        // 预处理所有无向边
        // edge存[a,b,w]: 代表从a到b所需的时间点为w
        // 虽然我们可以往四个方向移动,但只要对于每个点都添加「向右」和「向下」
        // 两条边的话,其实就已经覆盖了所有边
        List<int[]> edges = new ArrayList<>();
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < m; ++j) {
                int index = i * m + j;
                int a, b, w;
                if (i + 1 < n) {
                    a = index; b = (i + 1) * m + j;
                    w = Math.abs(grid[i][j] - grid[i + 1][j]);
                    edges.add(new int[]{a, b, w});
                }
                if (j + 1 < m) {
                    a = index; b = i * m + (j + 1);
                    w = Math.abs(grid[i][j] - grid[i][j + 1]);
                    edges.add(new int[]{a, b, w});
                }   
            }
        }
        Collections.sort(edges, (a, b) -> a[2] - b[2]); // 根据w权值升序
        // 从「小边」开始添加,当某一条边应用之后,恰好使得「起点」和「结点」联通
        // 那么代表找到了「最短路径」中的「权重最大的边」
        int start = 0, end = len - 1;
        for (int[] edge : edges) {
            int a = edge[0], b = edge[1], w = edge[2];
            unionFind.union(a, b);
            if (unionFind.isConnected(start, end)) return w;
        }
        return 0;
    }
}

Kruskal虽然没有求出完整最小生成树,但是对所有边进行了排序。我们可以使用Prim算法+优先队列。

下面将此问题抽象为一张带权有向图,每个单元格为顶点,有指向相邻顶点的有向边,边的权值为指向顶点的高度绝对差。然后用Prim算法,求出最小生成树上左上角的点到右下角的点的路径上的最大边权值。此时不用对所有边进行排序。

class Solution {
    public int minimumEffortPath(int[][] grid) {
        int n = grid.length, m = grid[0].length;

        boolean[][] vis = new boolean[n][m];
        // 必须将 先前加入队列时的边权重 加入int[]中
        Queue<int[]> minHeap = new PriorityQueue<>((a, b) -> a[2] - b[2]);
        minHeap.offer(new int[]{0, 0, 0});
        int[][] dist = new int[n][m];
        for (int[] row : dist)
            Arrays.fill(row, Integer.MAX_VALUE);
        dist[0][0] = 0;

        int[][] directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
        while (!minHeap.isEmpty()) { // 找最短的边
            int[] front = minHeap.poll();
            int x = front[0], y = front[1], d = front[2];
            if (vis[x][y]) continue;
            vis[x][y] = true;
            if (x == n - 1 && y == m - 1) break;
            // 更新
            for (int i = 0; i < 4; ++i) {
                int u = x + directions[i][0], v = y + directions[i][1];
                if (u >= 0 && u < n && v >= 0 && v < m 
                    && !vis[u][v] && // prim算法
                    Math.max(d, Math.abs(grid[x][y] - grid[u][v]))
                        <= dist[u][v]) {
                        dist[u][v] = Math.max(d,
                            Math.abs(grid[x][y] - grid[u][v]));
                        minHeap.offer(new int[]{u, v, dist[u][v]});
                    }
            }
        }
        return dist[n - 1][m - 1];
    }
}

复杂度分析:

  • 时间复杂度: O ( M N log ⁡ M N ) O(MN \log MN) O(MNlogMN) 。 使用了优先队列的 Prim 算法的时间复杂度是 O ( E log ⁡ E ) O(E \log E) O(ElogE) ,这里 E E E 是边数,至多是顶点数的 4 4 4 倍,顶点数为 M N MN MN ,因此 O ( E log ⁡ E ) = O ( 4 M N log ⁡ M N ) = O ( M N log ⁡ M N ) O(E \log E) = O(4MN \log MN) = O(MN \log MN) O(ElogE)=O(4MNlogMN)=O(MNlogMN)
  • 空间复杂度: O ( M N ) O(MN) O(MN) 。 数组 v i s vis vis d i s t dist dist 的大小为 O ( M N ) O(MN) O(MN) ,优先队列中保存的边的总数也是 M N MN MN 级别的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/871420.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

音画欣赏|《亲亲湖边水》

《亲亲湖边水》60x50cm陈可之1988年绘 贝加尔湖畔 【李健】 在我的怀里 在你的眼里 那里春风沉醉 那里绿草如茵 月光把爱恋 洒满了湖面 两个人的篝火 照亮整个夜晚 多少年以后 如云般游走 那变换的脚步 让我们难牵手 这一生一世 有多少你我 被吞没在月光如水的夜里 多想某…

「何」到底该读「なん」还是「なに」?柯桥学日语

「何」到底该读「なん」还是「なに」&#xff1f; 首先&#xff0c;讲一个规律&#xff0c;大家记住就行。当「何」后面所接单词的第一个发音在“た”、“だ”、“な”行时&#xff0c;读作“なん”。一般这种情况下&#xff0c;后面跟的是の、でも、です和だ。 用例&#xff…

Ubuntu安装bfloat16==1.1出现问题 error: subprocess-exited-with-error

报错 error: subprocess-exited-with-error python setup.py bdist_wheel did not run successfully. 解决方法 确保你的系统上已经安装了 C/C 编译器&#xff08;如 gcc、g&#xff09;。 如果你使用的是 Linux 系统&#xff0c;你可以使用包管理器来安装它们。命令如下 u…

华为OD机试真题 Java 实现【数组去重和排序】【2023 B卷 100分】

目录 专栏导读一、题目描述二、输入描述三、输出描述四、解题思路五、Java算法源码六、效果展示1、输入2、输出3、说明 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷&#…

详细介绍如何对音乐信息进行检索和音频节拍跟踪

在本文中,我们将了解节拍的概念,以及我们在尝试跟踪节拍时面临的挑战。然后我们将介绍解决问题的方法以及业界最先进的解决方案。 介绍 音乐就在我们身边。每当我们听到任何与我们的心灵和思想相关的音乐时,我们就会迷失其中。我们下意识地随着听到的节拍而敲击。您一定已…

SpringBoot 异步、邮件任务

异步任务 创建一个Hello项目 创建一个类AsyncService 异步处理还是非常常用的&#xff0c;比如我们在网站上发送邮件&#xff0c;后台会去发送邮件&#xff0c;此时前台会造成响应不动&#xff0c;直到邮件发送完毕&#xff0c;响应才会成功&#xff0c;所以我们一般会采用多线…

线程池,以及线程池的实现以及面试常问的问题,工厂模式,常见的锁策略(面试常考,要了解,不行就背)

一、&#x1f49b; 线程池的基本介绍 内存池&#xff0c;进程池&#xff0c;连接池&#xff0c;常量池&#xff0c;这些池子概念上都是一样的&#xff5e;&#xff5e; 如果我们需要频繁的创建销毁线程&#xff0c;此时创建销毁的成本就不能忽视了&#xff0c;因此就可以使用线…

数据结构刷题训练:设计循环队列(力扣OJ)

目录 文章目录 前言 1. 题目&#xff1a;设计循环队列 2. 思路 3. 分析 3.1 定义循环队列 3.2 创建队列 3.3 判空和判满 3.4 入队 3.5 出队 3.6 取队头队尾数据 3.7 销毁队列 4. 题解 总结 前言 当谈到队列数据结构时&#xff0c;很多人可能会想到普通的队列&#xff0c;即先进…

拷贝对象时的一些编译器优化

在传参和传返回值的过程中&#xff0c;一般编译器会做一些优化&#xff0c;减少对象的拷贝&#xff0c;这个在一些场景下还是非常有用的

Linux之awk判断和循环

echo zhaoy 70 72 74 76 74 72 >> score.txt echo wangl 70 81 84 82 90 88 >> score.txt echo qiane 60 62 64 66 65 62 >> score.txt echo sunw 80 83 84 85 84 85 >> score.txt echo lixi 96 80 90 95 89 87 >> score.txt把下边的内容写入到s…

FL Studio for Windows-21.1.0.3713中文直装版功能介绍及系统配置要求

FL Studio 21简称FL水果软件,全称是&#xff1a;Fruity Loops Studio编曲&#xff0c;由于其Logo长的比较像一款水果因此&#xff0c;在大家更多的是喜欢称他为水果萝卜&#xff0c;FL studio21是目前最新的版本&#xff0c;这是一款可以让你的计算机就像是一个全功能的录音室&…

SpringBoot复习(39)Servlet容器的自动配置原理

Servlet容器自动配置类为ServletWebServerFactoryAutoConfiguration 可以看到通过Import注解导入了三个配置类&#xff1a; 通过这个这三个配置类可以看出&#xff0c;它们都使用了ConditionalOnClass注解&#xff0c;当类路径存在tomcat相关的类时&#xff0c;会配置一个T…

matlab解常微分方程常用数值解法2:龙格库塔方法

总结和记录一下matlab求解常微分方程常用的数值解法&#xff0c;本文将介绍龙格库塔方法&#xff08;Runge-Kutta Method&#xff09;。 龙格库塔迭代的基本思想是&#xff1a; x k 1 x k a k 1 b k 2 x_{k1}x_{k}a k_{1}b k_{2} xk1​xk​ak1​bk2​ k 1 h f ( x k , t …

ZDH-wemock模块

本次介绍基于版本v5.1.1 目录 项目源码 预览地址 安装包下载地址 wemock模块 wemock模块前端 配置首页 配置mock wemock服务 下载地址 打包 运行 效果展示 项目源码 zdh_web: https://github.com/zhaoyachao/zdh_web zdh_mock: https://github.com/zhaoyachao/z…

带你了解接收参数@PathVariable、@ModelAttribute 等相关注解

&#x1f600;前言 本篇博文是关于SpringBoot 接收客户端提交数据/参数会使用到的相关注解应用说明&#xff0c;希望能够帮助到您&#x1f60a; &#x1f3e0;个人主页&#xff1a;晨犀主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是晨犀&#xff0c;希望我的文…

elasticsearch 基础

ES 搜索技术历史 今天看的是《Elasticsearch实战与原理解析》 第一章 搜索技术发展史 1、搜索技术发展史 宏观而言&#xff0c;搜索引擎的发展经历了五个尖端和两大分类。五个阶段分别是ftp文件检索阶段、分类目录阶段、文本相关性检索阶段、网页链接分析阶段和用户意图识别…

警惕 C++ 中的隐式类型转换

今天文章的主题灵感来自客户的一个问题&#xff1a; 我在研究一个代码中的栈溢出问题。为了减小栈帧的大小&#xff0c;我尽可能多地删除了局部变量&#xff0c;但仍有很多栈空间无法解释。除了局部变量、参数、保存的寄存器和返回地址之外&#xff0c;栈上还有什么其他的东西…

第八章 CUDA内存应用与性能优化篇(上篇)

cuda教程目录 第一章 指针篇 第二章 CUDA原理篇 第三章 CUDA编译器环境配置篇 第四章 kernel函数基础篇 第五章 kernel索引(index)篇 第六章 kenel矩阵计算实战篇 第七章 kenel实战强化篇 第八章 CUDA内存应用与性能优化篇 第九章 CUDA原子(atomic)实战篇 第十章 CUDA流(strea…

机器学习线性代数基础

本文是斯坦福大学CS 229机器学习课程的基础材料&#xff0c;原始文件下载 原文作者&#xff1a;Zico Kolter&#xff0c;修改&#xff1a;Chuong Do&#xff0c; Tengyu Ma 翻译&#xff1a;黄海广 备注&#xff1a;请关注github的更新&#xff0c;线性代数和概率论已经更新完毕…