LeetCode 双周赛 104(2023/05/13)流水的动态规划,铁打的结构化思考
本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问。
T1. 老人的数目(Easy)
- 标签:模拟、计数
T2. 矩阵中的和(Medium)
- 标签:模拟、排序
T3. 最大或值(Medium)
- 标签:动态规划、前后缀分解、贪心
T4. 英雄的力量(Hard)
- 标签:排序、贪心、动态规划、数学
T1. 老人的数目(Easy)
https://leetcode.cn/problems/number-of-senior-citizens/
简单模拟题,直接截取年龄字符后计数即可:
class Solution {
fun countSeniors(details: Array<String>): Int {
return details.count { it.substring(11, 13).toInt() > 60 }
}
}
除了将字符串转为整数再比较外,还可以直接比较子串与 “60”
的字典序:
class Solution {
fun countSeniors(details: Array<String>): Int {
return details.count { it.substring(11, 13) > "60" }
}
}
复杂度分析:
- 时间复杂度:$O(n)$ 其中 n 为 details 数组的长度;
- 空间复杂度:$O(1)$ 仅使用常量级别空间。
T2. 矩阵中的和(Medium)
https://leetcode.cn/problems/sum-in-a-matrix/
简单模拟题。
先对每一行排序,再取每一列的最大值。
class Solution {
fun matrixSum(nums: Array<IntArray>): Int {
var ret = 0
for (row in nums) {
row.sort()
}
for (j in 0 until nums[0].size) {
var mx = 0
for (i in 0 until nums.size) {
mx = Math.max(mx, nums[i][j])
}
ret += mx
}
return ret
}
}
复杂度分析:
- 时间复杂度:$O(nmlgm + nm)$ 其中 n 和 m 分别为矩阵的行数和列数,排序时间 $O(nmlgm)$,扫描时间 $O(nm)$;
- 空间复杂度:$O(lgm)$ 排序递归栈空间。
T3. 最大或值(Medium)
https://leetcode.cn/problems/maximum-or/
题目描述
给你一个下标从 0 开始长度为 n
的整数数组 nums
和一个整数 k
。每一次操作中,你可以选择一个数并将它乘 2
。
你最多可以进行 k
次操作,请你返回 **nums[0] | nums[1] | ... | nums[n - 1]
的最大值。
a | b
表示两个整数 a
和 b
的 按位或 运算。
示例 1:
输入:nums = [12,9], k = 1
输出:30
解释:如果我们对下标为 1 的元素进行操作,新的数组为 [12,18] 。此时得到最优答案为 12 和 18 的按位或运算的结果,也就是 30 。
示例 2:
输入:nums = [8,1,2], k = 2
输出:35
解释:如果我们对下标 0 处的元素进行操作,得到新数组 [32,1,2] 。此时得到最优答案为 32|1|2 = 35 。
提示:
1 <= nums.length <= 105
1 <= nums[i] <= 109
1 <= k <= 15
问题结构化
1、概括问题目标
计算可以获得的最大或值。
2、分析问题要件
在每次操作中,可以从数组中选择一个数乘以 2,亦相当于向左位移 1 位。
3、观察问题数据
- 数据量:问题数据量上界为 $10^5$,要求算法时间复杂度低于 $O(n^2)$;
- 数据大小:元素值的上界为 $10^9$,操作次数 k 的上界为 15(这个性质有什么用呢?);
- 输出结果:以长整型 Long 的形式返回结果。
4、观察测试用例
以示例 1 nums=[12, 9], k = 1 为例,最优答案是对 9 乘以 2,说明操作最大值并不一定能获得最大或值。
5、提高抽象程度
- 权重:二进制位越高的位对数字大小的影响越大,因此我们应该尽量让高位的二进制位置为 1;
- 是否为决策问题?由于每次操作有多种位置选择,因此这是一个决策问题。
6、具体化解决手段
- 1、贪心:结合「数据大小」分析,由于操作次数 k 的上界为 15 次,无论如何位移都不会溢出 Long。因此,我们可以将 k 次位移操作作用在同一个数字上,尽可能让高位的位置置为 1;
- 2、动态规划(背包):假设已经计算出数组前 i - 1 个元素能够组成的最大或值,那么考虑拼接 nums[i],可以选择不操作 nums[i],也可以选择在 nums[i] 上操作 x 次,那么问题就变成「前 i - 1 个元素中操作 k - x 次的最大或值」与「num[i] 操作 x 次的或值」合并的或值。「前 i - 1 个元素中操作 k - x 次的最大或值」这是一个与原问题相似但规模更小的子问题,可以用动态规划解决,更具体地可以用背包问题模型解决。
题解一(贪心 + 前后缀分解)
枚举所有数字并向左位移 k 次,计算所有方案的最优解:
class Solution {
fun maximumOr(nums: IntArray, k: Int): Long {
val n = nums.size
// 前后缀分解
val pre = IntArray(n + 1)
val suf = IntArray(n + 1)
for (i in 1 .. n) {
pre[i] = pre[i - 1] or nums[i - 1]
}
for (i in n - 1 downTo 0) {
suf[i] = suf[i + 1] or nums[i]
}
var ret = 0L
for (i in nums.indices) {
ret = Math.max(ret, (1L * nums[i] shl k) or pre[i].toLong() or suf[i + 1].toLong())
}
return ret
}
}
由于每个方案都需要枚举前后 n - 1 个数字的或值,因此这是一个 $O(n^2)$ 的解法,会超出时间限制。我们可以采用空间换时间的策略,预先计算出每个位置(不包含)的前后缀的或值,这个技巧就是「前后缀分解」。
在实现细节上,我们可以把其中一个前缀放在扫描的时候处理。
class Solution {
fun maximumOr(nums: IntArray, k: Int): Long {
val n = nums.size
// 前后缀分解
val suf = IntArray(n + 1)
for (i in n - 1 downTo 0) {
suf[i] = suf[i + 1] or nums[i]
}
var ret = 0L
var pre = 0L
for (i in nums.indices) {
ret = Math.max(ret, pre or (1L * nums[i] shl k) or suf[i + 1].toLong())
pre = pre or nums[i].toLong()
}
return ret
}
}
复杂度分析:
- 时间复杂度:$O(n)$ 其中 n 为 nums 数组的长度;
- 空间复杂度:$O(n)$ 后缀或值数组长度空间。
题解二(动态规划)
使用背包问题模型时,定义 dp[i][j] 表示在前 i 个元素上操作 k 次可以获得的最大或值,则有:
- 状态转移方程:$dp[i][j] = max{dp[i-1][j], dp[i - 1][j - x] | (nums[i] << x)}$
- 终止条件:$dp[n][k]$
class Solution {
fun maximumOr(nums: IntArray, k: Int): Long {
val n = nums.size
// 以 i 为止,且移动 k 次的最大或值
val dp = Array(n + 1) { LongArray(k + 1) }
for (i in 1 .. n) {
for (j in 0 .. k) {
for (m in 0 .. j) {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - m] or (1L * nums[i - 1] shl m) /* 移动 m 次 */)
}
}
}
return dp[n][k]
}
}
另外,这个背包问题可以取消物品维度来优化空间:
class Solution {
fun maximumOr(nums: IntArray, k: Int): Long {
val n = nums.size
// 以 i 为止,且移动 k 次的最大或值
val dp = LongArray(k + 1)
for (i in 1 .. n) {
// 逆序
for (j in k downTo 0) {
for (m in 0 .. j) {
dp[j] = Math.max(dp[j], dp[j - m] or (1L * nums[i - 1] shl m) /* 移动 m 次 */)
}
}
}
return dp[k]
}
}
- 时间复杂度:$O(n·k^2)$ 其中 n 为 nums 数组的长度;
- 空间复杂度:$O(k)$ DP 数组空间
相似题目:
T4. 英雄的力量(Hard)
https://leetcode.cn/problems/power-of-heroes/
题目描述
给你一个下标从 0 开始的整数数组 nums
,它表示英雄的能力值。如果我们选出一部分英雄,这组英雄的 力量 定义为:
i0
,i1
,...ik
表示这组英雄在数组中的下标。那么这组英雄的力量为max(nums[i0],nums[i1] ... nums[ik])2 * min(nums[i0],nums[i1] ... nums[ik])
。
请你返回所有可能的 非空 英雄组的 力量 之和。由于答案可能非常大,请你将结果对 109 + 7
取余。
示例 1:
输入:nums = [2,1,4]
输出:141
解释:
第 1 组:[2] 的力量为 22 * 2 = 8 。
第 2 组:[1] 的力量为 12 * 1 = 1 。
第 3 组:[4] 的力量为 42 * 4 = 64 。
第 4 组:[2,1] 的力量为 22 * 1 = 4 。
第 5 组:[2,4] 的力量为 42 * 2 = 32 。
第 6 组:[1,4] 的力量为 42 * 1 = 16 。
第 7 组:[2,1,4] 的力量为 42 * 1 = 16 。
所有英雄组的力量之和为 8 + 1 + 64 + 4 + 32 + 16 + 16 = 141 。
示例 2:
输入:nums = [1,1,1]
输出:7
解释:总共有 7 个英雄组,每一组的力量都是 1 。所以所有英雄组的力量之和为 7 。
提示:
1 <= nums.length <= 105
1 <= nums[i] <= 109
问题结构化
1、概括问题目标
计算所有组合方案的「力量」总和。
2、分析问题要件
枚举所有子集,计算子集的力量值计算公式为$「最大值^2*最小值」$。
3、观察问题数据
- 数据量:问题数据量上界为 $10^5$,要求算法时间复杂度低于 $O(n^2)$;
- 数据大小:元素值的上界为 $10^9$,乘法运算会溢出整型上界,需要考虑大数问题。
4、观察问题测试用例:
以数组 nums=[1, 2, 3] 为例:
- 分析小规模问题:[] 空集的力量值是 0,只包含 1 个元素子集的力量值计算也没有问题;
子集 | 最大值 | 最小值 | 力量值 |
---|---|---|---|
{} | 0 | 0 | 0 |
1 | 1 | $1^2*1$ | |
2 | 2 | $2^2*2$ | |
3 | 3 | $3^2*3$ |
- 分析规模为 2 的子集问题:
子集 | 最大值 | 最小值 | 力量值 |
---|---|---|---|
2 | 1 | $2^2*1$ | |
3 | 1 | $3^2*1$ | |
3 | 2 | $3^2*2$ |
- 分析规模为 3 的子集问题:
子集 | 最大值 | 最小值 | 力量值 |
---|---|---|---|
3 | 1 | $3^2*1$ |
5、如何解决问题
- 手段 1(暴力枚举):如果枚举所有子集,再求每个子集的力量值,那么时间复杂度会达到非常高的 $O(n·2^n)$,其中有 $2^n$ 种子集(一共有 n 个数字,每个数字有选和不选两种状态),每个子集花费 $O(n)$ 线性扫描最大值和最小值。
至此,问题陷入瓶颈,解决方法是重复以上步骤,枚举掌握的数据结构、算法和技巧寻找思路,突破口在于从另一个角度来理解问题规模(动态规划的思路)。
6、继续观察问题测试用例
同样以数组 nums = [1, 2, 3] 为例:
- 考虑空集的力量值问题:
子集 | 最大值 | 最小值 |
---|---|---|
{} | 0 | 0 |
- 考虑到「1」为止的力量值问题:
子集 | 最大值 | 最小值 |
---|---|---|
{} | 0 | 0 |
1 | 1 |
- 考虑到「2」为止的力量值问题:
子集 | 最大值 | 最小值 |
---|---|---|
{} | 0 | 0 |
1 | 1 | |
2 | 2 | |
2 | 1 |
- 考虑到「3」为止的力量值问题:
子集 | 最大值 | 最小值 |
---|---|---|
{} | 0 | 0 |
1 | 1 | |
2 | 2 | |
2 | 1 | |
3 | 3 | |
3 | 1 | |
3 | 2 | |
3 | 1 |
这又说明了什么呢?
- 关键点 1 - 递推地构造子集:
我们发现子集问题可以用递推地方式构造,当我们增加考虑一个新元素时,其实是将已有子集复制一份后,再复制的子集里添加元素。例如我们在考虑「2」时,是将 {} 和 {1} 复制一份后添加再添加元素「2」。
- 关键点 2 - 最大值的贡献:
由于我们是从小到大增加元素,所以复制后新子集中的最大值一定等于当前元素,那么问题的关键就在「如何计算这些新子集的最小值」。
- 关键点 3 - 最小值的贡献:
由于我们采用子集复制的方式理解子集构造问题,容易发现数字越早出现,最小值出现次数越大(哆啦 A 梦的翻倍药水)。
例如最初最小值为 1 的子集个数为 1 次,在处理「2」后最小值为 1 的子集个数为 2 次,因此在处理「3」时,就会累加 2 次以 1 为最小值的力量值:$2(3^21)$。同理会累加 1 次以 2 为最小值的力量值:$1(32*2)$,另外还要累加从空集转移而来的 {3}。
至此,问题的解决办法逐渐清晰。
7、解决问题的新手段
- 手段 2(动态规划):
考虑有 a, b, c, d, e 五个数,按顺序从小到大排列,且从小到大枚举。
当枚举到 d 时,复制增加的新子集包括:
- 以 a 为最小值的子集有 4 个:累加力量值 $4(d^2a)$
- 以 b 为最小值的子集有 2 个:累加力量值 $2(d^2b)$
- 以 c 为最小值的子集有 1 个:累加力量值 $1(d^2c)$
另外还有以 d 本身为最小值的子集 1 个:累加力量值 $1(d^2d)$,将 d 左侧元素对结果的贡献即为 s,则有 $pow(d) = d^2*(s + d)$。
继续枚举到 e 是,复制增加的新子集包括:
- 以 a 为最小值的子集有 8 个:累加力量值 $8(e^2a)$
- 以 b 为最小值的子集有 4 个:累加力量值 $4(e^2b)$
- 以 c 为最小值的子集有 2 个:累加力量值 $2(e^2c)$
- 以 d 为最小值的子集有 1个:累加力量值 $1(e^2d)$
另外还有以 e 本身为最小值的子集 1 个:累加力量值 $1(e^2e)$,将 e 左侧元素对结果的贡献即为 s`,则有 $pow(e) = e^2*(s` + e)$。
观察 s 和 s` 的关系:
$s = 4a + 2b + 1*c$
$s = 8a + 4b + 2c + d = s2 + d$
这说明,我们可以维护每个元素左侧元素的贡献度 s,并通过 s 来计算当前元素新增的所有子集的力量值,并且时间复杂度只需要 O(1)!
[4,3,2,1]
1 1 2 4
追加 5:
[5,4,3,2,1]
1 1 2 4 8
题解(动态规划)
根据问题分析得出的递归公式,使用递推模拟即可,先不考虑大数问题:
class Solution {
fun sumOfPower(nums: IntArray): Int {
var ret = 0L
// 排序
nums.sort()
// 影响因子
var s = 0L
for (x in nums) {
ret += (x * x) * (s + x)
s = s * 2 + x
}
return ret.toInt()
}
}
再考虑大数问题:
class Solution {
fun sumOfPower(nums: IntArray): Int {
val MOD = 1000000007
var ret = 0L
// 排序
nums.sort()
// 影响因子
var s = 0L
for (x in nums) {
ret = (ret + (1L * x * x % MOD) * (s + x)) % MOD // x*x 也可能溢出
s = (s * 2 + x) % MOD
}
return ret.toInt()
}
}
实战中我用的是先计算最大影响因子,再累减的写法:
class Solution {
fun sumOfPower(nums: IntArray): Int {
val MOD = 1000000007
var ret = 0L
val n = nums.size
// 排序
nums.sortDescending()
// 影响因子
var s = 0L
var p = 1L
for (i in 1 until n) {
s = (s + nums[i] * p) % MOD
p = (2 * p) % MOD
}
// 枚举子集
for (i in 0 until n) {
val x = nums[i]
ret = (ret + x * x % MOD * (s + x)) % MOD
if (i < n - 1) {
s = (s - nums[i + 1]) % MOD
if (s and 1L != 0L) {
s += MOD // 奇数除 2 会丢失精度
}
s = (s / 2) % MOD
}
}
return ret.toInt()
}
}
复杂度分析:
- 时间复杂度:$O(nlgn)$ 其中 n 为 nums 数组的长度,瓶颈在排序上,计算力量值部分时间复杂度为 O(n);
- 空间复杂度:$O(lgn)$ 排序递归栈空间。
往期回顾
- LeetCode 单周赛第 344 场 · 手写递归函数的通用套路
- LeetCode 单周赛第 343 场 · 结合「下一个排列」的贪心构造问题
- LeetCode 双周赛第 103 场 · 区间求和的树状数组经典应用
- LeetCode 双周赛第 102 场· 这次又是最短路。
LeetCode 双周赛 104(2023/05/13)流水的动态规划,铁打的结构化思考的更多相关文章
- LeetCode 双周赛 102,模拟 / BFS / Dijkstra / Floyd
本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问. 大家好,欢迎来到小彭的 LeetCode 周赛解题报告. 昨晚是 LeetCode 双周赛第 102 场,你 ...
- LeetCode 双周赛 98,脑筋急转弯转不过来!
本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问. 大家好,我是小彭. 昨晚是 LeetCode 第 98 场双周赛,你参加了吗?这场周赛需要脑筋急转弯,转不过 ...
- 刷爆 LeetCode 双周赛 100,单方面宣布第一题最难
本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问. 大家好,我是小彭. 上周末是 LeetCode 第 100 场双周赛,你参加了吗?这场周赛整体没有 Hard ...
- LeetCode 双周赛 101,DP/中心位贪心/裴蜀定理/Dijkstra/最小环
本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问. 大家好,我是小彭. 这周比较忙,上周末的双周赛题解现在才更新,虽迟但到哈.上周末这场是 LeetCode 第 ...
- leetcode 双周赛9 进击的骑士
一个坐标可以从 -infinity 延伸到 +infinity 的 无限大的 棋盘上,你的 骑士 驻扎在坐标为 [0, 0] 的方格里. 骑士的走法和中国象棋中的马相似,走 “日” 字:即先向左(或右 ...
- leetcode 双周赛9 找出所有行中最小公共元素
给你一个矩阵 mat,其中每一行的元素都已经按 递增 顺序排好了.请你帮忙找出在所有这些行中 最小的公共元素. 如果矩阵中没有这样的公共元素,就请返回 -1. 示例: 输入:mat = [[,,,,] ...
- [每日一题2020.06.16] leetcode双周赛T3 5423 找两个和为目标值且不重叠的子数组 DP, 前缀和
题目链接 给你一个整数数组 arr 和一个整数值 target . 请你在 arr 中找 两个互不重叠的子数组 且它们的和都等于 target .可能会有多种方案,请你返回满足要求的两个子数组长度和的 ...
- LeetCode双周赛#36
1604. 警告一小时内使用相同员工卡大于等于三次的人 题目链接 题意 给定两个字符串数组keyName和keyTime,分别表示名字为keytime[i]的人,在某一天内使用员工卡的时间(格式为24 ...
- LeetCode双周赛#35
1589. 所有排列中的最大和 #差分 #贪心 题目链接 题意 给定整数数组nums,以及查询数组requests,其中requests[i] = [starti, endi] .第i个查询求 num ...
- LeetCode双周赛#34
5492. 分割字符串的方案数 #组合公式 #乘法原理 #区间分割 题目链接 题意 给定01二进制串\(s\),可将\(s\)分割为三个非空 字符串\(s_1,s_2,s_3\),即(\(s_1+s_ ...
随机推荐
- 给临时停车号码牌插上翅膀:lua脚本语言加入—鲁哇客智能挪车号码牌技术升级之路
预计6月中旬上线的,带语音的智能挪车号码牌,会新增lua编程脚本的支持.类似于我们的手机,从功能机到智能机的进化,有着划时代的意义:产品功能不再由厂家决定,她可由lua编程脚本书写,随时编辑修改. l ...
- FastReport OpenSource发布到Linux上的准备
一.安装libgdiplus(libgdiplus是一个Mono库,用于对非Windows操作系统提供GDI+兼容的API) apt-get install build-essential lib ...
- k8s 关于pull image failed 问题
问题描述: Failed to pull image "nginx": rpc error: code = Unknown desc = failed to pul 解决办法: 1 ...
- ESP32 优化 IRAM 内存方法整理 ---ESP32
有以下三种方便的方法来优化 IRAM 内存: 启用 menuconfig -> Compiler option -> Optimization Level -> Optimize f ...
- 使用Git进行版本控制,不同的项目怎么设置不同的提交用户名和邮箱呢?
1.全局设置用户名和邮箱 因为平时除了开发公司项目还会写自己的项目或者去维护开源项目,一般情况下,公司会要求提交代码时使用自己的真名或者拼音和公司邮箱,以前就只会设置全局用户名或邮箱如下 git co ...
- operator简介
原理 operator 是一种 kubernetes 的扩展形式,利用自定义资源对象(Custom Resource)来管理应用和组件,允许用户以 Kubernetes 的声明式 API 风格来管理应 ...
- uniapp中easycom用法详解
Uniapp中的easycom是一种组件自动注册机制,可以让开发者更加方便地使用和管理组件.下面详细介绍下关于easycom使用方法. 什么是easycom? easycom是Uniapp框架提供的一 ...
- Java面试——写一个生产者与消费者
更多内容,前往个人博客 一.通过synchronize 中的 wait 和 notify 实现 [1]我们可以将生产者和消费者需要的方法写在公共类中 1 package com.yintong.con ...
- 开源不到 48 小时获 35k star 的推荐算法「GitHub 热点速览」
本周的热点除了 GPT 各类衍生品之外,还多了一个被马斯克预告过.在愚人节开源出来的推特推荐算法,开源不到 2 天就有了 35k+ 的 star,有意思的是,除了推荐算法本身之外,阅读源码的工程师们甚 ...
- 使用 Solon Cloud 的 Jaeger 做请求链路跟踪
<dependency> <groupId>org.noear</groupId> <artifactId>jaeger-solon-cloud-plu ...