往期回顾:LeetCode 单周赛第 350 场 · 滑动窗口与离散化模板题单周赛 352 概览本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。
【资料图】
T1.最长奇偶子数组(Easy)
标签:滑动窗口、枚举T2.和等于目标值的质数对(Medium)
标签:质数筛、散列表、数学T3.不间断子数组(Medium)
标签:滑动窗口、平衡树、单调队列T4.所有子数组中不平衡数字之和(Hard)
标签:平衡树、散列表、前后缀分解、乘法原理T1.最长奇偶子数组(Easy)https://leetcode.cn/problems/longest-even-odd-subarray-with-threshold/
题解一(滑动窗口 + 枚举子数组)容易想到的方法是枚举每个位置开始的子数组,并计算最长奇偶子数组长度,可以得到时间复杂度为 O(n^2) 的解法。
class Solution { fun longestAlternatingSubarray(nums: IntArray, threshold: Int): Int { var i = 0 var j = 0 val n = nums.size var ret = 0 while (j < n) { while (i < n && (nums[i] % 2 != 0 || nums[i] > threshold)) i++ if (i >= n) break j = i + 1 while (j < n && (nums[j] % 2 != nums[j - 1] % 2 && nums[j] <= threshold)) j++ ret = Math.max(ret, j - i) i ++ } return ret }}
复杂度分析:
时间复杂度:$O(n^2)$ 最坏情况整个数组都是奇偶子数组;空间复杂度:$O(1)$ 仅使用常量级别空间。题解二(枚举分组)实际上,数组被分割为若干个满足奇偶子数组的片段,最长奇偶子数组不会被其他更长的奇偶子数组所包含。因此,我们不需要枚举所有位置开始的子数组,而是枚举所有片段,修改仅在于于 ++j 修改为 i = j 而已。
class Solution { fun longestAlternatingSubarray(nums: IntArray, threshold: Int): Int { var i = 0 var j = 0 val n = nums.size var ret = 0 while (j < n) { while (i < n && (nums[i] % 2 != 0 || nums[i] > threshold)) i++ if (i >= n) break j = i + 1 while (j < n && (nums[j] % 2 != nums[j - 1] % 2 && nums[j] <= threshold)) j++ ret = Math.max(ret, j - i) i = j // 唯一修改 } return ret }}
复杂度分析:
时间复杂度:$O(n)$ i 指针和 j 指针最多移动 n 次;空间复杂度:$O(1)$ 仅使用常量级别空间。T2.和等于目标值的质数对(Medium)https://leetcode.cn/problems/prime-pairs-with-target-sum/
题解一(线性筛 + 散列表)先预处理出数据范围内所有质数,再使用两数之和寻找匹配项。
class Solution { companion object { private val U = 1000000 private val primes = generatePrime(U) private val primeSet = primes.toHashSet() private fun generatePrime(n : Int): LinkedList { // 线性筛 val primes = LinkedList() val isPrime = BooleanArray(n + 1) { true } for (e in 2..n) { if (isPrime[e]) { primes.add(e) } // 标记 for (prime in primes) { if (prime * e >= n) break isPrime[prime * e] = false if (e % prime == 0) break // 保证被最小的质因子标记 } } return primes } } fun findPrimePairs(n: Int): List> { val ret = LinkedList>() for (x in primes) { val y = n - x // 去重 if (y < x) break if (primeSet.contains(y)) ret.add(listOf(x, y)) } return ret }}
复杂度分析:
时间复杂度:预处理时间 $O(U)$,每次查询时间为 $O(n)$;空间复杂度:预处理空间 $O(U)$,每次查询空间为 $O(1)$,不考虑结果数组。题解二(奇数优化)根据奇偶数性质,如果 n 为奇数,那么当且仅当 偶数 + 奇数 = 奇数,而在所有质因子中,仅存在唯一的偶数 2。因此,当 n 为奇数时,只需要判断 n - 2 是否为质因子即可,且仅存在唯一的匹配。
class Solution { companion object { // 预处理 ... } fun findPrimePairs(n: Int): List> { val ret = LinkedList>() if (n % 2 == 1) { if (primeSet.contains(n - 2)) ret.add(listOf(2, n - 2)) return ret } for (x in primes) { val y = n - x // 去重 if (y < x) break if (primeSet.contains(y)) ret.add(listOf(x, y)) } return ret }}
复杂度分析:
时间复杂度:预处理时间 $O(U)$,每次查询时间为 $O(n)$;空间复杂度:预处理空间 $O(U)$,每次查询空间为 $O(1)$,不考虑结果数组。T3.不间断子数组(Medium)https://leetcode.cn/problems/continuous-subarrays/
题解一(滑动窗口 + 暴力 · 超出时间限制)这道题与 1438. 绝对差不超过限制的最长连续子数组 是几乎相同的,区别在于本题固定绝对差至多为 2,且目标结果是方案数而不是最长不间断子数组。
与本周赛 T1 类似,我们使用滑动窗口并维持窗口内的数据特征,从而计算满足条件的子数组方案数。同时我们发现,每个以 nums[i] 为结尾的最长不间断子数组 [i, j],都能提供 j - i + 1 个方案,因此我们只需要求出每段连续的不间断子数组,再累加结果。
class Solution { fun continuousSubarrays(nums: IntArray): Long { var i = 0 var ret = 0L for (j in nums.indices) { // 收缩左指针 for (k in i until j) { if (Math.abs(nums[k] - nums[j]) > 2) i = k + 1 } ret += j - i + 1 } return ret }}
复杂度分析:
时间复杂度:$O(n^2)$ 最坏情况下在整个数组都是不间断数组时,时间复杂度是 $O(n^2)$;空间复杂度:$O(1)$ 仅使用常量级别空间。题解二(滑动窗口 + 平衡树)题解一中每次移动右指针,都需要枚举窗口元素检查是否满足绝对差至多为 2,最坏情况下时间复杂度是 O(n^2)。为优化时间复杂度,我们使用有序集合,每次仅需要检查集合中的最小值与 nums[j] 的大小关系:
class Solution { fun continuousSubarrays(nums: IntArray): Long { var i = 0 var ret = 0L val tree = TreeMap() for (j in nums.indices) { // 收缩左指针 while (!tree.isEmpty() && (nums[j] - tree.firstKey() > 2 || tree.lastKey() - nums[j] > 2)) { tree[nums[i]] = tree[nums[i]]!! - 1 if (0 == tree[nums[i]]!!) tree.remove(nums[i]) i++ } tree[nums[j]] = tree.getOrDefault(nums[j], 0) + 1 ret += j - i + 1 } return ret }}
复杂度分析:
时间复杂度:$O(n)$ 每个元素最多入队一次,维护有序集合排序的时间复杂度是 $O(nlgn)$,由于绝对差至多为 2,有序集合中最多仅会存储 3 个键值对,排序时间降低为常数,因此时间复杂度是 $O(n)$;空间复杂度:$O(1)$ 有序集合空间,实际占用空间为常量级别空间。题解三(滑动窗口 + 双堆)同理,我们使用双堆也可以实现平衡树相同的功能。
class Solution { fun continuousSubarrays(nums: IntArray): Long { var ret = 0L var i = 0 val maxHeap = PriorityQueue() { i1, i2 -> nums[i2] - nums[i1] } val minHeap = PriorityQueue() { i1, i2 -> nums[i1] - nums[i2] } for (j in nums.indices) { // 收缩左指针 while (!maxHeap.isEmpty() && nums[maxHeap.peek()] - nums[j] > 2) { maxHeap.remove(i) minHeap.remove(i) i++ } while (!minHeap.isEmpty() && nums[j] - nums[minHeap.peek()] > 2) { maxHeap.remove(i) minHeap.remove(i) i++ } maxHeap.offer(j) minHeap.offer(j) ret += maxHeap.size // ret += j - i + 1 } return ret }}
复杂度分析:
时间复杂度:$O(n)$ 每个元素最多入堆两次,维护堆排序的时间复杂度是 $O(nlgn)$,由于绝对差至多为 2,堆中最多仅会存储 3 个元素,排序时间降低为常数,因此时间复杂度是 O(n);空间复杂度:$O(1)$ 双堆空间,实际占用空间为常量级别空间。题解四(滑动窗口 + 单调队列)求滑动窗口的极值问题有单调队列的经验解。
在有序集合的解法中,忽略了滑动窗口中元素的顺序关系:当元素 nums[i] 后方出现出现更大的元素时,那么 nums[i] 不可能对滑动窗口的 x - nums[j] 的结果有贡献;同理,当 nums[i] 后方出现更小的元素时,那么 nums[i] 不可能对滑动窗口的 nums[i] - x 的结果有贡献。
对结果没有贡献的元素,应该提前弹出数据结构(在平衡树和堆的解法中,会保留在数据结构中,从而拉低时间复杂度)。
class Solution { fun continuousSubarrays(nums: IntArray): Long { var ret = 0L var i = 0 // 从队头到队尾递减(维护滑动窗口的最大值) val maxQueue = ArrayDeque() // 从队头到队尾递增(维护滑动窗口的最小值) val minQueue = ArrayDeque() for (j in nums.indices) { // 维护单调性 while (!maxQueue.isEmpty() && maxQueue.peekLast() < nums[j]) maxQueue.pollLast() while (!minQueue.isEmpty() && minQueue.peekLast() > nums[j]) minQueue.pollLast() maxQueue.offer(nums[j]) minQueue.offer(nums[j]) // 维护滑动窗口极值 while (maxQueue.peekFirst() - minQueue.peekFirst() > 2) { if (nums[i] == maxQueue.peekFirst()) maxQueue.pollFirst() if (nums[i] == minQueue.peekFirst()) minQueue.pollFirst() i++ } ret += j - i + 1 } return ret }}
复杂度分析:
时间复杂度:$O(n)$ 在每次检查仅需要检查队尾元素,每个元素最多出队和出队两次,这是严格 $O(n)$ 的解法;空间复杂度:$O(1)$ 单调队列空间,实际占用空间为常量级别空间。T4.所有子数组中不平衡数字之和(Hard)https://leetcode.cn/problems/sum-of-imbalance-numbers-of-all-subarrays/
题解一(枚举子数组 + 平衡树)题目的不平衡度表示子数组排序后与前驱元素的差值大于 1 的个数(长度为 k 的子数组的最大不平衡度为 k - 1),最直接的做法是先排序再计数。我们可以维护子数组的有序集合,并维护插入前后的不平衡度:
如果在有序集合的首部或尾部插入,则直接调整插入后的平衡度;如果在有序集合的中间插入,则需要减去插入前贡献的不平衡度,再增加插入后贡献的不平衡度:class Solution { fun sumImbalanceNumbers(nums: IntArray): Int { var ret = 0 for (i in 0 until nums.size) { var cnt = 0 val tree = TreeSet() for (j in i until nums.size) { val pivot = nums[j] val lower = tree.floor(pivot) // 小于等于 val higher = tree.ceiling(pivot) // 大于等于 if (null != lower && null != higher && higher - lower > 1) cnt-- if (null != lower && pivot - lower > 1) cnt++ if (null != higher && higher - pivot > 1) cnt ++ tree.add(pivot) // 子数组结果 ret += cnt } } return ret }}
复杂度分析:
时间复杂度:$O(n·nlgn)$ 外层循环枚举 n 次,有序集合排序时间为 $O(nlgn)$;空间复杂度:$O(n)$ 有序集合空间。题解二(枚举子数组 + 散列表)由于我们并不需要得到排序后的数组,而是检查每个元素与前驱的关系,因此对于每个元素 nums[i],我们只需要检查 nums[i] + 1 和 nums[i] - 1 是否存在。
枚举子数组元素 i,并维护不平衡度 cnt:
如果 nums[i] 已经存在,那么增加 nums[i] 对平衡度没有影响;如果 nums[i] 不存在,那么可能构造一个不平衡度,再观察 nums[i] + 1 和 nums[i] - 1 是否出现过来扣除不平衡度。class Solution { fun sumImbalanceNumbers(nums: IntArray): Int { var ret = 0 for (i in 0 until nums.size) { var cnt = 0 val set = HashSet() for (j in i until nums.size) { val x = nums[j] // 维护不平衡度 if (!set.contains(x)) { cnt++ if (set.contains(x + 1)) cnt-- if (set.contains(x - 1)) cnt-- set.add(nums[j]) } // 子数组结果 ret += cnt - 1 // 减 1 是因为最后一个元素不会构造不平衡度 } } return ret }}
复杂度分析:
时间复杂度:$O(n^2)$ 外层循环枚举 n 次,内层循环是线性时间;空间复杂度:$O(n)$ 散列表空间。题解三(中心扩展 + 前后缀分解 + 乘法原理)思路参考:https://leetcode.cn/problems/sum-of-imbalance-numbers-of-all-subarrays/solutions/2327213/onde-by-dengyun-yyl3/
好棒的思维!
使用逆向思维,我们考虑每个元素 nums[i] 能够贡献的不平衡度,以 nums[i] 为中心点向左右扩展直到遇到最近的 nums[i] - 1,使用乘法原理可以计算出 nums[i] 对多少个子数组产生贡献度。
需要考虑到,如果 nums[i] 是作为子数组的最小值时,是不会产生贡献度的,所以我们要把这部分子数组减去。然而,在使用乘法原理时我们无法方便地知道 nums[i] 在子数组中排序的位置,也就无法知道应该减去多少无效子数组。使用整体思维,我们先忽略无效子数组,同时发现每个子数组中都会存在一个最小值,因此整体来看无效子数组的个数就是子数组的个数,即 N*(N+1)/2;
同时,为了优化时间复杂度,我们可以在第一次线性遍历中预处理出以 nums[i] 开始的后缀中最近的 nums[i] - 1 的位置。在第二次线性遍历中求出以 nums[i] 为中点的前缀中的最近 nums[i] - 1 的位置。
最后还有一个细节,考虑到存在重复数的测试用例 [2,3,1,4,3],排序后 [1,2,3,3,4] 中只有最左边的 3 会贡献不平衡度。为了避免重复计算,我们规定排序后最左边的 3 来自于当前子数组中最右边的 3,因此在预处理后缀数组时,我们要使用 Math.min(ids[nums[i]], ids[nums[i] - 1]) 来中断遍历。
class Solution { fun sumImbalanceNumbers(nums: IntArray): Int { val n = nums.size // 前缀数组和后缀数组 // ids:记录每个元素最近出现位置 var ids = IntArray(n + 1) { n } val prefix = IntArray(n + 1) { -1 } val suffix = IntArray(n + 1) { n } // 预处理后缀数组 for (i in n - 1 downTo 0) { suffix[i] = Math.min(ids[nums[i]], ids[nums[i] - 1]) ids[nums[i]] = i } // 预处理前缀数组 ids = IntArray(n + 1) { -1 } for (i in 0 until n) { prefix[i] = ids[nums[i] - 1] ids[nums[i]] = i } // 乘法原理 var ret = 0 for (i in 0 until n) { ret += (i - prefix[i]) * (suffix[i] - i) } return ret - n * (n + 1) / 2 }}
在计算前缀数组时累加结果:
class Solution { fun sumImbalanceNumbers(nums: IntArray): Int { val n = nums.size // 前缀数组和后缀数组 // ids:记录每个元素最近出现位置 var ids = IntArray(n + 1) { n } var prefix = -1 val suffix = IntArray(n + 1) { n } // 预处理后缀数组 for (i in n - 1 downTo 0) { suffix[i] = Math.min(ids[nums[i]], ids[nums[i] - 1]) ids[nums[i]] = i } // 预处理前缀数组 + 乘法原理 var ret = 0 ids = IntArray(n + 1) { -1 } for (i in 0 until n) { prefix = ids[nums[i] - 1] ids[nums[i]] = i ret += (i - prefix) * (suffix[i] - i) } return ret - n * (n + 1) / 2 }}
复杂度分析:
时间复杂度:$O(n)$ 两次线性遍历;空间复杂度:$O(n)$ 前后缀数组空间。往期回顾LeetCode 单周赛第 350 场 · 滑动窗口与离散化模板题LeetCode 单周赛第 348 场 · 数位 DP 模版学会了吗?LeetCode 双周赛第 107 场 · 很有意思的 T2 题LeetCode 双周赛第 104 场 · 流水的动态规划,铁打的结构化思考标签:
>**本文已收录到[AndroidFamily](https: github com pengxurui Androi
1、大汉天子东方朔算出皇帝坐牢是第2集。2、剧情简介3、梁王派出杀手,
庞大集团投资者索赔通道已开启昔日“4S店之王”退市后何去何从?202...
奔驰新E级自2016年上市以来,销量一路攀升,而且4S店终端基本没有什么
威海广泰:关于申请向不特定对象发行可转换公司债券审核问询函回复的提
国内首个GLP-1获批减重适应症华东医药开启减重市场新纪元2023-07-0420:
汇中股份与安富利达成战略伙伴关系海外市场增速将逐步释放2023-07-0420
捷安高科公告控股股东实际控制人郑乐观张安全和北京嘉景拟减持公司股份
今天(7月4日) 男子故意1次取1元逼哭柜员 冲上热搜第一引发网友热议7
海天股份:拟与雁江建投水务共同投建污泥处置中心项目2023-07-0419:54
7月4日,深圳市豪恩汽车电子装备股份有限公司(简称“豪恩汽电”,...
7月4日金融一线消息,国家金融监督管理总局网站昨日发布山西银保监局办
北森控股有望9月初纳入港股通:上市后营收稳步增长CoreHCM一体化客户收
高新发展在投资者互动平台表示,公司联营企业倍芯传感主要从事传感与感
紫阳富硒茶应该在干燥、通风、阴凉的地方保存,避免阳光和潮湿。可以用
驻日本大使吴江浩就日本福岛核污染水问题召开专题记者会2023年7月4日,
1、教育这块并没有明确的标准,其中地域、学科、年级,老师的资质等等
大家好,小乐来为大家解答以上的问题。螟蛉有子蜾蠃负之什么意思,螟蛉
玛莎拉蒂推出Tridente,这是其首个为客户和汽车爱好者提供的综合会员计
本文从宋代绘画创作角度出发,通过对宋代山水画风格、主题的研究,
2021款本田CR-V(参数|询价)加92号汽油,汽油标号不同代表燃油抗爆性不
在当今竞争激烈的社会中,学生和家长对于教育的关注点之一是教育机构是
中新社北京7月4日电中国汽车工业协会4日披露,2023年1-5月,中国汽车整
程序上从宽,更准确地说是程序上从简,是指:对于基层法院管辖的可
《碧蓝航线》在日本的人气直逼一些大厂的游戏,像《少女前线》也在韩国
在顺利实现日达产目标后,管理、技术、操作人员鼓足干劲、不骄不躁、再
电力行业数字化转型如火如荼海康威视助力寿光电厂实现巡检工作数字化升
农业农村部:全国农产品批发市场猪肉平均价格为18 92元 公斤,比昨天下
开放时间:每日9:00-17:00,计划开放两个月■馒头山路27号避暑纳凉点(
苹编程客栈果常常在暂时维护其网站期间,为其添加之前未曝光的新内容。
广告
X 关闭
广告
X 关闭