LeetCode 周赛上分之旅 # 37 多源 BFS 与连通性问题

news2024/11/26 18:54:35

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 BaguTree Pro 知识星球提问。

学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越抽象,它能覆盖的问题域就越广,理解难度也更复杂。在这个专栏里,小彭与你分享每场 LeetCode 周赛的解题报告,一起体会上分之旅。

本文是 LeetCode 上分之旅系列的第 37 篇文章,往期回顾请移步到文章末尾~

周赛 357

T1. 故障键盘(Easy)

  • 标签:模拟、字符串

T2. 判断是否能拆分数组(Medium)

  • 标签:思维

T3. 找出最安全路径(Medium)

  • 标签:BFS、连通性、分层并查集、极大化极小、二分查找

T4. 子序列最大优雅度(Hard)

  • 标签:贪心、排序、堆


T1. 故障键盘(Easy)

https://leetcode.cn/problems/faulty-keyboard/

题解(模拟)

简单模拟题。

  • 在遇到 i 字符时对已填入字符进行反转,时间复杂度是 O(n^2);
  • 使用队列和标记位可以优化时间复杂度,在遇到 i 时修改标记位和写入方向,在最后输出时根据标记位输出,避免中间的反转操作。
class Solution {
public:
    string finalString(string s) {
        vector<char> dst;
        for (auto& c : s) {
            if (c == 'i') {
                reverse(dst.begin(), dst.end());
            } else {
                dst.push_back(c);
            }
        }
        return string(dst.begin(), dst.end());
    }
};
class Solution {
public:
    string finalString(string s) {
        deque<char> dst;
        bool rear = true;
        for (auto& c : s) {
            if (c == 'i') {
                rear = !rear;
            } else {
                if (rear) {
                    dst.push_back(c);
                } else {
                    dst.push_front(c);
                }
            }
        }
        return rear ? string(dst.begin(), dst.end()) : string(dst.rbegin(), dst.rend());
    }
};

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n) 线性遍历和输出时间;
  • 空间复杂度: O ( n ) O(n) O(n) 临时字符串空间。

T2. 判断是否能拆分数组(Medium)

https://leetcode.cn/problems/check-if-it-is-possible-to-split-array/

题解(思维题)

思维题,主要题目的两个条件只要满足其中一个即可 😭

  • 条件 1:子数组的长度为 1 ⇒ 说明数组长度小于等于 2 的时候,一定可以满足(子数组的长度不大于 1);
  • 条件 2:子数组元素之和大于或等于  m ⇒ 需满足子数组 {a1, a2, a3} 与 {a4, a5, a6} 的子数组和均大于等于 m。

结合两个条件,如果我们能找到两个相邻的元素之和大于等于 m,那么总可以通过消除 1 个元素的方式完成题目要求。

例如在示例 3 [2, 3, 3, 2, 3] 中,我们以 [3,3] 为起点倒推:

  • [3, 3]
  • [2, 3, 3] 消除 2
  • [2, 3, 3, 2] 消除 2
  • [2, 3, 3, 2, 3] 消除 3
class Solution {
public:
    bool canSplitArray(vector<int>& nums, int m) {
        // 2 | 3, 3 | 2 | 3
        // 1, 3, 2, 2, 3
        // 1, 1, 1, 3, 3
        if (nums.size() <= 2) return true;
        for (int i = 1; i < nums.size(); i++) {
            if (nums[i] + nums[i - 1] >= m) return true;
        }
        return false;
    }
};

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n) 线性遍历时间;
  • 空间复杂度: O ( 1 ) O(1) O(1) 仅使用常量级别空间。

T3. 找出最安全路径(Medium)

https://leetcode.cn/problems/find-the-safest-path-in-a-grid/

题解一(多源 BFS + 二分答案)

