算法竞赛进阶指南-第五章-树状数组

算法竞赛进阶指南-第五章-树状数组

- 8 mins

树状数组

image-20230303205616856

下面这段代码可以计算出区间[1,x]分成的O(logx)个小区间

while(x > 0)
{
    printf("[%d,%d]\n",x - (x & -x) + 1, x);
    x -= x & -x;
}

image-20230303210010214

树状数组支持的基本操作有两个

查询前缀和,即序列a第1~x个数的和(O(logN))

int ask(int x)
{
    int ans = 0;
    for(; x; x -= x & -x) ans += c[x];
    return ans;
}

单点增加,即给序列中的一个数a[x]加上y,同时正确维护序列的前缀和(O(logN))

void add(int x, int y)
{
    for(; x <= N; x += x & -x) c[x] += y;
}

在执行所有操作之前,我们需要对树状数组进行初始化,针对原始序列a构造一个树状数组.

为了简便,一般的初始化方法是:直接建立一个全为0的数组c,然后对每个位置x执行add(x,a[x])时间复杂度为(O(NlogN))

更高效的初始化方法是:从小到大依次考虑每个节点x,借助lowbit运算扫描它的子节点并求和时间复杂度为(O(N))

树状数组与逆序对

image-20230303211611528

刚开始初始化为全零,然后倒序扫描序列a,查询前缀和,即比这个数小的且在它后面的数量,相加后,再将其本身数值对应出现次数加1.然后继续查找

楼兰图腾

以横坐标为标准排序,得到序列a

倒叙扫描序列a,通过树状数组得到每个a[i]后面有几个数比它大,即为right[i].

正序扫描序列a,通过树状数组得到每个a[i]前面有几个数比它大,即为left[i]

依次枚举每个点作为中心点,以该点为中心的v图腾个数为left[i]*right[i]

因为横纵坐标都不相同,所以a[i]后面比它小的数为n-i-right[i]

同理,a[i]前面比它小的数为i-1-left[i]

依次枚举每个点为中心点,以该点为中心的^图腾个数也可得到

#include <bits/stdc++.h>
using namespace std;
int n;
int y[200005], c[200005];
int ask(int x)
{
    int ans = 0;
    for (; x; x -= x & -x)
        ans += c[x];
    return ans;
}
void add(int x, int y)
{
    for (; x <= n; x += x & -x)
        c[x] += y;
}
long long left_v[200005], leftt[200005], right_v[200005], rightt[200005];
int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(0);
    cin >> n;

    for (int i = 1; i <= n; i++)
    {
        cin >> y[i];
    }
    for (int i = n; i > 0; i--)
    {
        right_v[i] = ask(y[i] - 1);
        rightt[i] = n - i - right_v[i];
        add(y[i], 1);
    }
    memset(c, 0, sizeof(y));
    for (int i = 1; i <= n; i++)
    {
        left_v[i] = ask(y[i] - 1);
        leftt[i] = i - 1 - left_v[i];
        add(y[i], 1);
    }
    int64_t ans1 = 0, ans2 = 0;
    for (int i = 1; i <= n; i++)
    {
        ans1 += left_v[i] * right_v[i];
        ans2 += leftt[i] * rightt[i];
    }
    cout << ans2 << ' ' << ans1;
    return 0;
}

思路呢,就是上面说的那么个思路,不过要注意的是,它数据范围里强调了int64_t,也就是相当于long long,所以要把前缀和和答案的数据类型都改为long long

树状数组的扩展应用

A Tiny Problem with Integers

利用差分的思想,将区间加和转化为两点加和,查询单点数值则转化为求前缀和

image-20230304144819934

#include <bits/stdc++.h>
using namespace std;
int n, m;
char c;
int l, r, d, x;
int dd[100005];
int a[100005];
int ask(int z)
{
    int ans = 0;
    for (; z; z -= z & -z)
        ans += dd[z];
    return ans;
}
void add(int z, int y)
{
    for (; z <= n; z += z & -z)
        dd[z] += y;
}
int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(0);
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }
    while (m--)
    {
        cin >> c;
        if (c == 'C')
        {
            cin >> l >> r >> d;
            add(l, d);
            add(r + 1, -d);
        }
        else
        {
            cin >> x;
            cout << ask(x) + a[x] << '\n';
        }
    }
}
A Simple Problem with Integers

image-20230304154923763

因此,我们增加一个树状数组,用于维护i*b[i]的前缀和(c0用来存储b[i],c1用来存储b的前缀和)

用sum存储a的原始前缀和image-20230304160756144

#include <bits/stdc++.h>
using namespace std;
const int SIZE = 100010;
int a[SIZE], n, m;
long long c[2][SIZE], sum[SIZE];//这里是定义c0和c1以及sum三个数组
long long ask(int k, int x)//这里是把两个ask合并后的函数
{
    long long ans = 0;
    for (; x; x -= x & -x)
        ans += c[k][x];
    return ans;
}
void add(int k, int x, int y)//同上
{
    for (; x <= n; x += x & -x)
        c[k][x] += y;
}
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        sum[i] = sum[i - 1] + a[i];
    }
    while (m--)
    {
        char op[2];
        int l, r, d;
        cin >> op >> l >> r;
        if (op[0] == 'C')//判断是C还是Q
        {
            cin >> d;
            add(0, l, d);
            add(0, r + 1, -d);
            add(1, l, l * d);
            add(1, r + 1, -(r + 1) * d);
        }
        else
        {
            long long ans = sum[r] + (r + 1) * ask(0, r) - ask(1, r);
            ans -= sum[l - 1] + l * ask(0, l - 1) - ask(1, l - 1);
            cout << ans << '\n';
        }
    }
}
Lost Cows

如果第k头牛前有Ak头比它低,那么它的身高Hk是数值1~n中第Ak+1小的没有在后面中出现过的数

因此,我们可以倒序扫描序列a,从最后一个开始,给该牛选择可用的第Ak+1矮的高度

具体操作,先建立一个01序列b,令其初值全为1,并用树状数组c维护b,以方便得到b的前缀和

然后倒序扫描a,对每个A做出下列操作

​ 查询第Ai+1个1在什么位置,令该牛的高度等于该位置

​ 把该位置的”1”改为0

关于具体的查询方法,可以使用树状数组+倍增

​ 开始时,初始化两个变量ans和sum,令其均为0

​ 从logn向下取整到0倒序考虑每个整数:image-20230304175727013

这里的原因是image-20230304175822704

结合树状数组的图,我们可以发现,16->8->4->2->1这条线是随2次幂指数依次降低的结果

第一步,假设p为4,则sum+c[ans+2^p]为整个b数组的前缀和,不会小于k

假设p为3,则sum+c[ans+2^p]为b数组前八位的前缀和,如果小于,则相加,然后看9~12,如果大于,则考虑1~4

也就是说,不论大于还是小于,都会考虑下一层,所以是倒序考虑每个整数,当p为0时,也就意味着考虑到了单个元素,那么对应的ans也就是第Ai+1个1的前一个(因为如果权值为0,那么也会相加,所以最后返回的ans一定是Ai+1个1的前一个),即Hi = ans + 1