这段代码使用了前缀和+单调队列的组合策略来高效解决"和至少为K的最短子数组"问题。我将从问题定义、核心思路到代码实现逐步拆解:
给定数组 nums
和整数 k
,找到和 ≥k 的最短非空子数组,返回其长度。
示例:nums = [2,-1,2]
, k = 3
→ 子数组 [2,-1,2]
和为3,长度3,返回3。
前缀和数组 prefix[i]
表示 nums
前 i
个元素的和。
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
。单调队列 q
存储前缀和数组的下标,确保队列中的下标对应的前缀和严格递增。
j
,快速找到满足 prefix[j] - prefix[i] ≥k
的最大左边界 i
(使子数组长度 j-i
最小)。i1 < i2
且 prefix[i1] ≥ prefix[i2]
,则 i1
永远不可能是最优解(因为 i2
更靠右且前缀和更小,使差值更大)。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;
}
前缀和计算:
prefix[i+1] = prefix[i] + nums[i]
,确保 prefix[j+1] - prefix[i]
表示子数组 nums[i..j]
的和。
维护单调队列:
prefix[j] ≤ prefix[q.back()]
,则弹出队尾。prefix[j] - prefix[q.front()] ≥k
,则更新最短长度并弹出队首。添加当前下标:
将 j
加入队列尾部,确保队列中的下标按前缀和递增。
输入:nums = [2,-1,2]
, k = 3
前缀和:prefix = [0, 2, 1, 3]
j |
prefix[j] |
队列 q |
操作 | 最短长度 |
---|---|---|---|---|
0 | 0 | [0] |
初始化队列 | ∞ |
1 | 2 | [0, 1] |
2-0=2<3,加入1 | ∞ |
2 | 1 | [0, 2] |
弹出1(1≥2),加入2 | ∞ |
3 | 3 | [2, 3] |
3-0=3≥3 → 长度3-0=3 | 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=3
→ 10-2=8≥3
, 10-5=5≥3
。结论:队首 i1
满足条件时,后续的 i2
、i3
可能也满足条件,且对应的子数组长度更短(因为 i2 > i1
,j-i2 < j-i1
)。
因此需要持续检查队首之后的元素,直到找到不满足条件的队首,才能保证不会遗漏更优解。
while
循环的核心作用队列中的前缀和递增,因此当 prefix[j] - prefix[q.front()] ≥k
时:
q.front()
(子数组长度最短)。示例:
prefix = [0, 1, 3, 5]
, k=2
, j=3
(prefix[j]=5
)。
i=0
:5-0=5≥2
→ 长度3-0=3。i=1
:5-1=4≥2
→ 长度3-1=2(更优)。i=2
:5-3=2≥2
→ 长度3-2=1(最优)。[3]
,循环停止。若用 if
仅检查一次队首,会漏掉后续更优的解(如长度2和1)。
每次弹出队首后,新的队首可能仍满足条件,需要继续检查:
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=2
(prefix[j]=3
)。
i=0
:3-0=3≥2
→ 记录长度2-0=2,弹出队首。[1]
,prefix[1]=2
,3-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
的必要性while
确保遍历所有可能。因此,while
循环是该算法正确性的关键,不能用 if
替代。