树状数组维护区间和/最值


–2022.2.18更新树状数组维护区间最值

树状数组维护区间和/最值

假设给定我们一个数组,让我们进行单点修改和区间查询操作,则需要O(1)和O(n)的时间复杂度,多次修改查询的总复杂度最坏为O(n^2),而用树状数组进行这些操作的时间复杂度仅有O(logn),总最坏时间复杂度为O(nlogn)。

引入-lowbit操作

lowbit(x)非负整数x在二进制表示下最低位1及其后面的0构成的数值。

例如

lowbit(44) = lowbit(101100) = (100) = 4;

一个数x(101100)将其取反后(010011)在加一(010100)后,我们可以观察到,在二进制上除最低位1及其后面的0,其余所有位都取反了,此时我们再与原数进行按位与操作,就能得到x的lowbit。

计算机在存储时用的是补码而非反码,因此在编译器中的-x表示的就是x取反后加一,因此:

lowbit(X) = x & -x;

树状数组维护区间和

树状数组的建立

我们在一个数组a上建立以上的树形结构,每个节点t[x]保存以x为根的子树中叶结点值的和

t[x]节点覆盖的长度即为lowbit(x)。

t[x]节点的父节点为t[x + lowbit(x)]。

单点修改操作 & 建立树状数组

单点修改

可见,我们修改一个点,也要修改其上的父节点,最坏的时间复杂度为O(logn)

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

如何建立树状数组?

有了单点修改的操作,我们只需要在读入时对每个t[i]进行add操作即可

for (int i = 1; i <= n; i ++ ){
	int x;
	cin >> x;
	add(i, a);
}

区间查询操作

区间查询

而对于区间查询(以7为例),我们要同时加上t[7], t[6],t[4](即覆盖了a[1]~a[7]的所有t[x])。

int ask(int x){
	int ans = 0;
	for (; x; x -= x & -x) ans += t[x];
	return ans;
}
//单点修改与区间查询
#include <bits/stdc++.h>
using namespace std;
#define int long long
int n, p;
int q[1123456], t[1123456];

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

void add(int x, int k)
{
    for (; x <= n; x += lowbit(x)) t[x] += k;
}

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

signed main()
{
    cin >> n >> p;
    for (int i = 1; i <= n; i ++ ){
        cin >> q[i];
        add(i, q[i]);
    }
    
    while (p -- ){
        int op, x, y;
        cin >> op >> x >> y;
        if (op == 1) add(x, y);
        if (op == 2) cout << ask(y) - ask(x - 1) << endl;
    }

    return 0;
}

区间修改与单点查询

要将一个区间内同时加上一个数k,或查询当前的第i个数,我们需要新的操作。

区间查询与单点修改

注意初始化树状数组时b[i] = q[i] - q[i - 1];

//区间修改与单点查询
#include <bits/stdc++.h>
using namespace std;
#define int long long
int n, p;
int q[1123456], t[1123456];

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

void add(int x, int k)
{
    for (; x <= n; x += lowbit(x)) t[x] += k;
}

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

signed main()
{
    cin >> n >> p;
    for (int i = 1; i <= n; i ++ ){
        cin >> q[i];
        add(i, q[i] - q[i - 1]);
    }
    
    while (p -- ){
        int op, l, r, x;
        cin >> op;
        if (op == 1){
            cin >> l >> r >> x;
            add(l, x), add(r + 1, -x);
        }
        if (op == 2){
            cin >> x;
            cout << ask(x) << endl;
        }
    }

    return 0;
}

区间修改与区间查询

区间查询与区间修改

如图,我们仍用树状数组维护差分数组b的前缀和,区间查询的值即为蓝色部分,我们可以发现蓝色部分不方便计算,因此构建一个大矩形(红色),再减去不计算的部分(黄色),这一部分我们可以维护另外一个i*b[i]的前缀和数组。

image-20220217152928395

