树状数组

树状数组

简介

1.应用

1.单点修改区间查询
2.区间修改单点查询(差分)
3.区间修改区间查询(差分+公式)
总而言之,就是动态维护前缀和。

2.树状结构图

image

3.lowbit函数

我们知道,任何一个正整数都可以被表示成一个二进制数。如:

\[(2)_{10}=(10)_2 \]

\[(4)_{10}=(100)_2 \]


那么定义一个函数\(f=lowbit(x)\),这个函数的值是\(x\)二进制表达式中最低位的\(1\)所对应的值。

比如:

\[(6)_{10}=(110)_2 \]

那么\(lowbit(6)\)就等于\(2\),因为\((110)_2\)中最低位(就是从右往左数的第二位)对应的数是\(2^1=2\)

所以假设一个数的二进制最低位的\(1\)在从右往左数的第\(k\)位,那么它的\(lowbit\)值就是

\(2^{k−1}\)

\(x \& -x\)的原理
我们都知道机一个数在计算机中是以补码的方式存储的

而负数的补码有这样一种求法:先求出这个数的正数的补码,然后从低位开始知道第一个\(1\)之前(包括这个\(1\))的所有数不变,后面的所有数取反。

现在再来看\(x\&-x\),我们发现,对\(x\)\(-x\)进行与运算之后,二进制下低位第一个\(1\)之前的所有数(包括这个\(1\)本身)不变,而高位所有数为0(一个数与上自己的取反的数)。

此时两个数与运算后得到的二进制数是:\(...1...\)

就正是二进制表达式中最低位的\(1\)所对应的值。

4. \(add\) 函数

\(add\) 函数中,我们是通过 \(x+lowbit(x)\) 取到 \(x\) 的父节点的。原理是什么呢?
首先,\(x\) 的父节点一定是树中高度比 \(x\) 高的节点中最矮的那个节点。
例如 \(6\) 的父节点是 \(8\) 而不是 \(16\)(高度比 \(8\) 高),也不是 \(7\) (高度比 \(6\) 低)。
而树的高度是由 \(lowbit(x)\) 决定的。
因此,\(x\) 的高度就是 \(lowbit(x)\),比它高的节点中高度最低的节点的高度假设为 \(lowbit(j)\),那么 \(lowbit(j)\) 肯定就是 \(x+lowbit(x)\)
他满足高度比 \(lowbit(x)\) 高,因为,\(x+lowbit(x)\) 会使得 \(x\) 的最低位的 \(1\) 一定更高了。
其次,他也满足最低。结合二进制理解,例如 \(x=01010\)\(lowbit(x)=2\),那么我们肯定希望 \(lowbit(j)=100=4\)
此时 \(x+lowbit(x)=01100\),其 \(lowbit()=100\)




应用一:单点修改,区间查询

PROCESS1: 题目链接

题目大意:求形如“山峰”或者“山谷”状三元组的数量

PROCESS2:思路

  1. 从暴力出发\(O(N^2)\)

我们可以预处理出来每个点左侧和右侧大于和小于它的点的数量,然后相乘即可(乘法原理)

  1. 树状数组\(O(Nlog_2^N)\)

树状数组中一种常用的求小于或者与某一个点的数量的方法:将原数组的权值映射到该点在树状数组的下标(经常与离散化结合使用)。例如\(tr[3]=5\)就表示权值为\(3\)的点有\(5\)个。这样在树状数组中求小于某一个数y的数的数量,就可以直接求\(y\)之前的前缀和,这个前缀和就代表所有小于\(y\)的数的个数和。
但这样就无法同时预处理一个点左侧小于它的点的数量和右侧小于它的点的数量,所以我们需要操作两侧,第一次预处理左侧的,然后第二次处理右侧时直接求结果即可。

注意答案可能爆int

PROCESS3:代码

点击查看代码
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 200010;

int n, a[N], g[N], l[N];
int tr[N];

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

void add(int x, int c)
{
    for(int i = x; i <= n; i += lowbit(i))  tr[i] += c;
}

int get(int x)
{
    int res = 0;
    for(int i = x; i; i -= lowbit(i))  res += tr[i];
    return res;
}


int main()
{
    cin >> n;
    for(int i = 1; i <= n; i ++ )   cin >> a[i];
    
    for(int i = 1; i <= n; i ++ )
    {
        int y = a[i];
        g[i] = get(n) - get(y);
        l[i] = get(y); 
        add(y, 1);
    }
    
    memset(tr, 0, sizeof tr);
    
    long long res1 = 0, res2 = 0;
    for(int i = n; i >= 1; i -- )
    {
        int y = a[i];
        res1 += (long long)(get(n) - get(y)) * g[i];//这里会爆int
        res2 += (long long)get(y) * l[i];
        add(y, 1);
    }
    
    cout << res1 << ' ' << res2 << endl;
    
    return 0;
}



应用二:区间修改,单点查询

PROCESS1:例题链接

PROCESS2:思路

把树状数组看做差分数组,差分数组的前缀和就是某个点的权值

PROCESS3:代码

点击查看代码
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n, m;
int tr[N], a[N];

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

