树状数组的那啥啥啥

emmmmm, 在我们学习树状数组之前, 我们应该知道lowbit(n)运算, lowbit(n)定义为非负整数n在二进制下“最低位的1及后面所有的0”构成的数值, 例如n = 10的二进制表示为\((1010)_2\),则\(lowbit(n) = 2 = (10)_2\), 显然可知

\[lowbit(n) = n \And (\sim n +1) = n \And (-n)\]

那就先写一道lowbit的例题吧:

题目描述

一天,CC买了N个容量可以认为是无限大的瓶子,开始时每个瓶子里有1升水。接着~~CC发现瓶子实在太多了,于是他决定保留不超过K个瓶子。每次他选择两个当前含水量相同的瓶子,把一个瓶子的水全部倒进另一个里,然后把空瓶丢弃。(不能丢弃有水的瓶子)

显然在某些情况下CC无法达到目标,比如N=3,K=1。此时CC会重新买一些新的瓶子(新瓶子容量无限,开始时有1升水),以到达目标。

现在CC想知道,最少需要买多少新瓶子才能达到目标呢?

输入格式

一行两个正整数, $ N,K(1\le N\le 2\times 10^9,K\le 10001≤N≤2×10
9 ,K≤1000)$

输出格式

一个非负整数,表示最少需要买多少新瓶子。
不难看出, 我们尽量把最小的两个瓶子给合并, 所以我们用lowbit将n分成二的整次幂, 再全都丢到一个优先队列中, 每次取出最小的两个数进行合并, 直到满足条件为止。

#include 

using namespace std;

typedef long long ll;
const int INF = 0x3f3f3f3f;
const int MAXN = 5e5 + 100;
const int MAXM = 3e3 + 10;
const double eps = 1e-5;

template < typename T > inline void read(T &x) {
    x = 0; T ff = 1, ch = getchar();
    while(!isdigit(ch)) {
        if(ch == '-') ff = -1;
        ch = getchar(); 
    }
    while(isdigit(ch)) {
        x = (x << 1) + (x << 3) + (ch ^ 48);
        ch = getchar();
    }
    x *= ff;
}

template < typename T > inline void write(T x) {
    if(x == 0) {
        putchar('0');
        return ;
    }
    if(x < 0) putchar('-'), x = -x;
    static T tot = 0, ch[20];
    while(x) {
        ch[++tot] = x % 10 + '0';
        x /= 10;
    }
    while(tot) putchar(ch[tot--]);
}

int n, k, ans, sum; 
priority_queue < int > q;

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

int main() {
    read(n); read(k);
    while(n) {
        int x = lowbit(n);
        double s = (log(x) / log(2));
        int y = s;
        q.push(-y);
        sum++;
        n -= x;
    }
    while(sum > k) {
        int x = -q.top(); q.pop();
        int y = -q.top(); q.pop();
        ans += (1 << y) - (1 << x);
        sum--;
        q.push(-y - 1);
    }
//  printf("ans = %d\n", ans);
    write(ans);
    return 0;
}

知道了lowbit的用法, 所以我们把一个序列分成\(log(x)\)小区间,

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

树状数组就是基于上述思想的数据结构, 其基本用途是维护序列的前缀和。对于给定的序列a, 我们建立一个数组c, 其中c[x]保存序列a的区间[x - lowbit(x) + 1, x]中所有数的和, 及\(\sum^x_{x - lowbit(x) + 1}a[i]\)

那我们就看树状数组最基本的操作吧

1.单点修改, 区间查询

题目描述

如题,已知一个数列,你需要进行下面两种操作:

1.将某一个数加上x

2.求出某区间每一个数的和

输入格式

第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。

第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。

接下来M行每行包含3个整数,表示一个操作,具体如下:

操作1: 格式:1 x k 含义:将第x个数加上k

操作2: 格式:2 x y 含义:输出区间[x,y]内每个数的和

输出格式

输出包含若干行整数,即为所有操作2的结果。


对于查询1~x的前缀和, 我们按照刚才提出的方法, 应该求出x二进制表示中每个等于1的位, 把[1, x]分成\(log(n)\)个小区间, 而每个小区间和都已经保存在数组c中。所以把上边的代码稍加改写即可在\(log(n)\)的时间内查询前缀和:

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

当然, 若要查询区间[l, r]中所有数的和, 只需要计算\(ask(r)-ask(l - 1)\)

对于单点修改, 一个数的改变会影响c[x]即其所有祖先节点保存的"区间和"包括a[x],而任意一个节点的祖先最多有\(log(n)\)个, 我们逐一对它们的c值更新即可。

inline void add(int x, int k) {
    for(; x <= n; x += x & -x) c[x] += k;
}

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

