LeetCode 周赛上分之旅 #41 结合离散化的线性 DP 问题

news2024/10/5 23:27:52

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

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

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

周赛 359

T1. 判别首字母缩略词(Easy)

  • 标签:模拟、字符串

T2. k-avoiding 数组的最小总和(Medium)

  • 标签:散列表、贪心、数学

T3. 销售利润最大化(Medium)

  • 标签:排序、桶排序、双指针、线性 DP、离散化

T4. 找出最长等值子数组(Medium)

  • 标签:分桶、双指针


T1. 判别首字母缩略词(Easy)

https://leetcode.cn/problems/check-if-a-string-is-an-acronym-of-words/

题解(模拟)

class Solution {
public:
    bool isAcronym(vector<string>& words, string s) {
        if (words.size() != s.length()) return false;
        for (int i = 0; i < words.size(); i++) {
            if (words[i][0] != s[i]) return false;
        }
        return true;
    }
};

复杂度分析:

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

T2. k-avoiding 数组的最小总和(Medium)

https://leetcode.cn/problems/determine-the-minimum-sum-of-a-k-avoiding-array/

题解一(散列表 + 贪心)

从 1 开始从小到大枚举,如果当前元素 cur 与已选列表不冲突,则加入结果中。为了验证是否冲突,我们使用散列表在 O(1) 时间复杂度判断。

class Solution {
    fun minimumSum(n: Int, k: Int): Int {
        val set = HashSet<Int>()
        var sum = 0
        var cur = 1
        repeat(n) {
            while (!set.isEmpty() && set.contains(k - cur)) cur++
            sum += cur
            set.add(cur)
            cur++
        }
        return sum
    }
}

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n) 线性遍历;
  • 空间复杂度: O ( n ) O(n) O(n) 散列表空间。

题解二(数学)

这道题还可以继续挖掘数学规律,我们发现当我们从 1 开始从小到大枚举时,每选择一个数的同时必然会使得另一个数 k - x 不可选。例如:

  • 选择 1,则 k - 1 不可选;
  • 选择 2,则 k - 2 不可选;
  • 选择 k / 2,则 k - k / 2 不可选。

可以发现,最终选择的元素被分为两部分:

  • 小于 k 的部分:选择所有和为 k 的配对中的较小值,即 1、2、3 … k / 2;
  • 大于等于 k 的部分:与其他任意正整数相加都不会等于 k,因此大于等于 k 的数必然可以选择,即 k、k + 1、k + 2、…、k + n - m - 1共 n - m 个数。

我们令 m = min(k / 2, n),使用求和公式可以 O(1) 求出两部分的总和:

  • 小于 k 的部分: m ( m + 1 ) / 2 m(m + 1)/ 2 m(m+1)/2
  • 大于等于 k 的部分: ( n − m ) ∗ ( 2 ∗ k + n − m − 1 ) / 2 (n - m) * (2*k + n - m - 1) / 2 (nm)(2k+nm1)/2
class Solution {
    fun minimumSum(n: Int, k: Int): Int {
        val m = Math.min(n, k / 2)
        return m * (m + 1) / 2 + (n - m) * (2 * k + n - m - 1) / 2
    }
}

复杂度分析:

  • 时间复杂度: O ( 1 ) O(1) O(1)
  • 空间复杂度: O ( 1 ) O(1) O(1)

T3. 销售利润最大化(Medium)

https://leetcode.cn/problems/maximize-the-profit-as-the-salesman/

问题分析

对于区间 [0, n) 的房子,如果我们选择 [i, j, gold] 的 offer,那么原问题的解就变成 gold + [0, i) + (j, n) 的两个子问题的解;

定义 dp[i] 表示到 [i] 为止可以收获的最大销售利润,则对于第 [i] 间房子有卖和不卖两种选择:

  • 不卖,那么 dp[i] = dp[i - 1]
  • 卖,那么 dp[i] = dp[i - 1] + gold。然而题目的销售 offer 是按照区间销售而不是按照单个房子销售,如果第 i 个房子没有处于 offer 的 end 端点的话,我们是不能卖出的。