根据题目描述,每个节点的安全系数定位为该节点到「小偷」节点的最小曼哈顿距离,而题目要求是寻找从 [0][0] 到 [n-1][n-1] 的最大安全系数。「使得最小曼哈顿距离最大」暗示可能是需要使用二分答案的极大化极小问题。

  • 多源 BFS 预处理: 先从每个「小偷」节点开始走 BFS 更新相邻节点的最小曼哈顿距离,单次 BFS 的时间复杂度是 O(n^2),虽然我们可以用剪枝优化,但整体的时间复杂度上界是 O(n^4)。为了优化时间复杂度,我们使用多源 BFS(也可以理解为拓扑排序,每次弹出的节点的曼哈顿距离最小),整体的时间仅为 O(n^2);
  • 二分答案: 安全系数与路径可达性存在单调性:
    • 当安全系数越大时,越不容易可达;
    • 当安全系数越小时,越容易可达。
    • 安全系数的下界为 0,上界为 n * 2 - 1,通过二分答案寻找满足可达性的最大安全系数:
class Solution {
    fun maximumSafenessFactor(grid: List<List<Int>>): Int {
        val INF = Integer.MAX_VALUE
        val directions = arrayOf(intArrayOf(0,1), intArrayOf(1,0), intArrayOf(0,-1), intArrayOf(-1,0))
        val n = grid.size
        // 特判
        if (grid[0][0] == 1 || grid[n - 1][n - 1] == 1) return 0
        // 多源 BFS(拓扑排序)
        val safe = Array(n) { IntArray(n) { -1 }}
        var queue = LinkedList<IntArray>()
        for (r in 0 until n) {
            for (c in 0 until n) {
                if (grid[r][c] == 1) {
                    queue.offer(intArrayOf(r, c))
                    safe[r][c] = 0
                }
            }
        }
        while (!queue.isEmpty()) {
            val temp = LinkedList<IntArray>()
            for (node in queue) {
                for (direction in directions) {
                    val newX = node[0] + direction[0]
                    val newY = node[1] + direction[1]
                    if (newX < 0 || newX >= n || newY < 0 || newY >= n || safe[newX][newY] != -1) continue
                    temp.offer(intArrayOf(newX, newY))
                    safe[newX][newY] = safe[node[0]][node[1]] + 1
                }
            }
            queue = temp
        }

        // for (row in safe) println(row.joinToString())

        // BFS(检查只通过大于等于 limit 的格子,能否到达终点)
        fun check(limit: Int) : Boolean {
            val visit = Array(n) { BooleanArray(n) }
            var queue = LinkedList<IntArray>()
            queue.offer(intArrayOf(0, 0))
            visit[0][0] = true
            while (!queue.isEmpty()) {
                val temp = LinkedList<IntArray>()
                for (node in queue) {
                    // 终止条件
                    if (node[0] == n - 1 && node[1] == n - 1) return true
                    for (direction in directions) {
                        val newX = node[0] + direction[0]
                        val newY = node[1] + direction[1]
                        if (newX < 0 || newX >= n || newY < 0 || newY >= n || visit[newX][newY] || safe[newX][newY] < limit) continue
                        temp.offer(intArrayOf(newX, newY))
                        visit[newX][newY] = true
                    }
                }
                queue = temp
            }
            return false
        }

        // 二分查找
        var left = 0
        var right = Math.min(safe[0][0], safe[n - 1][n - 1])
        while (left < right) {
            val mid = (left + right + 1) ushr 1
            if (!check(mid)) {
                right = mid - 1
            } else {
                left = mid
            }
        }
        return left
    }
}

复杂度分析:

  • 时间复杂度: O ( n 2 ⋅ l g n 2 ) O(n^2·lgn^2) O(n2lgn2) 其中 多源 BFS 时间为 O ( n 2 ) O(n^2) O(n2),单次检查的 BFS 时间复杂度为 O ( n 2 ) O(n^2) O(n2),二分的次数为 l g n 2 lgn^2 lgn2,整体时间复杂度是 O ( n 2 ⋅ l g n 2 ) O(n^2·lgn^2) O(n2lgn2)
  • 空间复杂度: O ( n 2 ) O(n^2) O(n2) safe 安全系数矩阵空间。

题解二(多源 BFS + 堆)

思路参考雪景式的题解。