void add(int x, int c)
{
    for(int i = x; i <= n; i += lowbit(i))  tr[i] += c;
}

int sum(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 ++ )   cin >> a[i];
    for(int i = 1; i <= n; i ++ )   add(i, a[i] - a[i - 1]);
    
    while (m -- )
    {
        string op;
        cin >> op;
        if(op == "C")
        {
            int l, r, c;
            cin >> l >> r >> c;
            add(l, c);
            add(r + 1, -c);
        }
        else
        {
            int x;
            cin >> x;
            cout << sum(x) << endl;
        }
    }
    
    
    return 0;
}



应用三:区间修改,区间查询

PROCESS1:题目链接

PROCESS2:思路

由于涉及到区间修改,那么我们只能用树状数组模拟差分数组,否则区间修改的时间复杂度必然是\(O(N)\)的。
此时,区间[1, n]的和为蓝色区域所示(b为树状数组)(a为原数组)
image
如果我们把该蓝色区域的补集(红色区域加上)
总的矩阵就是:\((n + 1) * \sum_{1}^{n}b_i = (n + 1) * a_n\)
再减去红色区域:\(\sum_{1}^{n}i*b_i\)(竖着看)
就是我们的蓝色区域,也就是区间[1,n]的前缀和
那么,我们只需要在维护一个树状数组,tr[i]=i*b[i]即可

PROCESS3:代码

点击查看代码
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 100010;

int n, m;
LL tr1[N], tr2[N];
int a[N];

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

void add(LL tr[], int x, LL c)
{
    for(int i = x; i <= n; i += lowbit(i))  tr[i] += c;
}

LL sum(LL tr[], int x)
{
    LL res = 0;
    for(int i = x; i; i -= lowbit(i))   res += tr[i];
    return res;
}

LL get(int x)
{
    return (long long)(x + 1) * sum(tr1, x) - sum(tr2, x); 
}

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++ )   cin >> a[i];
    for(int i = 1; i <= n; i ++ )  
    {
        add(tr1, i, a[i] - a[i - 1]);
        add(tr2, i, (long long)i * (a[i] - a[i - 1]));
    }
    
    while (m -- )
    {
        string op;
        cin>> op;
        if(op == "C")
        {
            int l, r, c;
            cin >> l >> r >> c;
            add(tr1, l, c);
            add(tr1, r + 1, -c);
            add(tr2, l, (long long)l * c);
            add(tr2, r + 1, (long long)-c * (r + 1));
        }
        else
        {
            int l, r;  cin >> l >> r;
            cout << get(r) - get(l - 1) << endl;
        }
    }
    
    return 0;
}



其他例题

例题一

PROCESS1:Acwing
PROCESS2:思路

凡是这类一开始时,所求答案完全未知的问题,应该在边界问题上寻找原问题的突破口。因为对于那些靠近问题空间“中部”的子问题,其“左右两边”可以认为和它是本质相同的子问题,没有办法直接解决。因此我们应该着重考虑边界处的子问题。

在本题中,我们首先考虑最后一头牛。由于它已经统计了在它之前的所有牛,因此,假如它比\(x\)头牛高,则它的高度一定是\(x+1\)(所有牛的高度是一个\([1, n]\)的排列)。我们采取从后往前考虑的方式,就是因为题目给出了“每头牛已知在自己前面比自己矮的牛的个数”这一条件,从后往前可以少考虑很多问题。

由于每头牛的高度各不相同且取遍区间\([1,n]\),因此,对于倒数第二头牛而言,它应该在除去上述\(x+1\)的区间[\([1,n]\)中,选取\(A_{n−1}+1\)小的数。其他的牛以此类推。

我们可以令树状数组的\(tr\)表示当前这个数有没有被选,然后二分查询(不然时间复杂度还是\(O(N^2)\)的)在没有被选的数当中第\(A_{i}+1\)大的数。初始化\(tr[i]=0,({i \in {1,n}})\)表示当前数没有被选,选择一个数之后就把当前数置为\(0\)

PROCESS3:代码

点击查看代码
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n, tr[N], h[N];
int res[N];

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

void add(int x, int c)
{
    for(int i = x; i <= n; i += lowbit(i))  tr[i] += c;
}

int sum(int x)
{
    int res = 0;
    for(int i = x; i >= 1; i -= lowbit(i))  res += tr[i];
    return res;
}

int main()
{
    cin >> n;
    for(int i = 2; i <= n; i ++ )   cin >> h[i];
    for(int i = 1; i <= n; i ++ )   add(i, 1);
    
    for(int i = n; i >= 1; i -- )
    {
        int l = 0, r = n, goal = h[i] + 1;
        while(l < r)
        {
            int mid = l + r >> 1;
            if(sum(mid) >= goal)    r = mid;
            else    l = mid + 1;
        }
        res[i] = r;
        add(r, -1);
    }
    
    for(int i = 1; i <= n; i ++ )   cout << res[i] << endl;
    return 0;
}

例题二

PROCESS1: 蓝桥杯




例题三:

PROCESS1: 蓝桥杯

posted @ 2022-05-09 10:35  光風霽月  阅读(18)  评论(0编辑  收藏  举报