算法竞赛进阶指南——0x42【树状数组】

本文详细介绍了树状数组(又称二分指数表)的基础概念,包括lowbit运算、树状数组的性质、基本操作如区间查询和单点更新。同时,通过实例展示了树状数组在解决逆序对问题中的应用,并探讨了二维树状数组的使用,用于处理涉及多个变量的问题。此外,还提供了AC代码和TLE代码的对比,以加深理解。
摘要由CSDN通过智能技术生成


在这里插入图片描述

回顾lowbit

lowbit运算定义为整数n在二进制下,最低位的以及后面所有的0构成的数值
lowbit(x)是用来跳到x的下一个结点或者上一个结点的的介质
lowbit(n)=n&(~n+1)=n&(-1-n+1)=n&(-n)

这段代码可以求出x在二进制下为1的位数

scanf("%lld", &x);
for (int i = 0; i <= 20; i++)
    H[1 << i] = i;//预处理一个像hash表一样的东西
while (x > 0)
{
    printf("%lld ", H[x & -x]);
    x -= (x & -x);
}

这段代码可以求出区间[1,x]划分的log(x)个小区间

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

树状数组

c[i]保存的是区间[x-lowbit(x)+1,x]中所有数的和

性质

  • 每个节点c[x]保存以它为根的子树中所有叶节点的和
  • 树的深度为O(log N)
  • 每个内部节点c[x]的子节点个数为lowbit(x)在二进制下的的位数
  • 除树根外,每个内部节点c[x]的父亲节点是c[x+lowbit(x)]

基本操作

树结点的序号都是偶数,奇数的叶子结点都是一个单独的区间
在这里插入图片描述

ll lowbit(ll x) { return x & -x; }//定义lowbit运算

初始化

void init()//初始化,将原始序列a数组构造成树状数组
{
    for (int i = 1; i <= n; i++)
        add(i, a[i]);
}

区间查询

ll ask(ll x)//区间查询
{
    ll res = 0;
    for (; x; x -= lowbit(x))
        res += c[x];
    return res;
}

单点更新

void add(ll x, ll y)//单点增加
{
    for (; x <= N; x += lowbit(x))
        c[x] += y;
}

树状数组与逆序对

牛客一个小栗子楼兰图腾

#pragma GCC optimize(3, "Ofast", "inline")
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
typedef pair<long long, long long> pll;
const int N = 200000 + 1100;
const int M = 2e6 + 1000;
const int inf = 0x3f3f3f3f;
const int mod = (1 << 31) - 1;
#define IOS                  \
    ios::sync_with_stdio(0); \
    cin.tie(0);              \
    cout.tie(0);
ll n, res1, res2;
ll a[N], t[N], r1[N], l1[N], r2[N], l2[N];
ll lowbit(ll x) { return x & -x; }
ll query(ll x)
{
    ll res = 0;
    for (; x; x -= lowbit(x))
        res += t[x];
    return res;
}
void add(ll x, ll y)
{
    for (x; x <= n; x += lowbit(x))
        t[x] += y;
}
int main()
{
    scanf("%lld", &n);
    memset(t, 0, sizeof(t));
    for (int i = 1; i <= n; i++)
        scanf("%lld", &a[i]);
    //右边
    for (int i = n; i >= 1; i--)
    {
        r1[i] = query(n) - query(a[i]); //V
        r2[i] = query(a[i] - 1);        //^
        add(a[i], 1);
    }
    //左边
    memset(t, 0, sizeof(t));
    for (int i = 1; i <= n; i++)
    {
        l1[i] = query(n) - query(a[i]); //V
        l2[i] = query(a[i] - 1);        //^
        add(a[i], 1);
    }
    for (int i = 1; i <= n; i++)
    {
        res1 += (r1[i] * l1[i]);
        res2 += (r2[i] * l2[i]);
    }
    printf("%lld %lld\n", res1, res2);
    return 0;
}

树状数组的扩展应用

牛客一道小栗子A Tiny Problem with intergers
TLE代码,过80

#pragma GCC optimize(3, "Ofast", "inline")
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
typedef pair<long long, long long> pll;
const int N = 200000 + 1100;
const int M = 2e6 + 1000;
const int inf = 0x3f3f3f3f;
const int mod = (1 << 31) - 1;
#define IOS                  \
    ios::sync_with_stdio(0); \
    cin.tie(0);              \
    cout.tie(0);
ll n, m, x, l, r;
ll a[N], ca[N];
int main()
{
    // IOS;
    scanf("%lld%lld", &n, &m);
    for (int i = 1; i <= n; i++)
    {
        scanf("%lld", &a[i]);
        ca[i] = a[i] - a[i - 1];
    }
    while (m--)
    {
        char c;
        scanf(" %c", &c); //scanf是从标准缓冲区中读取输入的字符的;空格就可以抵消那个回车;或者写两个getchar()
        // fflush(stdin);    //语句来清空缓冲区
        if (c == 'Q')
        {
            scanf("%lld", &x);
            printf("%lld\n", a[x]);
        }
        else if (c == 'C')
        {
            scanf("%lld%lld%lld", &l, &r, &x);
            ca[l] += x, ca[r + 1] -= x;//影响在l处产生,在r+1处消除
            for (int i = 1; i <= n; i++)
                a[i] = a[i - 1] + ca[i];
        }
    }
    return 0;
}

AC代码

#pragma GCC optimize(3, "Ofast", "inline")
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
typedef pair<long long, long long> pll;
const int N = 200000 + 1100;
const int M = 2e6 + 1000;
const int inf = 0x3f3f3f3f;
const int mod = (1 << 31) - 1;
#define IOS                  \
    ios::sync_with_stdio(0); \
    cin.tie(0);              \
    cout.tie(0);
