给定一个整数数组 nums,返回区间和在 [lower, upper] 之间的个数,包含 lower 和 upper。
区间和 S(i, j) 表示在 nums 中,位置从 i 到 j 的元素之和,包含 i 和 j (i ≤ j)。
说明:
最直观的算法复杂度是 O(n2) ,请在此基础上优化你的算法。
示例:
输入: nums = [-2,5,-1], lower = -2, upper = 2,
输出: 3
解释: 3个区间分别是: [0,0], [2,2], [0,2],它们表示的和分别为: -2, -1, 2。
来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/count-of-range-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
二分查找 / 插入
我们维护一个有序数组,表示当前的已知的区间和,然后每次向右推进右端点,我们得到一个新的前缀和,因为区间之和是 sum[r] - sum[l-1]
,目标变为:
- 已知当前区间右端点前缀和 sr,找到所有左端点 sl,使得他们满足
lower<= sr - sl <=upper
那么有 sl 的范围:[sr-upper, sr-lower]
在已知的,升序存放sl值的数组中,二分查找:
找第一个 i 使得 sl大于等于 sr-upper
找第一个 j 使得 sl 大于 sr-lower
j-i 就是当前 sr(右端点)可以组成的逆序对个数
然后再把 sr 加入 sl 数组(插入位置也要二分搜索得到),准备为下一个sr做准备
很遗憾不能过
class Solution {
public:
int countRangeSum(vector<int>& nums, int lower, int upper)
{
vector<long long> sumArr{0};
long long ans=0, sum=0;
for(int i=0; i<nums.size(); i++)
{
sum += nums[i];
auto l = lower_bound(sumArr.begin(), sumArr.end(), sum-upper);
auto r = upper_bound(sumArr.begin(), sumArr.end(), sum-lower);
ans += r-l;
auto x = lower_bound(sumArr.begin(), sumArr.end(), sum);
sumArr.insert(x, sum);
}
return (int)ans;
}
};
引入multiset优化,防止退化
因为最坏情况维护的二分查找数组,查找时可能会退化到O(n),而不是稳定O(log(n)),为了维护稳定的O(log(n)),我们需要使用**红黑树(multiset)**代替之前的有序数组,进行二分查找
终于AC了
class Solution {
public:
int countRangeSum(vector<int>& nums, int lower, int upper)
{
multiset<long long> sumSet{0};
long long ans=0, sum=0;
for(int i=0; i<nums.size(); i++)
{
sum += nums[i];
auto l = sumSet.lower_bound(sum-upper);
auto r = sumSet.upper_bound(sum-lower);
ans += distance(l, r);
sumSet.insert(sum);
}
return (int)ans;
}
};
正解:逆序对
其实就是求解前缀和数组 nums 中,找 i, j 使得 i < j 且满足lower<= nums[j] - nums[i] <= upper
的有多少对,仔细一看,那就是有点像【逆序对问题啊】,这种找一对数,满足一定的大小关系(A[i] + bias < or > A[j])的
确定大小关系
即找nums[i] 满足 nums[j]-upper <= nums[i] <= nums[j]-lower
问题拆解为:
找nums[i1] 使得 nums[j]-upper <= nums[i1]
找nums[i2] 使得 nums[i2] <= nums[j]-lower
问题转化为:
如果将数组拆分两半,每一半都升序(归并递归调用后),在左边确定i1, i2
- 找第一个 i1 使得
nums[i1] < nums[j]-upper
(因为递增,i1右边全都是nums[i1] >= nums[j]-upper
,合法的,i1 及其左边都是不合法的) - 找第一个 i2 使得
nums[i2] <= nums[j]-lower
(i2 及其左边都是合法的)
i2 - i1 就是满足两个边界的答案,如图证明:
注意
从大到小枚举 j 下标,因为要在升序序列中nums[i]< nums[j]
的,如果从小到大枚举 j,因为 i 不走回头路,那么 nums[j] 变大可能导致之前 i 向前迭代而排除的一些nums[i]变得可用。
class Solution {
public:
int mer(vector<long long>& nums, int l, int r, int lower, int upper)
{
if(l>=r || l<0 || r>=nums.size()) return 0;
int mid=(l+r)/2, cnt=0, i1=mid, i2=mid;
cnt += mer(nums, l, mid, lower, upper)+mer(nums, mid+1, r, lower, upper);
for(int j=r; j>mid; j--)
{
while(i1>=l && nums[i1]>=nums[j]-upper) i1--;
while(i2>=l && nums[i2]>nums[j]-lower) i2--;
cnt += i2-i1;
}
inplace_merge(nums.begin()+l, nums.begin()+mid+1, nums.begin()+r+1);
return cnt;
}
int countRangeSum(vector<int>& nums, int lower, int upper)
{
vector<long long> NUMS(nums.size()+1); NUMS[0]=0;
for(int i=0; i<nums.size(); i++)
NUMS[i+1]=nums[i]+NUMS[i];
return mer(NUMS, 0, NUMS.size()-1, lower, upper);
}
};