在题解一预处理的基础上,同样走一次 BFS 也能够算出最大安全系数,思路类似于 Dijkstra 最最短路算法中使用当前最短路最短的节点去松弛相邻边,我们优先让当前曼哈顿距离最大的节点去松弛相邻节点,以保证每个节点都能够从较大的路径转移过来。

class Solution {
    fun maximumSafenessFactor(grid: List<List<Int>>): Int {
        ...
        // 类最短路(使用曼哈顿距离最大的节点去松弛相邻边)
        val heap = PriorityQueue<IntArray>() { e1, e2 ->
            e2[0] - e1[0]
        }
        heap.offer(intArrayOf(safe[0][0], 0, 0))
        val visit = Array(n) { BooleanArray(n) }
        visit[0][0] = true
        while (!heap.isEmpty()) {
            val node = heap.poll()
            if (node[1] == n - 1 && node[2] == n - 1) return node[0]
            for (direction in directions) {
                val newX = node[1] + direction[0]
                val newY = node[2] + direction[1]
                if (newX < 0 || newX >= n || newY < 0 || newY >= n || visit[newX][newY]) continue
                // 松弛相邻边
                heap.offer(intArrayOf(Math.min(node[0], safe[newX][newY]), newX, newY))
                visit[newX][newY] = true
            }
        }
        return 0
    }
}

复杂度分析:

  • 时间复杂度: O ( n 2 ⋅ l g n 2 ) O(n^2·lgn^2) O(n2lgn2) 其中 多源 BFS 时间为 O ( n 2 ) O(n^2) O(n2),基于堆的 BFS 的时间复杂度为 O ( n 2 ⋅ l g n 2 ) O(n^2·lgn^2) O(n2lgn2)
  • 空间复杂度: O ( n 2 ) O(n^2) O(n2) safe 安全系数矩阵空间。

题解三(多源 BFS + 分层并查集)

思路参考灵神的题解。

其实,求从 [0][0] 到 [n - 1][n - 1] 的最大安全系数,也相当于连通性问题的变形,而连通性问题有并查集的解法。为了求得最大安全系数,我们使用分层并查集:

  • 首先,在预处理阶段求出每个节点的最小曼哈顿距离,并将节点按照曼哈顿距离分类;
  • 其次,我们从最大的曼哈顿距离开始逆序合并,当 [0][0] 和 [n - 1][n - 1] 连通时返回结果。
class Solution {
    fun maximumSafenessFactor(grid: List<List<Int>>): Int {
        val directions = arrayOf(intArrayOf(0,1), intArrayOf(1,0), intArrayOf(0,-1), intArrayOf(-1,0))
        val n = grid.size
        // 特判
        if (grid[0][0] == 1 || grid[n - 1][n - 1] == 1) return 0
        // 多源 BFS(拓扑排序)
        val safe = Array(n) { IntArray(n) { -1 }}
        // 分层
        val groups = LinkedList<LinkedList<IntArray>>()
        var queue = LinkedList<IntArray>()
        for (r in 0 until n) {
            for (c in 0 until n) {
                if (grid[r][c] == 1) {
                    queue.offer(intArrayOf(r, c))
                    safe[r][c] = 0
                }
            }
        }
        groups.add(queue)
        while (!queue.isEmpty()) {
            val temp = LinkedList<IntArray>()
            for (node in queue) {
                for (direction in directions) {
                    val newX = node[0] + direction[0]
                    val newY = node[1] + direction[1]
                    if (newX < 0 || newX >= n || newY < 0 || newY >= n || safe[newX][newY] != -1) continue
                    temp.offer(intArrayOf(newX, newY))
                    safe[newX][newY] = safe[node[0]][node[1]] + 1
                }
            }
            queue = temp
            if (!queue.isEmpty()) groups.add(queue)
        }

        // for (row in safe) println(row.joinToString())
        // for (row in groups) println(row.joinToString())

        val helper = UnionFind(n)
        // 逆序合并
        for (i in groups.size - 1 downTo 0) {
            for (node in groups[i]) {
                val x = node[0]
                val y = node[1]
                for (direction in directions) {
                    val newX = x + direction[0]
                    val newY = y  + direction[1]
                    // 合并曼哈顿距离大于等于当前层的节点
                    if (newX < 0 || newX >= n || newY < 0 || newY >= n || safe[newX][newY] < i) continue
                    helper.union(x * n + y, newX * n + newY)
                }
            }
            if (helper.find(0) == helper.find(n * n - 1)) return i
        }
        return 0
    }

