树状数组 + 离散化 + 前缀和

4316. 合适数对 - AcWing题库

通过数据范围推断复杂度应该是O(nogn)

看到静态求和,可以联想到前缀和,于是我们先求出整个序列的前缀和数组sum。

这时脑中顿时蹦出一个想法:枚举右端点,然后二分出第一个满足条件的左端点。

二分的判断条件:sumr−suml−1<t
可惜啦,由于有负数的存在,单调性并不满足,所以二分是假做法。

怎么办呢?

我们可以重新审视满足答案要求的公式:

sumr − sum(l−1) <  t 

我们可以发现对于每一个枚举的右端点R,sumr和t的值是恒定的,不会改变,那么我们能不能用一个数组p来记录每一个sumL的值的个数,通过前缀和的方式得出满足条件的L的个数。很显然,是不可以的,或者说不能直接通过前缀和实现,因为在我们枚举到一个右端点R的时候,数组p中不能端点R后面的值,因为L必须在R的左侧,否则就会有重复,所以说这个数组p是动态变化的,只有当前端点R已经遍历过了,才可以在数组p中加入sumr,作为下一个端点R的sumL,因此前缀和也是动态变化的。但树状数组和线段树可以解决此类动态求前缀和的问题。

正解:在值域上维护一个树状数组,维护前缀和数值的个数的和,枚举每个右端点r,每次将答案累加上树状数组中大于sumr−t的总个数,然后再给sumr 这个位置的个数加上1,方便后续的计算

由于值域很大很大,树状数组开不下,所以我们要先将前缀和数组离散化

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 400010;

typedef long long LL;

int n, tr[N];
LL m, s[N];
vector<LL> nums;

int get(LL x)
{
    return lower_bound(nums.begin(), nums.end(), x) - nums.begin() + 1;
    //+1是确保梳状数字下标从1开始
}

int lowbit(int x)
{
    return x & -x;
}

void add(int x, int v)//树状数组修改操作
{
    for(int i = x; i < N; i += lowbit(i))
        tr[i] += v;
}

int query(int x)//树状数组查询操作
{
    int res = 0;
    for(int i = x; i ; i -= lowbit(i))
        res += tr[i];
    return res;
}

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++ )
    {
        LL x;  cin >> x;
        s[i] = s[i - 1] + x;
    }
    
    for(int i = 0; i <= n; i ++ )//将所有前缀和以及后续询问用到的数值全部离散化
    {
        nums.push_back(s[i]);
        nums.push_back(s[i] - m);
    }
    
    sort(nums.begin(), nums.end());//离散化之前一定要线排序
    nums.erase(unique(nums.begin(), nums.end()), nums.end());//离散化之去重

    LL res = 0;//结果可能爆int
    //对于每一个R,L可以取得的范围是1~R,那么对应与前缀和下标(L-1)就是0~R-1
    //所以在R=1之前我们要把L=0时的值s[0]加入里面
    add(get(s[0]), 1);
    for(int i = 1; i <= n; i ++ )
    {
        //大于s[i]-m的个数就是总个数减去小于等于s[i]-m的个数
        res += i - query(get(s[i] - m));
        add(get(s[i]), 1);//加入到前缀和当中
    }
    
    cout << res << endl;
    
    return 0;
}

posted @ 2022-05-05 08:41  光風霽月  阅读(27)  评论(0编辑  收藏  举报