LeetCode 周赛上分之旅 #42 当 LeetCode 考树上倍增,出题的趋势在变化吗
️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 BaguTree Pro 知识星球提问。
学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越抽象,它能覆盖的问题域就越广,理解难度也更复杂。在这个专栏里,小彭与你分享每场 LeetCode 周赛的解题报告,一起体会上分之旅。
本文是 LeetCode 上分之旅系列的第 42 篇文章,往期回顾请移步到文章末尾~
周赛 360
T1. 距离原点最远的点(Easy)
- 标签:模拟
T2. 找出美丽数组的最小和(Medium)
- 标签:散列表、贪心、数学
T3. 使子序列的和等于目标的最少操作次数(Hard)
- 标签:位运算、散列表、排序
T4. 在传球游戏中最大化函数值(Hard)
- 标签:树、倍增、动态规划、内向基环树
T1. 距离原点最远的点(Easy)
https://leetcode.cn/problems/furthest-point-from-origin/
题解(模拟)
根据题意 “_” 既可以作为 “L” 也可以作为 “R”。容易想到,为了使得终点距离原点更远,当所有 “_” 仅作为 “L” 或 “R” 对结果的贡献是最优的,此时问题的结果就取决于 “L” 和 “R” 的差绝对值。
class Solution {
fun furthestDistanceFromOrigin(moves: String): Int {
return moves.count{ it == '_' } + abs(moves.count{ it == 'L' } - moves.count{ it == 'R' })
}
}
一次遍历:
class Solution {
fun furthestDistanceFromOrigin(moves: String): Int {
var cntL = 0
var cntR = 0
for (e in moves) {
when (e) {
'L' -> {
cntL ++
cntR --
}
'R' -> {
cntL --
cntR ++
}
else -> {
cntL ++
cntR ++
}
}
}
return max(abs(cntL), abs(cntR))
}
}
复杂度分析:
- 时间复杂度:$O(n)$ 线性遍历;
- 空间复杂度:$O(1)$ 仅使用常量级别空间。
T2. 找出美丽数组的最小和(Medium)
https://leetcode.cn/problems/find-the-minimum-possible-sum-of-a-beautiful-array/
这道题与上周周赛 359 T2 2829. k-avoiding 数组的最小总和 相比,除了数据范围之外是完全相同的,有点离谱。
题解一(散列表 + 贪心)
从 $1$ 开始从小到大枚举,如果当前元素 $cur$ 与已选列表不冲突,则加入结果中。为了验证是否冲突,我们使用散列表在 $O(1)$ 时间复杂度判断。
class Solution {
fun minimumPossibleSum(n: Int, k: Int): Long {
val set = HashSet<Int>()
var sum = 0L
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)$ 散列表空间。
题解二(数学)
这道题还可以继续挖掘数学规律,我们发现当我们从 $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$
- 大于等于 k 的部分:$(n - m) * (2*k + n - m - 1) / 2$
class Solution {
fun minimumPossibleSum(n: Int, k: Int): Long {
val m = 1L * Math.min(n, k / 2)
return m * (m + 1) / 2 + (n - m) * (2 * k + n - m - 1) / 2
}
}
复杂度分析:
- 时间复杂度:$O(1)$
- 空间复杂度:$O(1)$
T3. 使子序列的和等于目标的最少操作次数(Hard)
https://leetcode.cn/problems/minimum-operations-to-form-subsequence-with-target-sum/
这道题的考点不复杂,难点在模拟问题挺考验编码功底的。
问题分析
- 关键信息: $nums$ 数组中所有元素都是 $2$ 的幂,元素顺序对结果没有影响;
- 问题是否有解: 考虑到所有数最终都能拆分成 $1$,那么只要 $nums$ 数组的和大于等于 $target$ 就一定有解;
# 二进制位
nums: _ _ _ 1 _ _ _ _
target: _ _ _ _ _ 1 _ _
- 子问题: 问题是否有解的判断不仅适用于原问题,对于仅考虑二进制位最低位 $[0]$ 到 $[k]$ 的子问题亦是如此。
以示例 1 nums = [1,2,8], target = 7
与示例 2 nums = [1,32,1,2], target = 12
为例,我们将统计 $nums$ 中不同 $2$ 的幂的出现次数:
# 二进制位
nums: _ _ _ _ 1 _ 1 1
target: _ _ _ _ _ 1 1 1
# 二进制位
nums: _ _ 1 _ _ _ 1 2 # 1 出现 2 次
target: _ _ _ _ 1 1 _ _
那么当我们从右向左枚举二进制位 $k$ 时,如果「$nums$ 中小于等于 $2^k$ 的元素和」$≥$ 「$target$ 中低于等于 $k$ 位的值」,那么对于仅考虑 $[0, k]$ 位上的子问题是有解的。否则,我们需要找到 $nums$ 中最近大于 $2^k$ 的最近数组做拆分:
# 只考虑低 2 位,可以构造
nums: _ _ _ _ 1 _ | 1 1
target: _ _ _ _ _ 1 | 1 1
# 只考虑低 3 位,无法构造,需要找到最近的 “1” 做拆分
nums: _ _ _ _ 1 | _ 1 1
target: _ _ _ _ _ | 1 1 1
# 只考虑低 3 位,无法构造,需要找到最近的 “1” 做拆分
nums: _ _ 1 _ _ | _ 1 2
target: _ _ _ _ 1 | 1 _ _
# 只考虑低 6 位,可以构造
nums: _ _ | 1 _ _ _ 1 2
target: _ _ | _ _ 1 1 _ _
组合以上技巧:
写法一(数组模拟)
思路参考灵神的题解。
- 首先,我们使用长为 $32$ 的数组,计算出 $nums$ 数组中每个 $2$ 的幂的出现次数;
- 随后,我们从低位到高位枚举二进制位 $i$,在每轮迭代中将 $nums$ 数组中的 $2^i$ 元素累加到 $sum$ 中,此举相当于在求「低 $i$ 位的子问题」可以构造的最大值;
- 最后,我们比较 $sum$ 是否大于等于 $target$(只考虑低 $i$ 位),此举相当于在判断「低 $i$ 位的子问题」是否可构造。如果不可构造,我们尝试寻找最近的 $2^j$ 做拆分;
- 另外,有一个优化点:当我们拆分将 $2^j$ 拆分到 $2^i (j > i)$ 时并不是直接丢弃 $2^j$,而是会留下 $2{j-1}、2… 2^i$ 等一系列数,可以直接跳到第 $j$ 位继续枚举。
注意一个容易 WA 的地方,在开头特判的地方,由于元素和可能会溢出 $Int$ 上界,所以我们需要转换为在 $Long$ 上的求和。
class Solution {
fun minOperations(nums: List<Int>, target: Int): Int {
if (nums.fold(0L) { it, acc -> it + acc } < target) return -1
// if (nums.sum() < target) return -1 // 溢出
// 计数
val cnts = IntArray(32)
for (num in nums) {
var i = 0
var x = num
while (x > 1) {
x = x shr 1
i += 1
}
cnts[i]++
}
var ret = 0
var i = 0
var sum = 0L
while(sum < target) {
// 累加低位的 nums
sum += (cnts[i]) shl i
// println("i=$i, sum=$sum")
// 低 i 位掩码
val mask = (1 shl (i + 1)) - 1
// 构造子问题
if (sum < target and mask) {
var j = i + 1
while (cnts[j] == 0) { // 基于开头的特判,此处一定有解
j++
}
// 拆分
ret += j - i
i = j
} else {
i += 1
}
}
return ret
}
}
复杂度分析:
- 时间复杂度:$O(n·U + U$) 其中 $n$ 为 $nums$ 数组的长度,$U$ 为整型大小 $32$;
- 空间复杂度:$O(U)$ 数组空间。
写法二(散列表模拟)
在计数的部分,我们可以使用散列表模拟,复杂度相同。
class Solution {
fun minOperations(nums: List<Int>, target: Int): Int {
if (nums.fold(0L) { it, acc -> it + acc } < target) return -1
// if (nums.sum() < target) return -1 // 溢出
// 计数
val cnts = HashMap<Int, Int>()
for (num in nums) {
cnts[num] = cnts.getOrDefault(num, 0) + 1
}
var ret = 0
var i = 0
var sum = 0L
while(sum < target) {
// 累加低位的 nums
sum += (cnts[1 shl i] ?: 0) shl i
// println("i=$i, sum=$sum")
// 低 i 位掩码
val mask = (1 shl (i + 1)) - 1
// 构造子问题
if (sum < target and mask) {
var j = i + 1
while (!cnts.containsKey(1 shl j)) { // 基于开头的特判,此处一定有解
j++
}
// 拆分
ret += j - i
i = j
} else {
i += 1
}
}
return ret
}
}
复杂度分析:
- 时间复杂度:$O(n + U)$ 其中 $n$ 为 $nums$ 数组的长度,$U$ 为整型大小 $32$;
- 空间复杂度:$O(U)$ 散列表空间。
写法三(逆向思维)
思路参考雪景式的题解,前两种写法是在从小到大枚举「选哪个」,我们也可以枚举「不选哪个」。
- 思考 1: 在原问题有解$(sum > target)$的情况下,如果从 $sum$ 中剔除最大的元素 $x$ 后,依然满足剩余的元素和 $sum’ > target$,那么直接将 $x$ 去掉,这是因为一定存在比 $x$ 操作次数更小的方案能够构造 $target$(元素越大拆分次数越多)。
- 思考 2: 如果从 $sum$ 中剔除最大的元素 $x$ 后不能构造,说明 $x$ 是一定要选择或者拆分,此时考虑 $x$ 对 $target$ 的影响:
- 如果 $x > target$,那么 $x$ 需要先拆分
- 如果 $x ≤ target$,那么 $x$ 可以被选择并抵消 $target$
class Solution {
fun minOperations(nums: MutableList<Int>, target: Int): Int {
var sum = nums.fold(0L) { it, acc -> it + acc }
if (sum < target) return -1
// 排序
nums.sortDescending()
// 从大到小枚举
var ret = 0
var left = target
while (sum > left) {
val x = nums.removeFirst()
if (sum - x >= left){
sum -= x
} else if (x <= left) {
sum -= x
left -= x
} else {
ret += 1
nums.add(0, x / 2)
nums.add(0, x / 2)
}
// println("ret=$ret, sum=$sum, left=$left, x=$x, nums=${nums.joinToString()}")
}
return ret
}
}
复杂度分析:
- 时间复杂度:$O(nlgn + n + U)$ 瓶颈在排序,枚举阶段每个元素最多访问 $1$ 次,拆分次数最多为 $U$;
- 空间复杂度:$O(lgn)$ 排序递归栈空间。
T4. 在传球游戏中最大化函数值(Hard)
https://leetcode.cn/problems/maximize-value-of-function-in-a-ball-passing-game/
题解(树上倍增)
从近期周赛的趋势看,出题人似乎有意想把 LeetCode 往偏竞赛的题目引导。
这道题如果知道树上倍增算法,其实比第三题还简单一些。
- 问题目标: 找到最佳方案,使得从起点开始传球 $k$ 次的路径和最大化;
- 暴力: 对于暴力的做法,我们可以枚举以每名玩家为起点的方案,并模拟传球过程求出最佳方案。但是这道题的步长 $k$ 的上界非常大 $10^{10}$,如果逐级向上传球,那么单次查询的时间复杂度是 $O(k)$。现在,需要思考如何优化模拟 $k$ 次传球的效率;
- 倍增思想: 借鉴 1483. 树节点的第 K 个祖先 的解法,我们可以利用倍增算法将线性的操作施加指数级别的贡献:
- 如果可以预处理出每个玩家的多级后驱玩家,那么在查询时可以加速跳转;
- 由于每个数都可以进行二进制拆分为多个 $2$ 的幂的和,如果预处理出第 $20、21、22、23、...、2^i$ 个后驱玩家,那么求解第 $k$ 次传球时可以转化为多次 $2^i$ 个后驱玩家跳转操作,大幅减少操作次数。
class Solution {
fun getMaxFunctionValue(receiver: List<Int>, k: Long): Long {
val n = receiver.size
val m = 64 - k.countLeadingZeroBits()
// 预处理
// dp[i][j] 表示 i 传球 2^j 次后的节点
val dp = Array(n) { IntArray(m) }
// dp[i][j] 表示 i 传球 2^j 次的路径和
val sum = Array(n) { LongArray(m) }
for (i in 0 until n) {
dp[i][0] = receiver[i]
sum[i][0] = receiver[i].toLong()
}
for (j in 1 until m) {
for (i in 0 until n) { // 这道题没有根节点,不需要考虑 child == -1 的情况
val child = dp[i][j - 1]
// 从 i 条 2^{j-1} 次,再跳 2^{j-1}
dp[i][j] = dp[child][j - 1]
sum[i][j] = sum[i][j - 1] + sum[child][j - 1]
}
}
// 枚举方案
var ret = 0L
for (node in 0 until n) {
var i = node
var x = k
var s = node.toLong() // 起点的贡献
while (x != 0L) {
val j = x.countTrailingZeroBits()
s += sum[i][j]
i = dp[i][j]
x = x and (x - 1)
}
ret = max(ret, s)
}
return ret
}
}
复杂度分析:
- 时间复杂度:预处理时间为 $O(nlgk)$,枚举时间为 $O(nlgk)$,其中 $n$ 为 $receivers$ 数组的长度;
- 空间复杂度:预处理空间 $O(nlgk)$。
另外,这道题还有基于「内向基环树」的 $O(n)$ 解法。
推荐阅读
LeetCode 上分之旅系列往期回顾:
️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~
LeetCode 周赛上分之旅 #42 当 LeetCode 考树上倍增,出题的趋势在变化吗的更多相关文章
- 刷爆 LeetCode 周赛 337,位掩码/回溯/同余/分桶/动态规划·打家劫舍/贪心
本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问. 大家好,我是小彭. 上周末是 LeetCode 第 337 场周赛,你参加了吗?这场周赛第三题有点放水,如果 ...
- LeetCode 周赛 342(2023/04/23)容斥原理、计数排序、滑动窗口、子数组 GCB
本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问. 大家好,我是小彭. 前天刚举办 2023 年力扣杯个人 SOLO 赛,昨天周赛就出了一场 Easy - Ea ...
- LeetCode:接雨水【42】
LeetCode:接雨水[42] 题目描述 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水. 上面是由数组 [0,1,0,2,1,0,1,3,2,1, ...
- Kindle:自动追更之云上之旅
2017年5月27: 原来的程序是批处理+Python脚本+Calibre2的方式,通过设定定时任务的方式,每天自动发动到自己的邮箱中.缺点是要一直开着电脑,又不敢放到服务器上~~ 鉴于最近公司查不关 ...
- 【Leetcode周赛】从contest-41开始。(一般是10个contest写一篇文章)
Contest 41 ()(题号) Contest 42 ()(题号) Contest 43 ()(题号) Contest 44 (2018年12月6日,周四上午)(题号653—656) 链接:htt ...
- 【Leetcode周赛】从contest-111开始。(一般是10个contest写一篇文章)
Contest 111 (题号941-944)(2019年1月19日,补充题解,主要是943题) 链接:https://leetcode.com/contest/weekly-contest-111 ...
- LeetCode 周赛 332,在套路里摸爬滚打~
本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问. 大家好,今天是 3T 选手小彭. 上周是 LeetCode 第 332 场周赛,你参加了吗?算法解题思维需要 ...
- LeetCode 周赛 333,你管这叫 Medium 难度?
本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问. 大家好,我是小彭. 上周是 LeetCode 第 333 场周赛,你参加了吗?这场周赛质量很高,但难度标得不 ...
- LeetCode 周赛 334,在算法的世界里反复横跳
本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问. 大家好,我是小彭. 今天是 LeetCode 第 334 场周赛,你参加了吗?这场周赛考察范围比较基础,整体 ...
- LeetCode 周赛 336,多少人直接 CV?
本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问. 大家好,我是小彭. 今天早上是 LeetCode 第 336 场周赛,你参加了吗?这场周赛整体质量比较高,但 ...
随机推荐
- IBM小型机 - 检测码:B150B10C,设备卡住不运行
检测码:B150B10C 问题 开机后,出现检测码:B150B10C,然后就卡在这里不动,无法进行下一步: 这里的错误码表示--机器的内存控制模块被deconfig(取消配置): 解决方法 登录cel ...
- LeetCode 周赛 345(2023/05/14)体验一题多解的算法之美
本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问. 往期回顾:LeetCode 双周赛第 104 场 · 流水的动态规划,铁打的结构化思考 周赛概览 T1. 找 ...
- 2014年蓝桥杯C/C++大学B组省赛真题(李白打酒)
题目描述: 题目描述 话说大诗人李白,一生好饮.幸好他从不开车. 一天,他提着酒壶,从家里出来,酒壶中有酒2斗.他边走边唱: 无事街上走,提壶去打酒. 逢店加一倍,遇花喝一斗. 这一路上,他一共遇到店 ...
- 基于.NetCore+React单点登录系统
对于有多个应用系统的企业来说,每一个应用系统都有自己的用户体系,这就造成用户在切换不同应用系统时,就要多次输入账号密码,导致体验非常不好,也造成使用上非常不便. 针对这个问题,我们就可以采用单点登录的 ...
- TypeError: Cannot read property 'getAttribute' of undefined
今天使用echarts + vue 做 图标,运行时提示vue.runtime.esm.js?2b0e:619 [Vue warn]: Error in mounted hook: "Typ ...
- Windows操作系统中常用的Dos命令
打开CMD的方式: Win+R 输入cmd打开控制台(推荐使用) 在任意文件夹下,按住shift键,点击鼠标右键,选择在此处打开命令行窗口 在资源管理器的地址栏路径前面加上cmd+空格: 常用的Dos ...
- .Net全网最简RabbitMQ操作【强烈推荐】
[前言] 本文自1年前的1.0版本推出以来,已被业界大量科技公司采用.同时也得到了.Net圈内多位大佬的关注+推荐,文章也被多家顶级.Net/C#公众号转载. 现在更新到了7.0版本,更好的服务各位. ...
- SpringBoot进阶教程(七十六)多维度排序查询
在项目中经常能遇到,需要对某些数据集合进行多维度排序的需求.对于集合多条件排序解决方案也有很多,今天我们就介绍一种,思路大致是设置一个分值的集合,这个分值是按照需求来设定大小的,再根据分值的大小对集合 ...
- 翻车了,被读者找出 BUG
大家好呀,我是小楼. 本文是上篇文章<使用增强版 singleflight 合并事件推送,效果炸裂!>的续集,没看过前文必须要先看完才能看本文,实在不想看,拉到文章末尾,给我点个赞再退出吧 ...
- 聊聊Flink必知必会(二)
Checkpoint与Barrier Flink是一个有状态的流处理框架,因此需要对状态做持久化,Flink定期保存状态数据到存储空间上,故障发生后从之前的备份中恢复,这个过程被称为Checkpoin ...