    class UnionFind(private val n: Int) {
        private val parents = IntArray(n * n) { it }
        private val ranks = IntArray(n * n)

        fun find(x: Int): Int {
            var cur = x
            while (cur != parents[cur]) {
                parents[cur] = parents[parents[cur]]
                cur = parents[cur]
            }
            return cur
        }

        fun union(x: Int, y: Int) {
            val rootX = find(x)
            val rootY = find(y)
            if (ranks[rootX] < ranks[rootY]) {
                parents[rootX] = rootY
            } else if (ranks[rootX] > ranks[rootY]){
                parents[rootY] = rootX
            } else {
                parents[rootY] = rootX
                ranks[rootX]++
            }
        }
    }
}

复杂度分析:

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2) 其中 多源 BFS 时间为 O ( n 2 ) O(n^2) O(n2),基于路径压缩和按秩合并的并查集时间复杂度为 O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度: O ( n 2 ) O(n^2) O(n2) safe 安全系数矩阵空间。

T4. 子序列最大优雅度(Hard)

https://leetcode.cn/problems/maximum-elegance-of-a-k-length-subsequence/

题解(反悔贪心 + 堆)

  • 固定一个维度: 题目定义的优雅度 total_profit + distinct_categories^2 存在两个维度的变量,我们考虑固定其中一个维度来简化问题讨论:
    • 对所有节点按照利润从大到小逆序排列,并选择前 k 个节点,此时的 total_profit 是最大的;
    • 在此基础上,我们继续遍历剩余的 n - k 个节点,并考虑替换前 k 个节点中的某个节点,由于已经选择的节点 total_profit 是最大的,因此需要让替换后的类目数变多。
  • 分类讨论(替换哪个):
    • 1、如果某个已选节点与第 i 个节点的类目相同,那么替换后不会让类目数变大,不可能让优雅度变大;
    • 2、如果某个已选节点与第 i 个节点的类目不同,但只出现一次,那么替换出不会让类目变大,不可能让优雅度变大。否则,如果出现多次,替换后类目数变大,有可能让优雅度变大;
    • 3、为了让优雅度尽可能大,我们期望替换后的 total_profit 的减少量尽可能小,同时数目类别应该增大,否则无法获得更大的优雅度。为了让替换后的 total_profit 的减少量尽可能小,我们应该替换已选列表中利润最小同时重复的节点。
  • 怎么高效替换:
    • 使用堆维护利润最小同时重复的元素,由于我们是从大到小线性枚举的,因此直接使用线性表模拟堆的能力;
    • 新替换进去的不会被替换出去(想想为什么)。
class Solution {
    fun findMaximumElegance(items: Array<IntArray>, k: Int): Long {
        Arrays.sort(items) { e1, e2 ->
            e2[0] - e1[0]
        }
        var ret = 0L
        var totalProfit = 0L
        // duplicate:小顶堆
        val duplicate = LinkedList<Int>()
        // categorySet:类目表
        val categorySet = HashSet<Int>()
        for ((i, item) in items.withIndex()) {
            val profit = item[0]
            val category = item[1]
            if (i < k) {
                totalProfit += item[0]
                // 记录重复节点
                if (categorySet.contains(category)) {
                    duplicate.add(profit)
                }
                categorySet.add(category)
            } else if (!duplicate.isEmpty() && !categorySet.contains(category)){
                // 替换
                totalProfit += profit - duplicate.pollLast()
                categorySet.add(category)
            } else {
                // 不会让类目数量变大
            }
            // println(duplicate.joinToString())
            ret = Math.max(ret, totalProfit + 1L * categorySet.size * categorySet.size)
        }
        return ret
    }
}

复杂度分析:

  • 时间复杂度: O ( n l g n ) O(nlgn) O(nlgn) 瓶颈在排序;
  • 空间复杂度: O ( n ) O(n) O(n) 堆空间。