因此,我们需要找到枚举 start 端点(从后往前遍历)或枚举 end 端点(从前往后遍历)的方法,并使用转移方程 dp[i] = max(dp[i], dp[start] + gold) 更新答案。

题解一(排序 + 线性 DP + 双指针)

第一种方法,我们先对所有 offer 按照 end 端点排序,并使用 j 指针指向当前终止时间最早的 offer。在动态规划的过程中,当 i 指针与 j 指针的 end 端点重合时,可以尝试更新结果。

class Solution {
    fun maximizeTheProfit(n: Int, offers: List<List<Int>>): Int {
        val m = offers.size
        // 排序
        Collections.sort(offers) { e1, e2 ->
            e1[1] - e2[1]
        }
        var j = 0
        // 线性 DP
        val dp = IntArray(n + 1)
        for (i in 1 .. n) {
            // 不卖
            dp[i] = dp[i - 1]
            // 卖
            while (j < m && i - 1 == offers[j][1]) { // while:可能存在终点重叠的区间
                dp[i] = Math.max(dp[i], dp[offers[j][0]] + offers[j][2])
                ++j
            }
        }
        return dp[n]
    }
}

复杂度分析:

  • 时间复杂度: O ( m l g m + n + m ) O(mlgm + n + m) O(mlgm+n+m) 排序预处理时间为 O ( m l g m ) O(mlgm) O(mlgm),动态规划时间为 O ( n + m ) O(n + m) O(n+m)
  • 空间复杂度: O ( n ) O(n) O(n) 排序递归栈空间 + DP 数组空间。

题解二(桶排序 + 线性 DP)

第二种方法,同样是对所有 offer 按照 end 端点排序,但我们使用桶排序优化。

class Solution {
    fun maximizeTheProfit(n: Int, offers: List<List<Int>>): Int {
        // 分桶
        val buckets = Array(n) { LinkedList<IntArray>() }
        for (offer in offers) {
            buckets[offer[1]].add(intArrayOf(offer[0], offer[2]))
        }
        // 线性 DP
        val dp = IntArray(n + 1)
        for (i in 1 .. n) {
            // 不卖
            dp[i] = dp[i - 1]
            // 卖
            for (e in buckets[i - 1]) {
                dp[i] = Math.max(dp[i], dp[e[0]] + e[1])
            }
        }
        return dp[n]
    }
}

复杂度分析:

  • 时间复杂度: O ( n + m ) O(n + m) O(n+m) 预处理时间为 O ( n + m ) O(n + m) O(n+m),其中包含 O ( n ) O(n) O(n) 时间的数组创建时间,使用散列表可以优化预处理时间到 O ( m ) O(m) O(m)。动态规划中求最值部分每个 offer 最多访问 1 次整体时间,因此动态规划的时间复杂度为 O ( n + m ) O(n + m) O(n+m)
  • 空间复杂度: O ( n ) O(n) O(n) DP 数组和分桶数组空间。

题解三(线性 DP + 离散化)

如果 n 的值域非常大的话,以上两种解法的时间和空间可能无法满足。我们发现由于影响题目的关键点仅在与 offer 的 start 端点和 end 端点,而中间空白的点或者被覆盖的点是无关紧要的。

因此,我们可以使用离散化的技巧,将所有 offer 的 start 端点和 end 端点去重后组合成新的坐标轴 points,将在 [0, n) 上的线性 DP 转换为在 [0, m) 上的线性 DP。