//区间修改与区间查询
#include <bits/stdc++.h>
using namespace std;
#define int long long
int n, p;
int q[1123456], t[1123456], s[1123456], sum[1123456];

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

void add1(int x, int k)
{
    for (int i = x; i <= n; i += lowbit(i)) t[i] += k, s[i] += x * k;
}

int ask1(int x)
{
    int ans = 0;
    for (int i = x; i; i -= lowbit(i)) ans += (x + 1) * t[i] - s[i];
    return ans;
}

signed main()
{
    cin >> n >> p;
    for (int i = 1; i <= n; i ++ ){
        cin >> q[i];
        sum[i] = q[i] + sum[i - 1];
        add1(i, q[i] - q[i - 1]);
    }
    
    while (p -- ){
        int op, l, r, x;
        cin >> op;
        if (op == 1){
            cin >> l >> r >> x;
            add1(l, x), add1(r + 1, -x);
        }
        if (op == 2){
            cin >> l >> r;
            cout << ask1(r) - ask1(l - 1) << endl;
        }
    }

    return 0;
}

本篇题解图片来自〔manim | 算法 | 数据结构〕 完全理解并深入应用树状数组 | 支持多种动态维护区间操作 个人认为讲的很详细。

树状数组维护最大值

用树状数组维护最大值。同样可以让每个节点t[x]保存以x为根的子树中叶结点值的最大值,对于维护的最大值,我们同样可以进行单点修改和区间查询操作。

单点修改

由之前维护区间和的单点修改操作,我们很容易想到:

void add(int x, int k){ // 将q[x]变为k
	for (; x <= n; x += lowbit(x)) t[x] = max(t[x], k);
}

这段代码是由前面树状数组维护区间和里单点操作修改而成,仅将t[x]+=k 变为 t[x] = max(t[x], k)。

那么这段代码是否可行呢?

如果我们要将这个数变大,即k >= q[x],那么这段代码是没有问题的,t[x]每次都会更新,然后往上一步步寻找。但想一下,如果k < q[x],并且q[x]恰好是t[x]覆盖范围内唯一的最大值,那么我们要将q[x]变小,其范围内最大值,即t[x]也一定会变小,但这个max操作并不会将t[x]变小,因此这样的单点修改是不可行的。

唯一的方法,是连同q[x]和t[x]一起更新,不进行max操作,然后往上找相关的数继续更新。

例

还是该图,我们想一下,与t[8]直接相关的有几个数?

很容易观察到:

t[8] = max({a[8], t[4], t[6], t[7]});

我们转化为二进制观察:

t[1000] = max({a[1000], t[100], t[110], t[111]});

这里我们直接给出公式:

t[i] = max({q[i], t[i - lowbit(i)/(2^1)], t[i - lowbit[i]/(2^2)]...t[i - 2^0]});

代码:

void update(int x, int k){ // 将q[x]变为k
    q[x] = k; // 将原数组也改变,可加可不加
	for (; x <= n; x += lowbit(x)){
		t[x] = k; // 直接更新
		for (int i = 1; i < lowbit(x); i <<= 1) t[x] = max(t[x], t[x - i]);//公式
	}
}

区间查询

查询区间[l, r]的最大值,需要用上原数组。

区间查询的过程我们主要用分类讨论解决。

如果 r - lowbit(r) + 1 >= l, ans = max(query(l, r - lowbit(r)), t[r]);

否则 res = max(query(l, r - 1), q[r]);

直接看代码吧,不难理解:

int query(int l, int r)
{
	int ans = 0;
    while (r >= l){
		ans = max(q[r], ans), r -- ;
        for (; r - lowbit(r) >= l; r -= lowbit(r)) ans = max(t[r], ans);
    }
    return ans;
}

尾部添数

树状数组维护最大值支持向区间尾部添加一个值。

void add(int x)
{
	q[ ++ tot] = x;
    t[tot] = max(q[tot], query(tot + 1 - lowbit(tot), tot - 1));
}

文章作者: Cedeat
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Cedeat !
  目录