推荐阅读

LeetCode 上分之旅系列往期回顾:

  • LeetCode 单周赛第 356 场 · KMP 字符串匹配殊途同归
  • LeetCode 单周赛第 355 场 · 两题坐牢,菜鸡现出原形
  • LeetCode 双周赛第 109 场 · 按部就班地解决动态规划问题
  • LeetCode 双周赛第 107 场 · 很有意思的 T2 题

⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~

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

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

相关文章

ArcGIS制作带蒙版的遥感影像地图

这次文章我们来介绍一下&#xff0c;如何通过一个系统的步骤完成ArcGIS制作带蒙版的遥感影像地图。 主要的步骤包括&#xff1a; 1 添加行政区划数据 2 导出兴趣去乡镇矢量范围 3 添加遥感影像底图 4 制作蒙版 5 利用自动完成面制作蒙版 6 标注乡镇带晕渲文字 7 …

Golang 包详解以及go mod

Golang 中包的介绍和定义 包(package)是多个 Go 源码的集合,是一种高级的代码复用方案,Go 语言为我们提供了 很多内置包,如 fmt、strconv、strings、sort、errors、time、encoding/json、os、io 等。 Golang 中的包可以分为三种:1、系统内置包 2、自定义包 3、第三方包…

云服务器SVN仓库搭建(以阿里云为例)

远程连接阿里云服务器 安装svn(注意需要root权限使用命令sudo su) yum install subversion 安装成功后查看svn版本 svnserve --version 创建版本库的根目录 mkdir /var/svn 创建代码仓库 svnadmin create /var/svn/test 当前生成的目录结构 此处为svn的配置文件 创建用户名…

C# App.config和Web.config加密

步骤1&#xff1a;创建加密命令 使用ASP.NET提供的命令工具aspnet_regiis来创建加密命令。 1、打开控制台窗口&#xff0c;在命令行中输入以下命令&#xff1a; cd C:\Windows\Microsoft.NET\Framework\v4.xxxxx aspnet_regiis.exe -pef connectionStrings "C:\MyAppFo…

【PyQt5程序的打包和发布】

【PyQt5程序的打包和发布】 1 安装Pyinstaller模块2 打包普通Python程序3 打包PyQt5程序4 打包资源文件 1 安装Pyinstaller模块 pip install Pyinstaller2 打包普通Python程序 普通Python程序由Python内部库提供&#xff0c;不包含第三方库模板。 使用如下命令打包&#xff1…

【Docker】数据库动态授权组件在Kubernetes集群下的测试过程记录

目录 背景 组件原理 测试设计 环境 测试脚本 脚本build为linux可执行文件 镜像构建 Dockerfile Docker build 镜像有效性验证 总结 资料获取方法 背景 我们都知道出于安全性考虑&#xff0c;生产环境的权限一般都是要做最小化控制&#xff0c;尤其是数据库的操作授…

【现网】记一次并发冲突导致流量放大的生产问题

目录 事故现象 转账 业务背景介绍 背景一&#xff1a;转账流程 转账流程 转账异常处理 转账异常处理流程图 背景二&#xff1a;账户系统合并 实际全流程&#xff1a; 背景三&#xff1a;扣内存数据库逻辑 背景四&#xff1a;调用方重试逻辑 问题定位 总结 资料获取…

org.apache.hadoop.hive.ql.exec.DDLTask. show Locks LockManager not specified解决

Error while processing statement: FAILED: Execution Error, return code 1 from org.apache.hadoop.hive.ql.exec.DDLTask. show Locks LockManager not specified解决 当在Hive中执行show locks语句时&#xff0c;出现"LockManager not specified"错误通常是由于…

认识“协议“序列化和反序列化

目录 前言 1 应用层 2 在谈协议 3 序列化和反序列化 4 网络版计算器 4.1 指定协议 request结构体 response结构体 4.2 服务端编写 4.3 客户端的编写 5 Json for C 的序列化和反序列化使用样例 前言 之前的socket编程&#xff0c;都是在通过系统调用层面&#xff0c;…

acwing第 115 场周赛第二题题解:维护最大值和次大值

