OVSolitario-io

导航

树状数组(不能有0,否则加lowbit一直为0死循环)

lowbit:lowbit(x) = x & -x

我们知道反码 = 全1 - x(当前数) + 1 => 补码,即 (反码 + 1)(补码) - x

111111 - x + 1 <=> 1000000 - x

e.g.此时有前面x & -x有前面消去
截屏2025-11-23 20.02.06

树状数组

维护序列a1,a2··an,有O(logn)完成单点加值以及查询前缀和操作

原理:树状数组c[i] = a[i] - lowbit(i) + 1 ~ i的和
截屏2025-11-23 20.26.59

对于c[i] = a[i] - lowbit[i] + 1 ~ i, 有定义:

1 = 1 ~ 1,2 = 1 ~ 2,3 = 3 ~ 3,4 = 1 ~ 4,5 = 5 ~ 5,6 = 5 ~ 6,7 = 7 ~ 7,8 = 1 ~ 8

即有ci=???1000,实际记录的为???0001~???1000之和

???代表相同数

查询: 若查询1~x的和,当x!=0时(不断重复进行s+=c[x],x-=lowbit(x)操作)

每次减掉lowbit相当于去掉二进制最后一个1,即求一段和(存在和对应区间相接为整个区间,相加为1~x的和)

e.g.对于110110101每次减掉lowbit()存在:

110110001 ~ 110110100
110100001 ~ 110110000
110000001 ~ 110100000
100000001 ~ 110100000

依次求得区间[ ] [ ] [ ] [ ] ,最终得到有连续区间

LL query(int x) {//1....x
    LL s = 0;
    for(; x; x -= x & (-x)) {
        s += c[x];
    }
    return s;
}

修改:

方向相反,查询时不断减去,这里修改不断加

原理:看哪些c包含修改的数(受到影响),对应修改

void modify(int x, LL s) {//a[x] += s;
    for(; x <= n; x += x & (-x)) {
        c[x] += s;
    }
}

树状数组1:单点更新,区间查询 树状数组1
对于修改操作,可近似等价于modify中加操作:对于5->10的操作,即+5

即求出原本的值,再知道差值,即可对应修改 <=> 加操作

点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 501000;
int a[N], n, q;
LL c[N];
LL query(int x) {
    LL s = 0;
    for(; x; x -= x & (-x)) {
        s += c[x];
    }
    return s;
}
void modify(int x, LL s) {//a[x] += s;
    for(; x <= n; x += x & (-x)) {
        c[x] += s;
    }
}

int main() {
    scanf("%d%d", &n, &q);
    for(int i = 1; i <= n; ++ i) {
        scanf("%d", &a[i]);
        modify(i, a[i]);//对树状数组一开始为0,现给a[i]位置上加上值
    }
    for(int i = 1; i <= q; ++ i) {
        int ty;
        scanf("%d", &ty);
        if(ty == 1) {//modify
            int x, d;
            scanf("%d%d", &x, &d);
            modify(x, d);
        }
        else {//query
            int x, y;
            scanf("%d%d", &x, &y);
            printf("%lld\n", query(y) - query(x - 1));
        }
    }
}

修改值版本:

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 201000;
int a[N], n, q;
LL c[N];
LL query(int x) {
    LL s = 0;
    for(; x; x -= x & (-x)) {
        s += c[x];
    }
    return s;
}
void modify(int x, LL s) {//a[x] += s;
    for(; x <= n; x += x & (-x)) {
        c[x] += s;
    }
}

int main() {
    scanf("%d%d", &n, &q);
    for(int i = 1; i <= n; ++ i) {
        scanf("%d", &a[i]);
        modify(i, a[i]);//对树状数组一开始为0,现给a[i]位置上加上值
    }
    for(int i = 1; i <= q; ++ i) {
        int ty;
        scanf("%d", &ty);
        if(ty == 1) {//x=1为修改操作
            int x, d;
            scanf("%d%d", &x, &d);
            modify(x, d - a[x]);//a[x]为原来值,改为d那么增量为d-x
            a[x] = d;
        }
        else {
            int x;
            scanf("%d%d", &x);
            printf("%lld\n", query(x));
        }
    }
}

单点修改值: 由于树状数组支持单点更新,此时有old = a[x],现想将值更改有new = d,那么有new = old + (new + old) 即 a[x] += (d - a[x])
e.g.5 -> 8

+3(8 - 5)

即modify(x, d - a[x]),在x位置上增量d - a[x],然后将a[x] = d(新值),否则a[x]里面还存着之前的old值

逆序对2:逆序对2

树状数组2:区间更新,单点查询 树状数组2:差分 + 推式子