为了简便, 我们一边初始的方法是, 每读入一个a[i], 执行add(i, a[i])的操作, 时间复杂度是\(O(nlogn)\), 通常这种方法已经足够。
还有一种更高效的方法, 用前缀和的方式直接更新c[x], 时间复杂度为\(O(n)\), 不过用这种方法需要多开一个前缀和数组

    for(int i = 1; i <= n; ++i) {
        read(a[i]);
        add(i, a[i]);
    }
    for(int i = 1; i <= n; ++i) {
        read(a[i]);
        sum[i] = sum[i - 1] + a[i];
    }
    for(int i = 1; i <= n; ++i) {
        c[i] = sum[i] - sum[i - (i & -i)];
    }

2.区间查询, 单点修改

题目描述

如题,已知一个数列,你需要进行下面两种操作:

1.将某区间每一个数数加上x

2.求出某一个数的值

输入格式

第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。

第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。

接下来M行每行包含2或4个整数,表示一个操作,具体如下:

操作1: 格式:1 x y k 含义:将区间[x,y]内每个数加上k

操作2: 格式:2 x 含义:输出第x个数的值

输出格式

输出包含若干行整数,即为所有操作2的结果。


对于区间修改, 我们可以通过查分而转换成单点修改, 此时的c[i]数组就是区间[x - (x & -x) + 1, x], 所有差分数列的和, 所以更改后的a[i]的值就等于a[i] + c[i]的前缀和

inline void add(int x, int k) {
    for(; x <= n; x += x & -x) c[x] += k;
}

inline int ask(int x) {
    int ans = 0;
    for(; x; x -= x & -x) ans += c[x];
    return ans;
}
cout << a[i] += ask(i);

推荐一道例题:

树状数组的那啥啥啥_第1张图片

这个名字就当没看见吧。。。我也就不想打了, 直接粘学长的博客吧。

学长博客

这道题啊,超级厉害啊,各种坑挖的,什么无符号 longlong,什么坐标为0。

首先,你需要声明一个 unsigned long long 才不会被爆掉,然后记得每个坐标都加上一个1,因为数据里面是有位0的,而树状数组里面是不能为零的,不然会无线循环,Lowbit(0)=0 然后就不停地加或者减无法退出。

因为题目上让树神留下来5个MM,所以我们就可以开五个树状数组来记录下来元素能严格递增并且长度达到第i个树状数组的方案数,更新的步骤是这样的,将第i个数加入第一个树状数组里面,统计小于第i个数的个数,如果然后加到下一个树状数组里面这样每次循环4次。

最后输出第五个树状数组所有元素之和就好了。

其实可以把树状数组压成二维(只是形式上的), 修改和查询方法还是和一维相同, 二维树状数组就是别的写法了。

#include 

using namespace std;

typedef unsigned long long ull;
const int INF = 0x3f3f3f3f;
const int MAXN = 1e6 + 100;
const int MAXM = 3e3 + 10;
const double eps = 1e-5;

template < typename T > inline void read(T &x) {
    x = 0; T ff = 1, ch = getchar();
    while(!isdigit(ch)) {
        if(ch == '-') ff = -1;
        ch = getchar(); 
    }
    while(isdigit(ch)) {
        x = (x << 1) + (x << 3) + (ch ^ 48);
        ch = getchar();
    }
    x *= ff;
}

template < typename T > inline void write(T x) {
    if(x == 0) {
        putchar('0');
        return ;
    }
    if(x < 0) putchar('-'), x = -x;
    static T tot = 0, ch[20];
    while(x) {
        ch[++tot] = x % 10 + '0';
        x /= 10;
    }
    while(tot) putchar(ch[tot--]);
}

ull n, maxx, a[MAXN], c[6][MAXN]; 

inline void add(ull num, ull x, ull v) {
    for(; x <= maxx; x += x & -x) {
        c[num][x] += v;
    }
}

inline ull ask(ull num, ull x) {
    ull ans = 0;
    for(; x > 0; x -= x & -x) ans += c[num][x];
    return ans;
}

int main() {
    read(n);
    for(ull i = 1; i <= n; ++i) {
        read(a[i]); 
        a[i]++;
        maxx = max(maxx, a[i]);
    }
    for(ull i = 1; i <= n; ++i) {
        add(1, a[i], 1);
        ull sum = 0;
        for(ull j = 2; j <= 5; ++j) {
            sum = ask(j - 1, a[i] - 1);
            add(j, a[i], sum);
        }
    }
    write(ask(5, maxx));
    return 0;
}

3. 二维树状数组

你可能感兴趣的:(树状数组的那啥啥啥)