一、链接 5132. 奶牛照相 二、题目 约翰的农场有 nn 头奶牛&#xff0c;编号 1∼n1∼n。 其中&#xff0c;第 ii 头奶牛的宽度为 wiwi&#xff0c;高度为 hihi&#xff0c; 有一天&#xff0c;它们聚餐后决定拍照留念。 关于拍照的描述如下&#xff1a; 它们一共拍了 nn…

C++初阶语法——命名空间

前言&#xff1a;C&#xff0c;即cplusplus&#xff0c;顾名思义&#xff0c;是C语言promax版本&#xff0c;C兼容C语言。 C的诞生是因为贝尔实验室的本贾尼等大佬认为C语言的语法坑实在太多&#xff0c;拥有许多不足之处&#xff08;比如命名冲突&#xff0c;&#xff09;&…

Vue3 实现产品图片放大器

Vue3 实现类似淘宝、京东产品详情图片放大器功能 环境&#xff1a;vue3tsvite 1.创建picShow.vue组件 <script lang"ts" setup> import {ref, computed} from vue import {useMouseInElement} from vueuse/core/*获取父组件的传值*/ defineProps<{images:…

通信原理板块-书籍推荐及学习系列

微信公众号上线&#xff0c;搜索公众号小灰灰的FPGA,关注可获取相关源码&#xff0c;定期更新有关FPGA的项目以及开源项目源码&#xff0c;包括但不限于各类检测芯片驱动、低速接口驱动、高速接口驱动、数据信号处理、图像处理以及AXI总线等 关注公众号&#xff0c;后台回复通信…

在WebStorm中通过live-server插件搭建Ajax运行环境

1.下载node.js 官网: https://nodejs.cn/download/ 2.配置Node.js的HTTPS 使用淘宝的镜像&#xff1a; npm config set registry https://registry.npm.taobao.org 也可以使用cnpm npm install -g cnpm --registryhttps://registry.npm.taobao.org 配置之后可以验证是否成…

Linux(四)--包软件管理器与Linux上环境部署示例

一.包软件管理器【yum和apt】 1.先来学习使用yum命令。yum&#xff1a;RPM包软件管理器&#xff0c;用于自动化安装配置Linux软件&#xff0c;并可以自动解决依赖问题。通过yum命令我们可以轻松实现软件的下载&#xff0c;查找&#xff0c;卸载与更新等管理软件的操作。 最常用…

【Change】50 Matplotlib Visualizations, Python实现,源码可复现

详情请参考博客: Top 50 matplotlib Visualizations 因编译更新问题&#xff0c;本文将稍作更改&#xff0c;以便能够顺利运行。 1 Time Series Plot 时间串行图用于可视化给定指标如何随时间变化。在这里&#xff0c;您可以看到1949年至1969年间航空客运量的变化。查看此免费…

【Linux】socket编程简单的日志打印

1 UDP编程步骤 1.1 服务端 1.2 客户端 2 TCP编程步骤 2.1 服务端 2.2 客户端 3 日志打印

瑞吉外卖系统05

哈喽&#xff01;大家好&#xff0c;我是旷世奇才李先生 文章持续更新&#xff0c;可以微信搜索【小奇JAVA面试】第一时间阅读&#xff0c;回复【资料】更有我为大家准备的福利哟&#xff0c;回复【项目】获取我为大家准备的项目 最近打算把我手里之前做的项目分享给大家&#…

算法通关村—轻松搞定二叉树的高度和深度问题

1.二叉树的最大深度 二叉树的最大深度 给定一个二叉树 root &#xff0c;返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 1.1 递归 通过上面的步骤能够看出&#xff0c;深度取决于左右子树&#xff0c;只要左子树有&#xff0c;那么高…

java泛型和通配符的使用

泛型机制 本质是参数化类型(与方法的形式参数比较&#xff0c;方法是参数化对象)。 优势:将类型检查由运行期提前到编译期。减少了很多错误。 泛型是jdk5.0的新特性。 集合中使用泛型 总结&#xff1a; ① 集合接口或集合类在jdk5.0时都修改为带泛型的结构② 在实例化集合类时…