解法一:分治 O(NlogN)
这个题用分治做其实很难想。对于求区间和问题,前缀数组很有用。但这个题要求所有区间范围,如果枚举需要
O
(
N
2
)
O(N^2)
O(N2),其实也可以接受。
用分治的话,其实就是想明白了一件事,只要确保i < j,i,j在哪个位置其实是不影响结果的,因为我们只需要求区间个数。这就很好办了,分治就是划分,然后处理两个有序数组。利用分治可以很容易得到所有上述数组,用O(N)时间计算区间个数+合并即可。
在计算区间个数时,可以用到双指针。对第二个有序数组,咱们可以用双指针确定它的最小界和最大界。对第一个数组的每个元素都计算这样的界限即可。由于有序的特性,指针只能向右移动,故很有效。
也可以用二分来查找上界和下界,但对于有序多次查找,双指针更快。
typedef long long ll;
class Solution {
ll lower, upper;
public:
void msort(vector<ll>& v, int& sum, int l, int r)
{
if(l == r)
{
if(v[l] >= lower && v[l] <= upper)sum += 1;
return;
}
int mid = l + (r-l)/2;
msort(v, sum, l, mid);
msort(v, sum, mid+1, r);
// merge
int p1 = mid+1, p2 = mid+1;
for(int p0=l;p0<=mid;p0++)
{
while(p1 <= r && v[p1]-v[p0] < lower)p1++;
while(p2 <= r && v[p2]-v[p0] <= upper)p2++;
sum += p2-p1;
}
inplace_merge(v.begin()+l, v.begin()+mid+1, v.begin()+r+1);
}
int countRangeSum(vector<int>& nums, int lower, int upper) {
if(nums.size() == 0)return 0;
this->lower = lower;
this->upper = upper;
int sum = 0;
vector<ll> pref{nums[0]};
for(int i=1;i<nums.size();i++)
pref.push_back(nums[i]+pref[i-1]);
msort(pref, sum, 0, pref.size()-1);
return sum;
}
};
解法二:平衡二叉树
在解法一中,在得到前缀数组后,我们对于任何一个数,希望找到他的上界和下界,从而求出区间个数。解法一通过归并排序实现了查找。但平衡二叉树其实就是为这个问题而设计的。利用multiset,我们很容易用少量代码解决这个问题。
还是要利用解法一中分析的分割问题,这次,我们只需一个一个考虑。对每一个新元素,考虑已放入数组的上界和下界,相减即可。
这个算法很慢是因为muliset的迭代器不支持随机访问,相减是O(N)的,导致这个算法又变成平方复杂度。所以,用平衡树实际是不可行的。超时不是因为迭代器出bug,是因为真的超时了。一定要用的话可以用二分找到位置,相减即可。
typedef long long ll;
class Solution {
public:
int countRangeSum(vector<int>& nums, int lower, int upper) {
if(nums.size() == 0)return 0;
vector<ll> pref{nums[0]};
for(int i=1;i<nums.size();i++)
pref.push_back(nums[i]+pref[i-1]);
// 查找
int ret = 0;
multiset<ll> s;
for(auto i: pref)
{
if(i >= lower && i <= upper)ret += 1;
ll s0 = i-upper, s1 = i-lower;
auto it1 = s.lower_bound(s0), it2 = s.upper_bound(s1);
ret += distance(it1, it2);
s.insert(i);
}
return ret;
}
};
解法后续:线段树、树状数组
后面补上。