树状数组求逆序对_区间和的个数(树状数组)

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的位置所代表的数值。

5f9ab941d3c1bda9556a5005726f71a8.png

866222bd104acaf2b0551c371ca1b4f2.png

C1, C3, C5, C7的下标的二进制表示 最后一位都是1,放在第一层;C2, C6的下标的二进制表示都为10,放在第二层。

子节点和其父节点的关系:

子节点下标X + lowbit(X) = 父节点下标 Y

举例:2+lowbit(2)=4;  3+lowbit(3)=4

9966b8f27d31d829f40c8683f669ae5d.png

修改单点的值,只需要更新覆盖其值的数组元素就行啦。举例,修改单点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);    }}

查询前缀和

ad98ef4769461a34823cb5317a454696.png

举例,计算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]。

5ad34442208d018fecd3cc8e00b9c89b.png

eeedf122b219bcdef9a17e81110f4779.png

注意: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]。离散化构造线段树,此处展示的是非离散化的线段树构造。

b7e4fa0cb787e399320e00ce5883d1d7.png

倒序插入prefixsum[5]=-1,

fa3bf9c450fb06c7e8440e6c7de937a3.png

这时查找区间为[-3+prefixsum[5-1], -1+prefixsum[5-1]]=[-3,-1]。此时满足等式的只有 j=5,故此步res=1。

倒序插入prefixsum[4]=0,

acebb2105fc97be3bb7126f7dc317ebd.png

这时查找区间为[-3+prefixsum[4-1], -1+prefixsum[4-1]]=[-5,-3]。此时没有满足等式的情况,故此步res=0。

倒序插入prefixsum[3]=-2,

ca28838d5a3da98e2a606727af71b4de.png

这时查找区间为[-3+prefixsum[3-1], -1+prefixsum[3-1]]=[-3,-1]。此时满足等式的情况有 j=3 和 j=5,故此步res=2。

一次类推,一直计算到 插入prefixsum[0]的情况,最后的结果为各步res之和。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值