class Solution {
    fun maximizeTheProfit(n: Int, offers: List<List<Int>>): Int {
        // 对 start 和 end 离散化
        val pointSet = HashSet<Int>()
        for (offer in offers) {
            pointSet.add(offer[0])
            pointSet.add(offer[1])
        }
        // 排序
        val points = pointSet.toMutableList()
        points.sort()
        // 端点 -> id
        val m = points.size
        val ids = HashMap<Int, Int>()
        for (id in 0 until m) {
            ids[points[id]] = id
        }
        // 分桶
        val buckets = Array(m) { LinkedList<IntArray>() }
        for (offer in offers) {
            val start = offer[0]
            val end = offer[1]
            val gold = offer[2]
            buckets[ids[end]!!].add(intArrayOf(ids[start]!!, gold))
        }
        // 线性 DP
        val dp = IntArray(m + 1)
        for (i in 1 .. m) {
            // 不卖
            dp[i] = dp[i - 1]
            // 卖
            for (e in buckets[i - 1]) {
                dp[i] = Math.max(dp[i], dp[e[0]] + e[1])
            }
        }
        return dp[m]
    }
}

复杂度分析:

  • 时间复杂度: O ( m l g m + m ) O(mlgm + m) O(mlgm+m) 预处理时间为 O ( m l g m ) O(mlgm) O(mlgm),瓶颈在排序,线性 DP 时间为 O ( m ) O(m) O(m)
  • 空间复杂度: O ( m ) O(m) O(m) 离散化节点空间、分桶空间和线性 DP 空间都是 O ( m ) O(m) O(m) 时间复杂度。

相似题目:

  • 1235. 规划兼职工作
  • 1751. 最多可以参加的会议数目 II
  • 2008. 出租车的最大盈利
  • 2054. 两个最好的不重叠活动

T4. 找出最长等值子数组(Medium)

https://leetcode.cn/problems/find-the-longest-equal-subarray/

题解(分桶 + 同向双指针)

这道题比 T3 还稍微简单一些。

  • 分桶: 我们知道目标子数组一定发生在元素值相等的位置,因此我们可以先把所有元素下标按元素值分桶,再使用滑动窗口来寻找分桶内删除次数不超过 k 可以构造的最大连续子数组。
  • 滑动窗口: 使用同向双指针维护目标滑动窗口,当向右扩展窗口右端点时增加删除量 delete,如果 delete 大于 k 则需要缩小左端点,过程中记录连续相等子数组的最大长度。
class Solution {
    fun longestEqualSubarray(nums: List<Int>, k: Int): Int {
        val n = nums.size
        // 分桶
        val buckets = Array(n + 1) { ArrayList<Int>() }
        for ((i, num) in nums.withIndex()) {
            buckets[num].add(i)
        }
        // 同向双指针
        var ret = 1
        for (bucket in buckets) {
            val m = bucket.size
            var delete = 0
            var i = 0
            for (j in 1 until m) {
                // 增加删除量
                delete += bucket[j] - bucket[j - 1] - 1
                while (delete > k) {
                    // 恢复删除量
                    delete -= bucket[i + 1] - bucket[i] - 1
                    // 收缩左指针
                    i++
                }
                ret = Math.max(ret, j - i + 1)
            }
        }
        return ret
    }
}

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n) 分桶时间为 O ( n ) O(n) O(n),所有分桶的同向双指针时间总和为 O ( n ) O(n) O(n)
  • 空间复杂度: O ( n ) O(n) O(n) 分桶空间。

推荐阅读

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

  • LeetCode 单周赛第 358 场 · 结合中心扩展的单调栈贪心问题
  • LeetCode 单周赛第 357 场 · 多源 BFS 与连通性问题
  • LeetCode 双周赛第 111 场 · 按部就班地解决动态规划问题
  • LeetCode 双周赛第 110 场 · 结合排序不等式的动态规划

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

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

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

相关文章

设计模式——里氏替换原则

文章目录 里氏替换原则OO 中的继承性的思考和说明基本介绍一个程序引出的问题和思考解决方法 里氏替换原则 OO 中的继承性的思考和说明 继承包含这样一层含义&#xff1a;父类中凡是已经实现好的方法&#xff0c;实际上是在设定规范和契约&#xff0c;虽然它不强制要求所有的…

