当前位置: 首页 > news >正文

leetcode 862. 和至少为 K 的最短子数组

这段代码使用了前缀和+单调队列的组合策略来高效解决"和至少为K的最短子数组"问题。我将从问题定义、核心思路到代码实现逐步拆解:

问题定义

给定数组 nums 和整数 k,找到和 ≥k最短非空子数组,返回其长度。
示例nums = [2,-1,2], k = 3 → 子数组 [2,-1,2] 和为3,长度3,返回3。

核心思路

1. 前缀和数组

前缀和数组 prefix[i] 表示 numsi 个元素的和。

  • 作用:快速计算子数组和。子数组 nums[i..j] 的和 = prefix[j+1] - prefix[i]
  • 示例nums = [2,-1,2]prefix = [0, 2, 1, 3]
    子数组 [2,-1] 的和 = prefix[2] - prefix[0] = 1 - 0 = 1
2. 单调队列的作用

单调队列 q 存储前缀和数组的下标,确保队列中的下标对应的前缀和严格递增

  • 目标:对于每个右边界 j,快速找到满足 prefix[j] - prefix[i] ≥k最大左边界 i(使子数组长度 j-i 最小)。
  • 优化逻辑
    1. 淘汰不可能的左边界:若存在 i1 < i2prefix[i1] ≥ prefix[i2],则 i1 永远不可能是最优解(因为 i2 更靠右且前缀和更小,使差值更大)。
    2. 单调性加速查询:队列中前缀和递增,若队首 i 不满足条件,则后续元素更不可能满足,直接停止检查。

代码实现解析

int shortestSubarray(vector<int>& nums, int k) {int n = nums.size();// 计算前缀和数组vector<long long> prefix(n + 1, 0);for (int i = 0; i < n; ++i) {prefix[i + 1] = prefix[i] + nums[i];}deque<int> q;  // 存储前缀和数组的下标,按prefix值单调递增int ans = INT_MAX;for (int j = 0; j <= n; ++j) {// 移除队尾较大的元素,保持队列单调性while (!q.empty() && prefix[j] <= prefix[q.back()]) {q.pop_back();}// 检查队首是否满足条件,更新最短长度while (!q.empty() && prefix[j] - prefix[q.front()] >= k) {ans = min(ans, j - q.front());q.pop_front();  // 队首已经找到最优解,后续无需再考虑}q.push_back(j);  // 将当前下标加入队列}return ans == INT_MAX ? -1 : ans;
}
关键步骤详解
  1. 前缀和计算
    prefix[i+1] = prefix[i] + nums[i],确保 prefix[j+1] - prefix[i] 表示子数组 nums[i..j] 的和。

  2. 维护单调队列

    • 移除队尾较大元素:若 prefix[j] ≤ prefix[q.back()],则弹出队尾。
      目的:保持队列单调性,确保后续查询时队首是最小前缀和。
    • 检查队首条件:若 prefix[j] - prefix[q.front()] ≥k,则更新最短长度并弹出队首。
      原理:队首是当前最小前缀和,若其不满足条件,则后续元素更不可能满足;若满足,则队首是当前最优左边界(对应最短长度),弹出后不再考虑。
  3. 添加当前下标
    j 加入队列尾部,确保队列中的下标按前缀和递增。

示例演示

输入nums = [2,-1,2], k = 3
前缀和prefix = [0, 2, 1, 3]

jprefix[j]队列 q操作最短长度
00[0]初始化队列
12[0, 1]2-0=2<3,加入1
21[0, 2]弹出1(1≥2),加入2
33[2, 3]3-0=3≥3 → 长度3-0=33

复杂度分析

  • 时间复杂度:O(n)。每个元素最多入队和出队一次。
  • 空间复杂度:O(n)。队列最坏情况下存储所有下标。

关键点总结

  1. 前缀和:将子数组和转化为前缀和的差值,O(1)时间计算。
  2. 单调队列
    • 单调性:确保队列中的前缀和递增,队首是最小前缀和。
    • 淘汰机制:移除不可能的左边界,加速查询。
  3. 队首弹出逻辑:一旦队首满足条件,其对应的是当前最短长度,后续无需再考虑,直接弹出。

这种方法高效解决了子数组和的优化问题,是处理类似问题的经典策略。

在这段代码中,使用 while 循环而非 if 是由单调队列的性质问题的最优解特性共同决定的。以下是详细分析:

一、为什么必须用 while 循环?

场景假设

假设队列 q 中存储的前缀和下标为 [i1, i2, i3],对应的前缀和满足 prefix[i1] < prefix[i2] < prefix[i3](队列递增)。
当遍历到 j 时,prefix[j] - prefix[i1] ≥k,说明:

  • i1 是可行的左边界,对应子数组长度为 j - i1
  • 由于 prefix[i2] > prefix[i1],则 prefix[j] - prefix[i2] < prefix[j] - prefix[i1],但可能仍 ≥k
    例如:prefix[j]=10, prefix[i1]=2, prefix[i2]=5, k=310-2=8≥3, 10-5=5≥3

