树状数组及其拓展

树状数组的作用:在 n l o g n nlog_n nlogn 的时间内,完成单点修改与区间求值。如果用朴素算法求解,修改一个数后,我们还要遍历这个区间,设修改 m 次,时间复杂度为 n 2 n^2 n2 。时间显然相差很多吧。

知识储备 lowbit()

先不考虑这玩意儿干什么用的。想一下,如何求一个二进制数最末尾 1 与后面二进制数所组成的数(也就是1 这一位上的二进制值)。比如是 1010100,求出来是 ( 0000100 ) 2 (0000100)_2 (0000100)2,也就是4。

我们只要取他的反码,得到 0101011,再加 1。得到0101100,再将该数与原数按位 与(&),就能得到 0000100。

那为什么这一顿操作就能得出值了呢?简单思考一下,取反之后,从左边看起,0 变为 1, 1 变成了 0。再对这个数加 1,因为要进位,原先的 0 ,现在的 1变成 0,直到遇至第个位 0(原先是 1) 也变回 1,后面的数就不再进位了是吧。可以自己想一遍。再按位 与,后面没有进位的数都变成 0,前面的不就是答案吗。

计算机中取反加 1,其实就是这个数的负数形式。所以代码是这样的

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

思想以及实现

求区间和,很容易想到前缀和对吧。但是这组数是在变化的,所以我们要维护这个区间,这时就要用到树这一结构。因为树的查询很快, l o g n log_n logn 实现。

在这里插入图片描述

一棵普通的树是这样的。

请添加图片描述

但是树状数组我们可以将其变形为这样。

可以观察到 t_1 管理 a_1; t_2 管理 t_1, a_2; t_3管理 a_3; t_4管理t_2,t_3,a_4。虽然没什么规律,但是我们还是可以注意到:t_i,必定管理a_i。那么其他数字有什么规律呢?

我们上面说了lowbit的用法。如果把每个数的lowbit计算出来,可以发现每个数字的lowbit与他管理个数相同。

直接前驱与直接后继

直接前驱

既然 t 的管理区间与lowbit相同,那我们直接看它的管理区间吧。

请添加图片描述

还是这张图。7 管理区间为1,它的前驱就是7 - 1 = 6. 6 管理区间是 2 前驱就是6 - 2 = 4.前驱求出来有什么用呢?可以发现 7 与所有前驱加起来就是 a_1 到 a_7 的区间和。那就很方便能求一个区间,前缀和就能够实现了吧。

直接后驱

同样是管理区间。7 的管理区间为 1,直接后继是 7 + 1 = 8。2 的管理区间是 2 ,直接后继就是 2 + 2 = 4.仅举几例。求出直接后继后。我们可以做到修改,维护区间。因为假如把 t_1改变了,他的后继t_2, t_4, t_8 也会改变。

还有要注意的一点:树状数组不能从 0 开始命名,因为lowbit(0) = 0,0 - 0 = 0。会死循环。

代码

#include<iostream>
using namespace std;

#define N 500010

int n, m, t[N];
int u, v, w;

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

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

int query(int x)
{
    int ans = 0;
    while(x > 0)
    {
        ans += t[x];
        x -= lowbit(x);
      
    }
    return ans;
}

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++)
    {
        int num;
        cin >> num;
        add(i, num);
    }

    for(int i = 1; i <= m; i ++)
    {
        cin >> u >> v >> w;
        if(u == 1)
            add(v, w);
        else
            cout << query(w) - query(v - 1) << endl;
    }
    return 0;
}

看着代码打一遍就差不多会了的。(O…O)

2022-6-6 14:24:24

拓展应用

区间修改与维护

在区间[x, y]加上一个值 k。

一个一个加显然太慢了,这时我们就要用到前缀和的姐妹——差分。

因为差分的一个特殊性质:差分数组存储了 k 位置之前的数加起来等于 k 数。举个例子吧:

1 2 3 5 3 5

差分数组即为 1 1 1 2 -2 2

既然差分数组中的数仅能改变他之后的数,要改变一个区间里的数,只要改变改区间右边顶点的数,比如+ 4。如何保证区间前面的数不变?左端点的数 - 4 就好了。

#include<iostream>
using namespace std;

#define N 500010 
#define ll long long

ll n, m, t[N], last, now;
ll opt, u, v, w;

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

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

ll query(int x)
{
    ll ans;
    ans = 0;
    while(x > 0)
    {
        ans += t[x];
        x -= lowbit(x);
    }
    return ans;
}

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++)
    {
        cin >> now;
        add(i, now - last);
        last = now;
    }

    for(int i = 1; i <= m; i ++)
    {
        cin >> opt;
        if(opt == 1)
        {
            cin >> u >> v >> w;
            add(u, w);
            add(v + 1, -w);
        }

        else
        {
            cin >> u;
            cout << query(u) << endl;
        }
    }

    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值