Web会话技术

会话:用户打开浏览器&#xff0c;访问web服务器的资源&#xff0c;会话建立&#xff0c;直到有一方断开连接&#xff0c;会话结束。在一次会话中可以包含多次请求和响应 会话跟踪:一种维护浏览器状态的方法&#xff0c;服务器需要识别多次请求是否来自于同一浏览器&#xff0c;…

线性代数的学习和整理6:向量和矩阵详细,什么是矩阵?(草稿-----未完成)

43 矩阵 4.1 矩阵 4 整理网上总结一些 关于直击线性代数本质的 观点 矩阵的本质是旋转和缩放 矩阵里的数字0矩阵里的数字1&#xff0c;表示不进行缩放矩阵里的数字2等&#xff0c;表示缩放矩阵里的数字-3 表示缩放-3倍&#xff0c;并且反向矩阵里的数字的位置矩阵拆分为列向量…

学C的第三十四天【程序环境和预处理】

相关代码gitee自取&#xff1a; C语言学习日记: 加油努力 (gitee.com) 接上期&#xff1a; 学C的第三十三天【C语言文件操作】_高高的胖子的博客-CSDN博客 1 . 程序的翻译环境和执行环境 在ANSI C(C语言标准)的任何一种实现中&#xff0c;存在两个不同的环境。 &#xff0…

Baumer工业相机堡盟工业相机如何通过BGAPISDK设置相机的Bufferlist序列(C++)

Baumer工业相机堡盟工业相机如何通过BGAPISDK设置相机的Bufferlist序列&#xff08;C&#xff09; Baumer工业相机Baumer工业相机的Bufferlist序列功能的技术背景CameraExplorer如何查看相机Bufferlist功能在BGAPI SDK里通过函数设置相机Bufferlist参数 Baumer工业相机通过BGAP…

第9步---MySQL的索引和存储引擎

第9步---MySQL的索引和存储引擎 1.索引 1.1分类 索引可以快速的找出具有特定值的行。不用从头开始进行寻找了。 类别 hash和btree hash 根据字段值生生成一个hash的值 快速的进行定位到对应的行的值 可能会出现相同的值&#xff0c;找到对应的空间会出现对应的值 btree树…

深度学习|自监督学习、MAE学习策略、消融实验

前言&#xff1a;最近在阅读论文&#xff0c;发现太多机器学习的知识不懂&#xff0c;把最近看的一篇论文有关的知识点汇总了一下。 自监督学习、MAE学习策略、消融实验 自监督学习MAE学习策略消融实验 自监督学习 Pretrain-Finetune&#xff08;预训练精调&#xff09;模式&…

从LeakCanary看如何判断对象被回收

前面已经了解了Service&#xff0c;Fragment&#xff0c;ViewModel对象的销毁时机&#xff0c;那么在触发销毁时机后&#xff0c;我们怎么判断这些对象有没有回收呢&#xff1f; 大家都知道在Java中有强引用&#xff0c;弱引用&#xff0c;软引用&#xff0c;虚引用四种引用方…

2、手写模拟Spring底层原理

创建BeanDefinition bean定义 设置BeanDefinition 的类信息&#xff0c;作用域信息 创建beanDefinitionMap scope为原型&#xff1a; scope为单例&#xff1a; 总结&#xff1a; 扫描ComponentScan注解上的包扫描路径&#xff0c;将Component注解修饰的类&#xff0c;生成Bea…

数据结构之并查集

并查集 1. 并查集原理2. 并查集实现3. 并查集应用3.1 省份数量3.2 等式方程的可满足性 4. 并查集的优缺点及时间复杂度 1. 并查集原理 并查表原理是一种树型的数据结构&#xff0c;用于处理一些不相交集合的合并及查询问题。并查集的思想是用一个数组表示了整片森林&#xff0…

Apache Doris 极简运维之BE扩缩容(1)