结论:队首 i1 满足条件时,后续的 i2i3 可能也满足条件,且对应的子数组长度更短(因为 i2 > i1j-i2 < j-i1)。
因此需要持续检查队首之后的元素,直到找到不满足条件的队首,才能保证不会遗漏更优解。

二、while 循环的核心作用

1. 找到所有可行的左边界

队列中的前缀和递增,因此当 prefix[j] - prefix[q.front()] ≥k 时:

  • 队首是当前最小的前缀和,对应最大的可行左边界 q.front()(子数组长度最短)。
  • 但后续元素的前缀和更大,可能仍满足条件,且对应更短的子数组。

示例
prefix = [0, 1, 3, 5], k=2, j=3prefix[j]=5)。

  • 队首 i=05-0=5≥2 → 长度3-0=3。
  • 弹出队首后,新队首 i=15-1=4≥2 → 长度3-1=2(更优)。
  • 弹出队首后,新队首 i=25-3=2≥2 → 长度3-2=1(最优)。
  • 最终队列为 [3],循环停止。

若用 if 仅检查一次队首,会漏掉后续更优的解(如长度2和1)。

2. 维护队列的单调性和有效性

每次弹出队首后,新的队首可能仍满足条件,需要继续检查:

  • 队首一旦不满足条件,后续元素的前缀和更大,差值更小,必然不满足条件,循环可以终止。
  • 队首满足条件时,弹出队首是安全的,因为:
    • 该队首对应的子数组长度是当前可行解中最短的(因为队首是最大的左边界)。
    • 后续的左边界 i 更大(i > q.front()),对应的子数组长度更小,可能更优。

三、若用 if 会发生什么?

错误示例
// 错误:用if替代while
if (!q.empty() && prefix[j] - prefix[q.front()] >= k) {ans = min(ans, j - q.front());q.pop_front();
}

场景
prefix = [0, 2, 3], k=2, j=2prefix[j]=3)。

  • 队首 i=03-0=3≥2 → 记录长度2-0=2,弹出队首。
  • 此时队列变为 [1]prefix[1]=23-2=1<2,不满足条件。
  • 正确解:子数组 [2](下标1-1),和为2,长度1。
  • 错误原因:用 if 仅检查初始队首,未发现后续队首 i=1 可能满足条件(虽然本例中不满足,但存在其他情况)。

结论if 只能处理队首的单次检查,无法处理队列中多个连续满足条件的元素,导致遗漏更优解。

四、代码中的逻辑正确性

while (!q.empty() && prefix[j] - prefix[q.front()] >= k) {ans = min(ans, j - q.front()); // 记录当前最优解(可能不是全局最优)q.pop_front(); // 弹出队首,检查下一个元素
}
  • 循环条件:只要队首满足条件,就继续检查。
  • 操作顺序:先记录解,再弹出队首。
    • 因为队首是当前最大的左边界,对应的长度最短,弹出后新队首可能更优(长度更短)。
    • 例如:队首 i1 对应长度 L1,下一个队首 i2 > i1 对应长度 L2 < L1,必须记录 L2

五、总结:while 的必要性

  1. 处理多个连续可行解:队列中可能存在多个满足条件的左边界,while 确保遍历所有可能。
  2. 利用单调性剪枝:一旦队首不满足条件,后续元素必然不满足,直接终止循环,不会增加额外开销。
  3. 确保最优解:通过持续弹出队首,每次记录当前最短长度,最终得到全局最优解。

因此,while 循环是该算法正确性的关键,不能用 if 替代。

相关文章:

  • 软件开发MVC三层架构杂谈
  • C# 异步方法中缺少 `await` 运算符的隐患与解决方案
  • 计算机网络(3)——传输层
  • vue+threeJs 创造镂空管状
  • C# 深入理解类(析构函数和this关键字)
  • 集群聊天服务器学习 配置开发环境(VScode远程连接虚拟机Linux开发)(2)
  • OSPF ABR汇总路由
  • 无法同步书签,火狐浏览器修改使用国内的账号服务器
  • Ubuntu安装1Panel可视化管理服务器及青龙面板及其依赖安装教程
  • Kafka Streams 和 Apache Flink 的无状态流处理与有状态流处理
  • 人形机器人通过观看视频学习人类动作的技术可行性与前景展望
  • 鸿蒙UI开发——badge角标的使用
  • 为什么我输入对了密码,还是不能用 su 切换到 root?
  • 计算机网络学习20250524
  • 网络安全基础--第七课
  • TDengine 高可用——双活方案
  • axios接收zip文件文件
  • 2025 中青杯数学建模AB题
  • AUTOSAR图解==>AUTOSAR_SRS_LIN
  • 前端实战:用 JavaScript 模拟文件选择器,同步实现图片预览与 Base64 转换
  • 网站建设 西安/搜索词
  • 网站建设与实现 文献综述/图片搜索
  • 应用公园收费标准/seo收录查询工具
  • 有没有做市场评估的网站/好推建站
  • asp.net旅游网站开发文档/seo关键词是什么意思
  • photoshop做网站设计/网站运营推广选择乐云seo