考虑区间加,单点查询,有:

  • 对于区间加:考虑其差分数组,另d[i] = a[i] - a[i - 1],a[i] = d1 + d2 + ··· + di,此时将[l,r] + 1即操作\(d_l\) + 1 和 \(d_{r+ 1}\) - 1两值即构造出区间更新

  • 对于单点查询:构造出差分后,单点查询即d的前缀和

扩展:前缀查询
对于a1 = d1,a2 = d2,a3 = d3,实际上即x * d1 + (x-1) * d2 + d3 ->

\(\sum_{i = 1}^{x} (x + 1 - i)d_i = (x + 1)\sum_{i = 1}^{x}d_i - \sum_{i = 1}^{x}i * d_i\) 即维护\(d_i\)前缀和 和 i * \(d_i\)的前缀和

i * \(d_i\)的前缀和可以看做一个新的数组d'[i] = i * \(d_i\),维护d'[i]的前缀和即可

点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 501000;
int a[N], n, q;
LL c[N];
LL query(int x) {
    LL s = 0;
    for(; x; x -= x & (-x)) {
        s += c[x];
    }
    return s;
}
void modify(int x, LL s) {
    for(; x <= n; x += x & (-x)) {
        c[x] += s;
    }
}

int main() {
    scanf("%d%d", &n, &q);
    for(int i = 1; i <= n; ++ i) scanf("%d", &a[i]);
    //差分
    modify(1, a[1]);
    for(int i = 2; i <= n; ++ i) {
        modify(i, a[i] - a[i - 1]);
    }
    for(int i = 1; i <= q; ++ i) {
        int ty;
        scanf("%d", &ty);
        if(ty == 1) {
            int l, r, d;
            scanf("%d%d%d", &l, &r, &d);
            modify(l, d);
            modify(r + 1, -d);
        }
        else {
            int x;
            scanf("%d", &x);
            printf("%lld\n", query(x));//对差分求前缀和
        }
    }
}

扩展版:

点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef unsigned long long u64;//对2^64取模,用无符号整型自然溢出即可
const int N = 201000;
int n, q;
template<class T>//C++模版,可以自行搜索,T为自定义类型
//封装是因为有两个树状数组
struct BIT {
    T c[N];
    int size;
    void resize(int s) {
        size = s;
    }
    T query(int x) {
        assert(x <= size);
        T s = 0;
        for(; x; x -= x & (-x)) {
            s += c[x];
        }
        return s;
    }
    void modify(int x, T s) {
        //一定不能有x = 0,x = 0会死循环
        assert(x != 0);
        for(; x <= size; x += x & (-x)) {//这里会判断,所以r+1范围>n也可
            c[x] += s;
        }
    }
};
//封装之后比赛可以直接用,改一下类型即可
BIT<u64> c1, c2;//c1记前缀和,c2记i*di前缀和

int main() {
    scanf("%d%d", &n, & q);
    c1.resize(n);
    c2.resize(n);
    for(int i = 0; i < q; ++ i) {
        int ty;
        scanf("%d", &ty);
        if(ty == 1) {
            int l, r;
            u64 d;
            scanf("%d%d%lld", &l, &r, &d);
            c1.modify(l, d);
            c1.modify(r + 1, -d);
            c2.modify(l, l * d);
            c2.modify(r + 1, (r + 1) * (-d));
        }
        else {
            int x;
            scanf("%d", &x);
            u64 ans = (x + 1) * c1.query(x) - c2.query(x);
            printf("%llu\n", ans);
        }
    }
}

树状数组二分:查询最大的T < s

int query(LL s) {
    LL t = 0;//1~pos的和 
    int pos = 0;//pos记录当前走到什么位置
    for(int j = 18; j >= 0; -- j) {//从高位往低位做,只要任意2整次幂>=n即可
        if(pos + (1 << j) <= n && t + c[pos + (1 << j)] <= s) {
            pos += (1 << j);
            t += c[pos];
        }
    }
}

更常用写法:不记录t而是直接用s去减
s不断减去前缀,保证s >= 0即可(能减则减掉)

点击查看代码
int query(LL s) {
    LL t = 0;
    int pos = 0;
    for(int j = 18; j >= 0; -- j) {
        if(pos + (1 << j) <= n && c[pos + (1 << j)] <= s) {
            pos += (1 << j);
            s -= c[pos];
        }
    }
    return pos;
}

有for j = $\left \lceil logn \right \rceil $ ~ 0,记录pos当前走到的位置,t记1~pos的和

有forj从高位->低位,令${pos}' $= pos + \(2^j\)
由于从高位->低位,做到j位时存在:

