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

单周赛 352 概览

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<Int> {
// 线性筛
val primes = LinkedList<Int>()
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<List<Int>> {
val ret = LinkedList<List<Int>>()
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<List<Int>> {
val ret = LinkedList<List<Int>>()
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<Int, Int>()
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<Int>() { i1, i2 ->
nums[i2] - nums[i1]
}
val minHeap = PriorityQueue<Int>() { 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<Int>()
// 从队头到队尾递增(维护滑动窗口的最小值)
val minQueue = ArrayDeque<Int>()
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<Int>()
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<Int>()
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 周赛 352(2023/07/02)一场关于子数组的专题周赛的更多相关文章

  1. LeetCode 643. Maximum Average Subarray I (最大平均值子数组之一)

    Given an array consisting of n integers, find the contiguous subarray of given length k that has the ...

  2. LeetCode初级算法--设计问题02:最小栈

    LeetCode初级算法--设计问题02:最小栈 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn.net ...

  3. 2021.07.02 P1383 高级打字机题解(可持久化平衡树)

    2021.07.02 P1383 高级打字机题解(可持久化平衡树) 分析: 从可以不断撤销并且查询不算撤销这一骚操作可以肯定这是要咱建一棵可持久化的树(我也只会建可持久化的树,当然,还有可持久化并查集 ...

  4. 2021.07.02 UVa1197 多路归并模板

    2021.07.02 UVa1197 多路归并模板 UVA11997 K Smallest Sums - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 分析: 题解 UVA11997 ...

  5. JZOJ 2020.02.01【NOIP提高组】模拟A 组

    2020.02.01[NOIP提高组]模拟A 组 二月份第一场比赛 闲话 惨烈啊! 50+30+0=80分 一题都没A 唉 最高150? \(zzh\) 暴虐A组 总结: 若干新东西 \(T1\) 我 ...

  6. JZOJ 2020.02.16【NOIP提高组】模拟A 组

    2020.02.16[NOIP提高组]模拟A 组 呼呼呼呼呼呼呼呼 今天暴力分可真多啊 第一次 \(A\) 组进前 \(5\) ! 呼呼呼呼呼呼呼呼 总有人虐场,总有人在场中被虐······ 总结 3 ...

  7. [LeetCode] Minimum Size Subarray Sum 最短子数组之和

    Given an array of n positive integers and a positive integer s, find the minimal length of a subarra ...

  8. iOS开发UI篇—核心动画(转场动画和组动画)

    转自:http://www.cnblogs.com/wendingding/p/3801454.html iOS开发UI篇—核心动画(转场动画和组动画) 一.转场动画简单介绍 CAAnimation的 ...

  9. LeetCode 560. Subarray Sum Equals K (子数组之和等于K)

    Given an array of integers and an integer k, you need to find the total number of continuous subarra ...

  10. [LeetCode] Maximum Sum of 3 Non-Overlapping Subarrays 三个非重叠子数组的最大和

    In a given array nums of positive integers, find three non-overlapping subarrays with maximum sum. E ...

随机推荐

  1. AspNetCore 成长杂记(一):JWT授权鉴权之生成JWT(其一)

    引子 最近不知怎么的,自从学了WebAPI(为什么是这个,而不是MVC,还不是因为MVC的Razor语法比较难学,生态不如现有的Vue等框架,webapi很好的结合了前端生态)以后,使用别人的组件一帆 ...

  2. Ubuntu-管理开机自启动服务

    1. 管理服务启停工具 systemctl -- 将应用程序抽象为一个service,然后对这个service进行创建.启停.状态查看.配合journalctl进行日志管理 子命令 效果 start ...

  3. Sitecore XP 10.3(latest) Docker一键部署

    本文演示通过PowerShell+Docker Desktop for Windows 一键部署Sitecore10.3(即Sitecore最新版)Docker开发/测试/演示 环境. 官方参考 Si ...

  4. vue项目PC端如何适配不同分辨率屏幕

    配置前言 项目构建:基于vue-cli3构建,使用postcss-px2rem px2rem-loader插件进行rem适配实现原理:每次打包,webpack通过使用插件postcss-px2rem, ...

  5. Pillow + pytesseract + tesseract-ocr 破解简单的图形验证码

    前言: 我们在做WEB UI自动化测试时,会遇到一些图形验证码,今天就来简单介绍下,如何来识别简单的图形验证码. 一.安装 ◇ Pillow pip3 install Pillow ◇ pytesse ...

  6. 新概念英语(New Concept English),前言

    本书向读者提供了一套完整的,经过实践检验的英语学习体系,使得学生能够发挥自己的最大潜能. 听力 口语 阅读 写作 学习语言不在于掌握一套规则和积累大量词汇. 而在于如何运用所学的知识. 学习单词,必须 ...

  7. Windows亚克力特效代码实现(Dev c++可以编译通过)

    #include <windows.h> #include <dwmapi.h> // 定义一个枚举类型,表示不同的窗口组合状态 enum AccentState { ACCE ...

  8. latex-作业模板(自用,因为记不住语法55)

    \documentclass[12pt, a4paper, oneside]{ctexart} \usepackage{amsmath, amsthm, amssymb, bm, graphicx, ...

  9. MAUI开发Android程序使PDA扫码广播消息转发至Web页面

    前言 公司系统的手持终端(PDA)是用的Vue写的前端代码 在PDA上用浏览器直接打开Web页面 PDA扫码的时候,输出模式直接用模拟键盘按键的方式输出 这样在Web页面上,如果一个输入框在当前有焦点 ...

  10. 2022-08-20:给定区间的范围[xi,yi],xi<=yi,且都是正整数, 找出一个坐标集合set,set中有若干个数字, set要和每个给定的区间,有交集。 求set的最少需要几个数。 比如给

    2022-08-20:给定区间的范围[xi,yi],xi<=yi,且都是正整数, 找出一个坐标集合set,set中有若干个数字, set要和每个给定的区间,有交集. 求set的最少需要几个数. ...