题目描述
返回 A
的最短的非空连续子数组的 长度,该子数组的和至少为 K
。
如果没有和至少为 K
的非空子数组,返回 -1
。
样例
输入:A = [1], K = 1
输出:1
输入:A = [1,2], K = 4
输出:-1
输入:A = [2,-1,2], K = 3
输出:3
注意
1 <= A.length <= 50000
-10 ^ 5 <= A[i] <= 10 ^ 5
1 <= K <= 10 ^ 9
算法1
(树状数组) O(nlogn)
- 构造前缀和数组 s(i),对于每个 i,找到下标最大的 j, (j<i),使得 s(j)+K≤s(i),则以 i 结尾的最小的答案就是 i−j。
- 将所有 0,s(i),K,s(i)+K 进行离散化,离散化到 [1,2n+2],然后使用权值树状数组寻找最大的 j。
- 具体地,在对应 s 值的树状数组位置上更新最大值。查询时从树状数组中寻找一个前缀最大值,然后再更新树状数组即可。
时间复杂度
- 每次更新和查询的时间复杂度为 O(logn),故总时间复杂度为 O(nlogn)。
C++ 代码
class Solution {
public:
void update(vector<int> &f, int x, int y) {
for (; x < f.size(); x += x & -x)
f[x] = max(f[x], y);
}
int query(vector<int> &f, int x) {
int t = -1;
for (; x; x -= x & -x)
t = max(f[x], t);
return t;
}
int shortestSubarray(vector<int>& A, int K) {
int n = A.size();
vector<int> s(2 * n + 2, 0), d(2 * n + 2, 0);
vector<int> f(2 * n + 3, -1);
for (int i = 1; i <= n; i++) {
s[i] = s[i - 1] + A[i - 1];
d[i] = s[i];
}
for (int i = n + 1; i <= 2 * n + 1; i++) {
s[i] = s[i - n - 1] + K;
d[i] = s[i];
}
sort(d.begin(), d.end());
for (int i = 0; i <= 2 * n + 1; i++)
s[i] = lower_bound(d.begin(), d.end(), s[i]) - d.begin() + 1;
update(f, s[n + 1], 0);
int ans = n + 1;
for (int i = 1; i <= n; i++) {
int t = query(f, s[i]);
if (t != -1)
ans = min(ans, i - t);
update(f, s[i + n + 1], i);
}
if (ans == n + 1)
ans = -1;
return ans;
}
};
算法2
(单调队列) O(n)
- 求出前缀和数组 s。
- 对于某个 s(i),我们期望有一个单调的队列,从 j=q.front() 开始,在满足 s(j)+K≤s(i) 的情况下一直向后移动 j,直到条件不满足,此时 s(i) 所能更新的答案就是 i−j。
- 现在考察这个队列的性质,为什么队列一定是单调递增的?这是因为如果出现了递减的情况,后一个元素一定比前一个元素更优(值更小更容易满足 s(j)+K≤s(i),且 j 更靠近 i)所以递减时,前一个元素是没有意义的。
- 根据这个性质,每次 s(i) 入队时,维护队列的单调性。
- 在每次 j 从队头遍历的过程中,是不是被遍历过元素可以丢弃呢?可以。这是因为被遍历的元素是不会再产生更优的元素了,下一次的 i 一定更大,与这个 j 无论如何都不会凑出更小的答案。
时间复杂度
- 每个元素最多进队一次,出队一次,故时间复杂度为 O(n)。
空间复杂度
- 需要 O(n) 的额外空间存储前缀和数组和单调队列。
C++ 代码
class Solution {
public:
int shortestSubarray(vector<int>& A, int K) {
int n = A.size();
vector<int> s(n + 1, 0);
deque<int> q;
for (int i = 1; i <= n; i++)
s[i] = s[i - 1] + A[i - 1];
q.push_back(0);
int ans = n + 1;
for (int i = 1; i <= n; i++) {
while (!q.empty() && s[q.front()] + K <= s[i]) {
ans = min(ans, i - q.front());
q.pop_front();
}
while (!q.empty() && s[q.back()] >= s[i])
q.pop_back();
q.push_back(i);
}
if (ans == n + 1)
ans = -1;
return ans;
}
};
max over int 不是group啊。我之前没想过这种树桩数组的用法因为他不是群 就没有办法段查询。竟然还有这种可以只用1-k查询的题目。世界观破碎了!
在这里提供一个 0-base的代码吧~~
struct BIT_t { std::vector<int> T; int n; BIT_t(int _n) : n(_n + 1), T(_n + 1, -1) {} auto lowbit(int x) { return x & (-x); } auto point_update(int i, int val) -> void { for (++i; i < n; i += lowbit(i)) { T[i] = std::max(val, T[i]); } } auto range_query(int i) -> int { int acc = -1; for (++i; i > 0; i -= lowbit(i)) { acc = std::max(T[i], acc); } return acc; } }; class Solution { public: int shortestSubarray(vector<int>& A, int K) { const int n = std::size(A); const auto pfsum = [&]() { auto acc = std::vector<int>{}; std::partial_sum(std::begin(A), std::end(A), std::back_inserter(acc)); return acc; }(); const auto all_nums = [&]() { auto acc = std::set<int>{}; for (const int x : pfsum) { acc.emplace(x); acc.emplace(x + K); } return std::vector<int>(std::begin(acc), std::end(acc)); }(); struct cdmap_t { mutable std::vector<int> i2v; mutable std::unordered_map<int, int> v2i; cdmap_t() = default; explicit cdmap_t(const std::vector<int>& arr) { for (int i = 0; i < std::size(arr); ++i) { i2v.emplace_back(arr[i]); v2i[arr[i]] = i; } } int operator[](int id) const { return i2v[id]; } int operator()(int val) const { return v2i[val]; } } cdmap(all_nums); struct state_t { mutable int i = 0; mutable BIT_t BIT; mutable int acc = INT_MAX; } st { .BIT = BIT_t(int(std::size(all_nums))) }; for (auto &[i, BIT, acc] = st; i < n; ++i) { const int t = BIT.range_query(cdmap(pfsum[i])); if (t != -1) { acc = std::min(acc, i - t); } if (pfsum[i] >= K) { acc = std::min(acc, i + 1); } BIT.point_update(cdmap(pfsum[i] + K), i); } return st.acc == INT_MAX ? -1 : st.acc; } };
请问一下如果这个题改成求区间和不大于K的最长区间长度是不是就不能用单调队列来做了
直接双指针就可以了
是那种right指针先往前跑,如果和大于K 在让left往前走吗,但是如果数组里面有负数怎么办呢
有正有负的话,同样维护一个单调递增的前缀和辅助数组
对于每个 s(i) 在辅助数组上二分,然后 s(i) 和辅助数组的末尾元素比较,如果大于辅助数组末尾,则插入。
其实还有一系列的镜像问题:
区间和不大于 K 的最短区间长度,区间和不大于 K 的最长区间长度
区间和至少为 K 的最短区间长度,区间和至少为 K 的最长区间长度
最接近 K 的区间和是多少
树状数组应该算是个通用解法
期待大佬什么时候有时间写一个这一系列问题的总结分享
能不能解释下这里树状数组维护的f是什么意思,query和update各自目标是什么,没看懂,谢谢了
不是很推荐树状数组的做法。
f
维护的是离散化后的前缀和1 到 s(i)
中的最大的坐标值。query
就是询问这个最大值,update
需要在树状数组上更新离散化后的s(i) + K
这个位置为i
。