ll n, m, x, l, r;
ll a[N], ca[N];
ll lowbit(ll x) { return x & -x; }
void add(ll x, ll y)
{
    for (x; x <= n; x += lowbit(x))
        ca[x] += y;
}
ll ask(ll x)
{
    ll res = 0;
    for (; x; x -= lowbit(x))
        res += ca[x];
    return res;
}
int main()
{
    // IOS;
    scanf("%lld%lld", &n, &m);
    for (int i = 1; i <= n; i++)
    {
        scanf("%lld", &a[i]);
        add(i, a[i] - a[i - 1]);//树状数组只是用来维护差值
    }
    while (m--)
    {
        char c;
        scanf(" %c", &c); //scanf是从标准缓冲区中读取输入的字符的;空格就可以抵消那个回车;或者写两个getchar()
        if (c == 'Q')
        {
            scanf("%lld", &x);
            printf("%lld\n", ask(x));//查询单点的值对应的是a数组的差分数组的前缀和
        }
        else if (c == 'C')
        {
            scanf("%lld%lld%lld", &l, &r, &x);
            add(l, x), add(r + 1, -x); //影响在l处产生,在r+1处消除
        }
    }
    return 0;
}

二维树状数组

牛客一道小栗子A Simple Problem with Integers
思想:分离包含有多个变量的项,使公式中不同变量之间相互独立

#pragma GCC optimize(3, "Ofast", "inline")
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
typedef pair<long long, long long> pll;
const int N = 100000 + 1100;
const int M = 2e6 + 1000;
const int inf = 0x3f3f3f3f;
const int mod = (1 << 31) - 1;
#define IOS                  \
    ios::sync_with_stdio(0); \
    cin.tie(0);              \
    cout.tie(0);
ll n, q, l, r, d;
ll a[N], t[2][N], sum[N];
ll lowbit(ll x) { return x & -x; }
void add(ll k, ll x, ll y)//二维树状数组记录的还是变化量而已【差分】
{
    for (x; x <= n; x += lowbit(x))
        t[k][x] += y;
}
ll query(ll k, ll x)//查询的是差分的前缀和,也即原数组的变化量
{
    ll res = 0;
    for (; x; x -= lowbit(x))
        res += t[k][x];
    return res;
}
int main()
{
    // IOS;
    scanf("%lld%lld", &n, &q);
    for (int i = 1; i <= n; i++)
    {
        scanf("%lld", &a[i]);
        sum[i] = sum[i - 1] + a[i];
    }
    while (q--)
    {
        char c;
        scanf(" %c", &c);
        if (c == 'Q')
        {
            scanf("%lld%lld", &l, &r);
            ll a = sum[r] + (r + 1) * query(0, r) - query(1, r);
            ll b = sum[l - 1] + l * query(0, l - 1) - query(1, l - 1);
            printf("%lld\n", a - b);
        }
        else if (c == 'C')
        {
            scanf("%lld%lld%lld", &l, &r, &d);
            add(0, l, d), add(0, r + 1, -d), add(1, l, l * d), add(1, r + 1, -(r + 1) * d);
        }
    }
    return 0;
}

牛客一道小栗子Lost Cows

#pragma GCC optimize(3, "Ofast", "inline")
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
typedef pair<long long, long long> pll;
const int N = 1e4;
const int M = 2e6 + 1000;
const int inf = 0x3f3f3f3f;
const int mod = (1 << 31) - 1;
#define IOS                  \
    ios::sync_with_stdio(0); \
    cin.tie(0);              \
    cout.tie(0);
ll n, lim;
ll a[N], t[N], ans[N], p[N];
ll lowbit(ll x) { return x & -x; }
void add(ll x, ll val)
{
    for (x; x <= n; x += lowbit(x))
        t[x] += val;
}
ll ask(ll x)
{
    ll res = 0;
    for (; x; x -= lowbit(x))
        res += t[x];
    return res;
}
ll er_query(ll x)
{
    ll l = 1, r = n, mid;
    while (l < r)
    {
        mid = (l + r) >> 1;
        if (ask(mid) >= x) //比较的是前缀和与x
            r = mid;
        else
            l = mid + 1;
    }
    return l;
}
void init()
{
    p[0] = 1;
    for (int i = 1; i < 20; i++)
        p[i] = 1 << i;
    lim = log(n) / log(2) + 1; //向上取整
}
ll query(ll x)
{
    ll val = 0, step = 0;
    for (int i = lim; i >= 0; i--) //枚举p[i]
        if (step + p[i] <= n && val + t[step + p[i]] < x)//倍增的思想
        {//因为已经预处理过p[i]是2的次幂,所以说t[step+p[i]]是step+p[i]的前缀和,eg:1 2 4 8 16,注意到p[i]最小可以取到1
            val += t[step + p[i]];
            step += p[i];
        }
    return step + 1; //***步长永远是趋于x,不可能>x,所以最后要step+1
}
int main()
{
    // IOS;
    while (~scanf("%lld", &n))
    {
        memset(t, 0, sizeof(t));
        add(1, 1);                   //
        for (int i = 2; i <= n; i++) //从2开始
        {
            scanf("%lld", &a[i]);
            add(i, 1); //
        }
        init();
        for (int i = n; i >= 1; i--) //1~n
        {
            ll pos = query(a[i] + 1); //法2树状数组+倍增查询
            // ll pos = er_query(a[i] + 1);法1:二分查询
            add(pos, -1); //查询到的位置上-1,即将1还原为0
            ans[i] = pos; //位置pos也即对应的答案【高度】
        }
        for (int i = 1; i <= n; i++)
            printf("%lld\n", ans[i]);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WTcrazy _

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值