327. 区间和的个数
给定一个整数数组 nums,返回区间和在 [lower, upper] 之间的个数,包含 lower 和 upper。
区间和 S(i, j) 表示在 nums 中,位置从 i 到 j 的元素之和,包含 i 和 j (i ≤ j)。
说明:
最直观的算法复杂度是 O(n^2) ,请在此基础上优化你的算法。
示例:
输入: nums = [-2,5,-1], lower = -2, upper = 2,
输出: 3
解释: 3个区间分别是: [0,0], [2,2], [0,2],它们表示的和分别为: -2, -1, 2。
***************** 前缀和 *******************
区间和问题,一般可以联想到前缀和
对于数组nums[n],其前缀和为:
sum[i]=sum[i-1]+nums[i-1]
(sum[0]=0; sum[1]=nums[0]; sum[2]=nums[0]+nums[1])
解法1: 暴力法
对于每一个i,求区间[0,i-1],[1,i-1],……,[i-1,i-1]是否满足条件
class Solution {public: int countRangeSum(vector<int>& nums, int lower, int upper) { int len=nums.size(); vector<int> Sum(len+1,0); int presum=0,cnt=0; for(int i=1;i<=len;i++) { presum += nums[i-1]; for(int j=1;j<=i;j++) { if(presum-Sum[j-1]>=lower && presum-Sum[j-1]<=upper) cnt++; } Sum[i]=presum; } return cnt; }};
时间复杂度:O(n^2)
空间复杂度:O(n),Sum数组用来存储前缀和。
解法2: 树状数组
用来解决动态前缀和问题,解决区间加/单点查询问题
查询复杂度为O(logN)
Lowbit函数:返回值是 入参转化为二进制后,最后一个1的位置所代表的数值。
C1, C3, C5, C7的下标的二进制表示 最后一位都是1,放在第一层;C2, C6的下标的二进制表示都为10,放在第二层。
子节点和其父节点的关系:
子节点下标X + lowbit(X) = 父节点下标 Y
举例:2+lowbit(2)=4; 3+lowbit(3)=4
修改单点的值,只需要更新覆盖其值的数组元素就行啦。举例,修改单点A2,则需要更新C2, C4, C8, C16的值(注意这里4是2+lobit(2), 8是4+lobit(4))。
故修改单点值的代码
void add(int x,int delta){ while(x<=n)//n为树状数组的长度 { C[x]+=delta; x+=lowbit(x); }}
查询前缀和
举例,计算Sum13=A1+A2+……+A13
只需要计算C13+C12+C8
C13, C12 , C8 可以理解为:13 ....... 二进制表示为 1101
lowbit(13)=1, 13-lowbit(13)=12 ....... 二进制表示为 1100
lowbit(12)=4, 12-lowbit(12)=8 ....... 二进制表示为 1000
故查询前缀和的代码
int query(int x){ int res=0; while(x) { res+=C[x];//C[x]为树状数组元素 x-=lowbit(x); } return res;}
树状数组的初始化:
public FeWickTree(int n){ this.len=n; this.tree=new int[n+1];}public FeWickTree(int[] nums){ this.len=nums.length; this.tree=new int[this.len+1]; for(int i=0;i<this.len;i++) { update(i,nums[i]); }}void update(int i,int delta){ while(i<=this.len) { tree[i]+=delta; i+=lowbit(i); }}
此题树状数组题解:
typedef long long LL;class Solution {public: vector data; vector<int> tr; // 查找小于等于x的第一个数的下标,找不到返回0 int binary_search1(LL x) { int l = 0, r = data.size() - 1; while (l < r) { int mid = l + r + 1 >> 1; if (data[mid] <= x) l = mid; else r = mid - 1; } if (l == 0 && data[0] > x) return 0; else return l + 1; } // 查找小于x的第一个数的下标,找不到返回0 int binary_search2(LL x) { int l = 0, r = data.size() - 1; while (l < r) { int mid = l + r + 1 >> 1; if (data[mid] < x) l = mid; else r = mid - 1; } if (l == 0 && data[0] >= x) return 0; else return l + 1; } int lowbit(int x) { return x & -x; } int find(int x) { int ans = 0; for (int i = x; i; i -= lowbit(i)) ans += tr[i]; return ans; } void add(int x, int c) { int n = tr.size() - 1; for (int i = x; i <= n; i += lowbit(i)) tr[i] += c; } int countRangeSum(vector<int>& nums, int lower, int upper) { int n = nums.size(); LL sum = 0; vector s(n); for (int i = 0; i < n; i ++) { sum += nums[i]; s[i] = sum; } data = s; data.push_back(0); sort(data.begin(), data.end()); data.erase(unique(data.begin(), data.end()), data.end()); tr = vector<int>(data.size() + 1); int ans = 0; int idx_zero = binary_search1(0); add(idx_zero, 1); for (auto x : s) { LL a = x - upper, b = x - lower; int idxa = binary_search2(a), idxb = binary_search1(b); int idxx = binary_search1(x); if (idxb != 0) { if (idxa == 0) ans += find(idxb); else ans += find(idxb) - find(idxa); } add(idxx, 1); } return ans; }};
解法3: 归并排序
前置知识点:求逆序对
对于数组a,若对于ia[j]。则a[i]与a[j]构成逆序对。
求解逆序对个数经典的解法是归并排序。思路如下:
对于数组a[low,high),若 a[low,mid) 和 a[mid,high) 都已归并有序,则
对于左半区间 a[low,mid) 的每个元素a[left],计算 a[mid,high) 有多少个元素小于它。实现的代码模板如下:
//O(n),其中 n=hi-loint right=mid;for(int left=low;left{ //向右移动right指针,直到 a[right]>=a[left] while(right!=high && a[left]>a[right]) right++; //比a[left]小的元素有 right-mid 个 count += right-mid;}//因为左半区间和右半区间都已经归并,因此 left 越往右越大//故 right 无需每次都从 mid 开始
求解此题时,分两步:
1)求前缀和得到sums
2)计算前缀和数组中,当 i < j 时,sums[j]-sums[i]的值在[lower,higher]之间。
求解逆序对的个数是该题的一个特例,也就是计算nums[j]-nums[i]<0的个数。
class Solution {public: void mergeResult(vector<int>& sums,int lo,int hi,int mid)//归并部分代码实现{ vector<int> tmp(hi-lo,0); int k=0,left=lo,right=mid; while(left { if(sums[left] tmp[k++]=sums[left++]; else tmp[k++]=sums[right++]; } if(left==mid) { while(right tmp[k++]=sums[right++]; } if(right==hi) { while(left tmp[k++]=sums[left++]; } for(int i=lo,m=0;i sums[i]=tmp[m++]; } int merge_sort(vector<int>& sums,int lo,int hi,int lower,int upper){ if(hi-lo<=1) return 0; int mid=(lo+hi)>>1; int count=merge_sort(sums,lo,mid,lower,upper)+merge_sort(sums,mid,hi,lower,upper); int right1=mid,right2=mid; for(int left=lo;left { // 统计右侧-nums[left] < lower 的个数 while(right1!=hi && sums[right1]-sums[left] right1++; // 统计右侧-nums[left] <= upper 的个数 while(right2!=hi && sums[right2]-sums[left]<=upper) right2++; // 因此右侧-nums[left]的差在 [lower,upper] 的个数为: //count += (right2 - mid) - (right1 - mid); 可以简写为下面这样: count += right2-right1; } //该函数可以C++函数替换(inplace_merge 原地归并) //inplace_merge(nums.begin() + lo, nums.begin() + mid, nums.begin() + hi); mergeResult(sums,lo,hi,mid); return count; } int countRangeSum(vector<int>& nums, int lower, int upper) { int len=nums.size(); vector<int> sums(len+1,0); for(int i=1;i<=len;i++) { //计算前缀和 sums[i]=sums[i-1]+nums[i-1]; } return merge_sort(sums,0,len+1,lower,upper); }};
解法4: 前缀和+线段树
用线段树可将时间复杂度降至为O(NlogN)。题目要求lower<=sum(i, j)<= upper,sum(i, j)=prefixsum(j) - prefixsum(i-1),那么lower+prefixsum(i-1)<=prefixsum(j)<=upper+prefixsum(i-1)。所以利用前缀和将区间和转换成了前缀和在线段树中 query 的问题,只不过线段树中父节点中存的不是子节点的和,而是子节点出现的次数。另外,由于前缀和会很大,所以需要离散化。举例,prefixsum=[-3,-2,-1,0],用前缀和下标进行离散化,所以线段树中左右区间变成[0,3]。
注意:prefixsum 计算完后需要去重,去重后并排序,方便构造线段树。最后一步往线段树中倒叙插入 prefixsum 的时候,用的是非去重的。例如往线段树中插入prefixsum[5],query操作实际是做区间匹配,即lower<=sum(i, j)<=upper
举例:nums[6]=[-3, 1, 2, -2, 2, -1],lower=-3, upper=-1, prefixsum=[-3, -2, 0, -2, 0, -1],去重以后并排序得到sum=[-3,-2,-1,0]。离散化构造线段树,此处展示的是非离散化的线段树构造。
倒序插入prefixsum[5]=-1,
这时查找区间为[-3+prefixsum[5-1], -1+prefixsum[5-1]]=[-3,-1]。此时满足等式的只有 j=5,故此步res=1。
倒序插入prefixsum[4]=0,
这时查找区间为[-3+prefixsum[4-1], -1+prefixsum[4-1]]=[-5,-3]。此时没有满足等式的情况,故此步res=0。
倒序插入prefixsum[3]=-2,
这时查找区间为[-3+prefixsum[3-1], -1+prefixsum[3-1]]=[-3,-1]。此时满足等式的情况有 j=3 和 j=5,故此步res=2。
一次类推,一直计算到 插入prefixsum[0]的情况,最后的结果为各步res之和。