\((j~后j位都为0)_2\) = ???000000

c[ ${pos}' $ ]操作即将当前第j位变为1 -> 有???100000,即有c[${pos}' $]存的刚好是???000001 ~ ???100000的和

存的是pos+1 ~ ${pos}' $的和 (pos为???000000)

当t + c[${pos}' $] <= s,即将这位改为1,其和仍小于等于s(则可将其加上去),则将pos = \({pos}'\),令t += c[${pos}' $]

每次尝试将这位改为1,和仍小的话则改为1

从高位到低位枚举,最后pos即满足条件pos

点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 501000;
int a[N], n, q;
LL c[N];
//查询变了, 不再是找前缀和, 变为最大前缀 <= s
int query(LL s) {
    LL t = 0;//1~pos的和 
    int pos = 0;//pos记录当前走到什么位置
    for(int j = 18; j >= 0; -- j) {//从高位往低位做,只要任意2整次幂>=n即可
        if(pos + (1 << j) <= n && t + c[pos + (1 << j)] <= s) {
            pos += (1 << j);
            t += c[pos];
        }
    }
    return pos;
}
void modify(int x, LL s) {
    for(; x <= n; x += x & (-x)) {
        c[x] += s;
    }
}

int main() {
    scanf("%d%d", &n, &q);
    for(int i = 1; i <= n; ++ i) {
        scanf("%d", &a[i]);
        modify(i, a[i]);//对树状数组一开始为0,现给a[i]位置上加上值
    }
    for(int i = 1; i <= q; ++ i) {
        int ty;
        scanf("%d", &ty);
        if(ty == 1) {
            int x, d;
            scanf("%d%d", &x, &d);
            modify(x, d - a[x]);
            a[x] = d;
        }
        else {
            LL s;
            scanf("%lld", &s);
            printf("%d\n", query(s));//对差分求前缀和
        }
    }
}

总结:对权值开树状数组可支持操作:(插入一个数,删除一个数,查询第k个),很像平衡树但也可通过树状数组做

插入一个数:即将对应的c[x] += 1(这里c[x]为x出现多少次的原数组)
删除一个数:c[x] -= 1
查询第k大:即查询从小->大的第k个,找到一最大前缀T有其和 <= k+1,即知道右边数字为小->大的第k个

截屏2025-11-26 08.36.11

二进制位从小大的大,若T(大到小找到<=k+1)则剩余为k

查询第k个数可以通过树状数组二分来实现

高维树状数组(一般会和其他结合有神奇应用):单点修改值,区间查询

高维有类似的写法,即对于c[i][j]记录的是a[i - lowbit(i) + 1 ~ i][j - lowbit(j) + 1 ~ j]的前缀和

对于多维的错误写法:导致x执行1次时,y已经变为0

LL query(int x, int y) {
    LL s = 0;
    for(; x; x -= x & (-x)) for(; y; y -= y & (-y)){
        s += c[x];
    }
    return s;
}
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 510;
int n, m, q;
int a[N][N];
LL c[N][N];
LL query(int x, int y) {
    LL s = 0;
    for(int p = x; p; p -= p & (-p)) { 
        for(int q = y; q; q -= q & (-q)) {//即保证x循环时存在y的写法
            s += c[p][q];
        }
    }
    return s;
}
void modify(int x, int y, LL s) {
    for(int p = x; p <= n; p += p & (-p)) {
        for(int q = y; q <= m; q += q & (-q)) {
            c[p][q] += s;
        }
    }
}

int main() {
    scanf("%d%d%d", &n, &m, &q);
    for(int i = 1; i <= n; ++ i) {
        for(int j = 1; j <= m; ++ j) {
            scanf("%d", &a[i][j]);
            modify(i, j, a[i][j]);
        }
    }
    for(int i = 1; i <= q; ++ i) {
        int ty;
        scanf("%d", &ty);
        if(ty == 1) {
            int x, y, d;
            scanf("%d%d%d", &x, &y, &d);
            modify(x, y, d - a[x][y]);
            a[x][y] = d;
        }
        else {
            int x, y;
            scanf("%d%d", &x, &y );
            printf("%lld\n", query(x, y));
        }
    }
}

高维树状数组神奇应用:平面上插n个点,支持两维都小于(xi <= X并且 yi <= Y)的里面有多少点满足

即可想象有一n*n数组,给对应插点位置 + 1 ->即变为求前缀和
截屏2025-11-26 09.12.14

当n = 1e5时,用hash表 + 二维树状数组方法,将用到的位置存到hash表中,即O(n\(log^2\)n)做法

posted on 2025-11-24 07:38  TBeauty  阅读(8)  评论(0)    收藏  举报