Apache Doris 极简运维之BE扩缩容&#xff08;1&#xff09; 一、环境信息硬件信息软件信息 二、缩容2.1 DROP BACKEND缩容2.2 DECOMMISSION BACKEND缩容2.2.1 缩容前2.2.2 缩容中2.2.3 缩容后 三、扩容3.1 扩容前3.2 扩容中3.3 扩容后 四、总结 一、环境信息 已部署三个BE节点…

十二、Linux如何修改文件/文件夹所属用户or用户组?chown命令

目录 1、基础语法 2、修改目标用户&#xff1a; 3、修改用户组&#xff1a; 4、使用-R命令&#xff0c;并同时修改用户/用户组 1、基础语法 chown [-R] [目标用户][:][目标用户组] 被修改文件/文件夹 &#xff08;1&#xff09;选项-R&#xff1a;同chmod&#xff0c;对文…

Yellowbrick新手入门简介:用于Python机器学习模型可视化的工具库

Yellowbrick 是一个新的 Python 库&#xff0c;它扩展了 Scikit-Learn API&#xff0c;将可视化合并到机器学习工作流程中。 Yellowbrick需要依赖诸多第三方库&#xff0c;包括Scikit-Learn&#xff0c;Matplotlib&#xff0c;Numpy等等。 Yellowbrick 是一个开源的纯 Python…

resource doesn‘t have a corresponding Go package.

resource doesnt have a corresponding Go package. GO这个鬼东西不能直接放src下。 ************ Building Go project: ProjectGoTest ************with GOPATH: D:\Go;D:\eclipse-jee-oxygen-2-win32-x86_64\workspace\ProjectGoTest >> Running: D:\Go\bin\go.exe …

项目管理实战笔记1:项目管理常识

序 看了下极客时间的《项目管理实战》&#xff0c;觉得跟之前学习PMP的标准资料还是有所侧重。重新整理下&#xff0c;相比书上繁杂的知识&#xff0c;这个更通俗易懂。 1 角色转换&#xff1a;三大误区 误区1&#xff1a;事必躬亲 自己做事情是可控的&#xff0c;做项目依赖…

树莓派第一讲:入门那些事(系统烧录、外设连接)

目录 基本了解&#xff1a; 系统烧录&#xff1a; 连接外设&#xff1a; 基本了解&#xff1a; 树莓派4B是一款单板计算机&#xff0c;采用ARM架构处理器&#xff0c;配备4GB内存、Gigabit以太网口、多个USB接口、HDMI输出接口等。它具备1.5Ghz运行的64位四核处理器&#x…

通过安全日志读取WFP防火墙放行日志

前言 之前的文档中&#xff0c;描写了如何对WFP防火墙进行操作以及如何在防火墙日志中读取被防火墙拦截网络通讯的日志。这边文档&#xff0c;着重描述如何读取操作系统中所有被放行的网络通信行为。 读取系统中放行的网络通信行为日志&#xff0c;在win10之后的操作系统上&am…

vmware17 开启虚拟化

前言 有时候需要在虚拟机上安装虚拟机&#xff0c;方便做一些测试 解决办法 在vmware17 上对虚拟机开启虚拟化即可 下图中都勾上即可 设置完成之后就可以在虚拟机上安装虚拟机

【LLM评估篇】Ceval | rouge | MMLU等指标

note 一些大模型的评估模型&#xff1a;多轮&#xff1a;MTBench关注评估&#xff1a;agent bench长文本评估&#xff1a;longbench&#xff0c;longeval工具调用评估&#xff1a;toolbench安全评估&#xff1a;cvalue&#xff0c;safetyprompt等 文章目录 note常见评测benchm…

18.安全机制

文章目录 安全机制认证&#xff08;Authentication&#xff09;鉴权&#xff08;Authorization&#xff09;概念和组成创建Role和ClusterRole创建RoleBinding 和ClusterRoleBindingResources 准入控制&#xff08;Admission Control&#xff09;实验&#xff1a;创建一个用户管…