高级农民
- 积分
- 4143
- 大米
- 颗
- 鳄梨
- 个
- 水井
- 尺
- 蓝莓
- 颗
- 萝卜
- 根
- 小米
- 粒
- 学分
- 个
- 注册时间
- 2017-1-22
- 最后登录
- 1970-1-1
|
两题对照起来看,
209. Minimum Size Subarray Sum 这个没有负数, 所以更加简单, an array of positive integers nums and a positive integer target
直接移动窗口就行。- class Solution:
- def minSubArrayLen(self, target: int, nums: List[int]) -> int:
- l, r = 0, 0
- n = len(nums)
- currentWindowSum = 0
- ans = float('inf')
- for r in range(n):
- currentWindowSum += nums[r]
- while currentWindowSum >= target:
- # move left and keep r, since left+1 to r must <= target since no negative
- ans = min(ans, r - l + 1)
- currentWindowSum -= nums[l]
- l += 1
- if ans != float('inf') : return ans
- else: return 0
复制代码 更难的是这道题目, 862. Shortest Subarray with Sum at Least K
Given an integer array nums and an integer k, return the length of the shortest non-empty subarray of nums with a sum of at least k.
没说正负,例子给了负数,一定窗口失效,因为你移动窗口无法保证单调性。
The negative value -1 breaks the monotonic sum property that a standard sliding window relies on, making a simple variable-length sliding window approach unreliable.
好题,这道 **862. Shortest Subarray with Sum ≥ K** 的关键在于:
👉 **前缀和 + 单调队列(Monotonic Queue)**
因为数组里有 **负数**,普通滑动窗口会失效。
下面我按「为什么要这样做 → 每一步在干嘛 → 为什么是对的」来讲。
---
## 一、为什么需要前缀和?
定义前缀和:
```
preSum[i] = nums[0] + nums[1] + ... + nums[i-1]
```
这样任意子数组 `[l, r)` 的和就是:
```
sum(l, r) = preSum[r] - preSum[l]
```
问题就变成了:
> 找 **最短的 r - l**,使
> `preSum[r] - preSum[l] >= k`
---
## 二、队列里存的是什么?
队列 `q` 里存的是 **前缀和的下标**,不是值:
```
q = [i1, i2, i3, ...]
```
并且保持一个非常重要的性质:
> **preSum[q[0]] < preSum[q[1]] < preSum[q[2]] < ...**
> 👉 前缀和 **严格递增**
这就是「单调递增队列」。
---
## 三、核心思路一句话版
对每个 `i`(当作右端点):
1. **从队尾**:
把“没用的、更大的前缀和”删掉
2. **从队头**:
尝试用当前 `i` 去更新最短答案
3. 把当前 `i` 放进队列
---
## 四、逐行解释代码里的两个 `while`
### 1️⃣ 为什么要「弹出队尾」?
```python
while q and preSum[q[-1]] >= preSum[i]:
q.pop()
```
含义:
如果 **之前的前缀和 ≥ 当前前缀和**,那之前那个下标是“劣等的”。
为什么?
假设:
```
q[-1] < i
preSum[q[-1]] >= preSum[i]
```
以后对某个 `r`:
```
preSum[r] - preSum[i] >= preSum[r] - preSum[q[-1]]
```
而且:
* `i` 更靠右 → 子数组 **更短**
* 和还 **不更小**
👉 所以 `q[-1]` 永远不可能是最优左端点,直接丢掉。
---
### 2️⃣ 为什么要「弹出队头」并更新答案?
```python
while q and preSum[i] - preSum[q[0]] >= k:
ans = min(ans, i - q[0])
q.pop(0)
```
含义:
当前 `i` 作为右端点,**已经可以** 和队头形成合法子数组。
为什么可以安全地弹出?
* 队头 `q[0]` 是 **最小的前缀和**
* 对当前 `i` 来说,它给出了:
* **最早**
* **最短**
* **第一次满足 ≥ k**
以后 `i` 只会变大:
* `i - q[0]` 只会更长
* 不可能再更优
👉 所以用完就扔。
---
## 五、完整流程(直觉版)
* 前缀和把「区间和」变成「差」
* 单调队列保证:
* 左端点 **尽量靠右**
* 前缀和 **尽量小**
* 每个下标 **只进队、出队一次**
* 时间复杂度:**O(n)**
---
## 六、用例 `[2, -1, 2], k = 3` 快速走一遍
```
nums = [2, -1, 2]
preSum = [0, 2, 1, 3]
index i = 0 1 2 3
```
* i = 0 → q = [0]
* i = 1 → q = [0,1]
* i = 2 → preSum[1] >= preSum[2] → pop → q = [0,2]
* i = 3 → preSum[3] - preSum[0] = 3 ≥ 3
→ ans = 3 → pop 0
最终答案 = 3 ✅
---
## 七、一句话总结(面试可用)
> 这题用 **前缀和 + 单调递增队列**,
> 队列保证左端点的前缀和递增,
> 每次右端点扩展时:
>
> * 去掉无用的大前缀和
> * 尽量收缩左端点更新最短长度
> 从而在 **O(n)** 时间内解决包含负数的最短子数组问题。 |
|