和至少为 K 的最短子数组
返回 A 的最短的非空连续子数组的长度,该子数组的和至少为 K 。
如果没有和至少为 K 的非空子数组,返回 -1 。
示例 1:
输入:A = [1], K = 1
输出:1
一开始想的很好,直接暴力循环嘛,然而这当然是不能通过全解的,这是困难难度,不是中等难度。
果然,最后一个样例,A有十万个数字,暴力循环直接超时。
看官方解,思路如下:
我们用数组 P 表示数组 A 的前缀和,即 P[i] = A[0] + A[1] + ... + A[i - 1]。我们需要找到 x 和 y,使得 P[y] - P[x] >= K 且 y - x 最小。
这一步大家都理解,无非是避免重复计算,优化算法。
我们用 opt(y) 表示对于固定的 y,最大的满足 P[x] <= P[y] - K 的 x,这样所有 y - opt(y) 中的最小值即为答案。
第一步得到了前缀和数组P,通过对于P的遍历,我们可以找到答案。到这一步都还好理解,但是仅仅如此优化的程度还不够,还是暴力循环的路子。
1. 如果 x1 < x2 且 P[x2] <= P[x1],那么 opt(y) 的值不可能为 x1,这是因为 x2 比 x1 大,并且如果 x1 满足了 P[x1] <= P[y] - K,那么 P[x2] <= P[x1] <= P[y] - K,即 x2 同样满足 P[x2]<= P[y] - K。
2. 如果 opt(y1) 的值为 x,那么我们以后就不用再考虑 x 了。这是因为如果有 y2 > y1 且 opt(y2) 的值也为 x,但此时y2 - x 显然大于 y1 - x,不会作为所有 y - opt(y) 中的最小值。
这是此题的两条性质,都还好理解。它们的作用是舍弃不必要的计算,来达到优化时间复杂度的作用。
性质一举例: 若P[1]=2,P[2]=5,而P[3]=3,显然题目给出的数组A中,A[0]=2,A[1]=3,A[2]=-2
那么在考虑元素坐标y=6时,寻找opt(y)即最大的满足 P[x] <= P[y] - K 的 x时,我们可以直接略过2,遍历顺序为P[1],[3]........这就是利用性质一来略去不必要的计算,实质则是利用前缀和的性质直接略去A中值为负数的坐标。但这不是简单的略去,我们略去的只是该坐标的前缀和,这个为负数的元素仍然存在于坐标位于它之后的前缀和中,例如上面提到的A[2]值为-2,我们舍去了P[2],但A[2]仍然被包含于P[3]中。
我们维护一个关于前缀和数组 P 的单调队列C,它是一个双端队列(deque),其中存放了下标 x:x0, x1, ... 满足 P[x0], P[x1], ... 单调递增。这是为了满足性质一。
当我们遇到了一个新的下标 y 时,我们会在C队尾移除若干元素,直到 P[x0],P[x1], ..., P[y] 单调递增。这同样是为了满足性质一。
同时,我们会在C队首也移除若干元素,如果 P[y] >= P[x0] + K,则将C队首元素移除,直到该不等式不满足。这是为了满足性质二。
注意,为了便于理解有修改。
这是这道题的精华之处,首先看第一条,之所以要有双端队列C,是为了便于两端pop元素。而之所以需要在两端pop元素,是为了略去不必要的计算(之所以能略去则是由于本题的性质所决定,上面已经提到)。
我们算法的所有操作,都在双端队列C上进行,前缀和数组P只是一个提供基础数据的作用。需要注意的是,双端队列中保存的x1,x2,x3指的是原题中给出的数组A的元素坐标,而非前缀和。
leetcode这串话,其实第一段和第二段是一个意思,即利用性质一使双端队列C单调递增,pop掉C队尾的不能使单调递增性质成立的前缀和元素,至于为什么上面已经提到。
性质二很简单,我就不详细说了。
代码如下:
1 int n = A.size();//这里需要注意一下,最好将A.size()转化为int,因为A.size()其实是自然数,后续要是有A.size()-7之类的会发生下溢 2 vector<int> p(n + 1, 0); 3 for (int i = 0; i < n; ++i) 4 { 5 p[i + 1] = p[i] + A[i]; 6 }//生成前缀和数组p 7 deque<int> c;//定义双端队列c 8 int ans = n + 1; 9 for (int y = 0; y <= n; ++y)//其实就是对前缀和队列p进行遍历 10 { 11 while (!c.empty() && p[y] <= p[c.back()])//性质一 12 { 13 c.pop_back(); 14 } 15 while (!c.empty() && p[y] - p[c.front()] >= K)//性质二 16 { 17 ans = min(ans, y - c.front());//寻找y-opt(y)最小值 18 c.pop_front(); 19 } 20 c.push_back(y); 21 22 } 23 return ans <= n ? ans : -1;
参考:
https://leetcode-cn.com/problems/shortest-subarray-with-sum-at-least-k/solution/he-zhi-shao-wei-k-de-zui-duan-zi-shu-zu-by-leetcod/