【学习笔记】树状数组原理

版权声明:欢迎转载评论 https://blog.csdn.net/m0_37809890/article/details/82465184

前言

介绍树状数组的原理以及一些应用。
非常不建议参考本文的参考代码,因为长,慢,丑。

预备知识

前缀和数组,差分数组
树状数组基础

问题:对长为1e5的数组,执行1e5次操作,包括单点增加与区间求和。
lowbit:x&-x,截取数字x的最后一个二进制1及之后的部分。
add(p,v):对p以及递归p+lowbit(p)处的值加v,O(logn)
sum(1,p):取p以及递归p-lowbit(p)处的值之和,O(logn)
sum(l,r):sum(1,r)-sum(1,l-1),同前缀和。

线段树基础
群论

半群:若非空集合 S 上有二元运算 o 满足封闭性、结合律,则称 < S , o > 为半群。
群:若半群 < S , o > 上的运算 o 满足有幺元且每个元素都有逆元,则称 < S , o > 为群。

树状数组原理

这里写图片描述
树状数组一般用于维护一个群上的运算,以较小的时空代价执行单点修改区间求和操作。

树状数组又被称作二叉索引树(Binary Index Tree,BIT),核心思想是将原序列的信息构建为一棵树,然后通过修改和查询这棵树来完成目的。树的节点数目与原序列长度恰好相同,且节点之间存在很强的逻辑关系,所以仍以数组的方式存储。

树上每一个节点都对应一个原位置,设原数组为a,树状数组为c,下标均为从1到n。
每一个树节点的值,都等于它所有子节点的值+原数组中对应位置的值。反过来看的话,每一个树节点的值,都会传递给它的父节点即 c i + l o w b i t ( i ) + = c i ,使用这种方法即可O(n)地从原数组构建树状数组。

观察树状数组的存储方式,发现 c i = Σ j = i l o w b i t ( i ) + 1 i c j ,当需要求 a p 处的前缀和时,只需要求 c p + c p l o w b i t ( p ) + c p l o w b i t ( p ) l o w b i t ( p l o w b i t ( p ) ) + . . . ,即递归 p l o w b i t ( p ) 直到p为0.

当需要修改 a p 时,在树上需要修改 c p , c p + l o w b i t ( p ) , c p + l o w b i t ( p ) + l o w b i t ( p + l o w b i t ( p ) ) , . . . 直到p大于n。
单点修改和区间求和操作都可以在O(logn)内完成。

再次注意:树状数组优化求取前缀和的方法,再根据前缀和之差来获得任意区间和。

树状数组与群

对长为1e5的数组,执行1e5次操作,包括单点乘一个数与区间求积。

同上,只不过将加换为了乘,最后将前缀和之积取商。

扫描二维码关注公众号,回复: 3159103 查看本文章

对长为1e5的数组,执行1e5次操作,包括单点乘一个数与区间求积,输出结果模1e9+7。

同上,所有乘的部分取模,最后取逆元相乘。

对长为1e5的数组,执行1e5次操作,包括两种。
1 p x 表示将p位置的数与x求最大值再放到p位置处。
2 r 表示求区间[1,r]内的最大值

修改时:对p及递归p+lowbit(p)进行max的判断。
查询时:输出r及递归r-lowbit(r)的max值。

此时维护的二元运算是max,它仍有半群的性质(封闭,结合律),但没有群的性质(逆元),所以无法求两个区间之差得到任意区间[l,r]内的最大值。由此例可见,树状数组上“单点修改”需要满足半群性质,“求前缀和”需要满足半群性质,“区间求和”需要满足群性质。

注意:在这里,单点修改操作区间求和操作中的运算是同一种运算。当两者不是同一种运算时,树状数组维护的运算以区间求和操作中的运算为准

对长为1e5的数组,执行1e5次操作,包括两种。
1 p x 表示将p位置的数加上x,x可以为负
2 r 表示求区间[1,r]内的最大值

查询时:输出r及递归r-lowbit(r)的max值
修改时:若x为正,将p更新,对递归p+lowbit(p)进行max的判断。若x为负,更新p,递归p+lowbit(p)全部进行重新求值。
单个值重新求值的复杂度是 O ( l o g n ) ,如对 c 8 进行重新求值需要 c 4 , c 6 , c 7 ,所以此次操作的复杂度是 O ( l o g 2 n )

注意:当单点修改操作中的运算与区间求和操作中的运算不匹配时,需要将单点修改中的运算转化为区间求和操作中的运算,如果转换失败,则O(logn)失效,需要用 O ( l o g 2 n ) 来进行重新求值。

对长为1e5的数组,执行1e5次操作,包括两种
1 p x 表示将p位置的数【经过一系列奇奇怪怪的变换之后】变成x
2 l r 表示求区间[l,r]的和,模1e9+7

修改:对p及递归p+lowbit(p)加上 x a p 再取模。
求和:求前缀和,相减,取模。

可以证明,这【一系列奇奇怪怪的变换】总能转换为所需要的运算(模意义加),因为变换后的值可以算得,所以这个运算就转化为差量的加法。
模意义加是群上的运算,其他群上的运算也具有这个特点。

综上所述,树状数组最适合于维护群上的运算。当用树状数组维护半群上的运算时,修改操作可能退化到 O ( l o g 2 n ) ,且只能求前缀和而无法求任意区间和。
9.10upd:树状数组实际上可以 O ( l o g 2 n ) 求任意区间最大值,但不推荐这种做法。

树状数组与线段树

两者的联系:线段树把所有右子节点都删掉后,就是一个树状数组,这也是lyf曾讲到的“左线段树”理论。
线段树
树状数组
两者的差别:

  1. 线段树是二叉树,而树状数组不是。
  2. 由1. 导致了 线段树节点数比树状数组多.
  3. 由1. 导致了 无逆元运算的修改操作需要对节点重新求值,树状数组是多叉树,一个节点需要O(logn),线段树是二叉树,只需要O(1)
  4. 由2. 导致了 树状数组只能求前缀和,再用大区间减去小区间来获得任意区间和。而线段树通过求若干个小区间的和来求任意区间和,不会受到逆元的限制。
  5. 由2. 导致了 线段树递推步数更多,时空常数更大。
  6. 由3. 4. 导致了线段树对半群上的运算仍有很好的支持。

树状数组应用

代码

再次说明:我的代码仅供参考,因为长,慢,丑。关于简洁,高效,美观的代码请前往洛谷模板题按效率排序查看。

class BinIdTree
{
    int n;
    vector<ll> save;
public:
    explicit BinIdTree(int sz = 0) : n(sz) //建立一个空BIT
    {
        save.assign(n + 1, 0);
    }
    explicit BinIdTree(vector<ll> &src) : n(src.size() - 1) //由已知数组O(n)建立
    {
        save.assign(src.begin(), src.end());
        for(int i = 1; i <= n; i++) if(i + (i & -i) <= n)
            save[i + (i & -i)] += save[i];
    }
    inline void add(int p, ll x) //单点修改
    {
        for(; p <= n; p += p & -p) save[p] += x;
    }
    inline ll sum(int l, int r) //区间求和
    {
        return sum(r) - sum(l - 1);
    }
    inline ll sum(int p)
    {
        ll res = 0;
        for(; p; p -= p & -p) res += save[p];
        return res;
    }
};

有两个数据成员n表示长度,save表示数组本身。
五个函数,第一个构造函数表示建立一个空bit,第二个接收一个数组参数,然后O(n)建立。
剩下的三个函数分别为单点修改,求前缀和,区间求和。

区间修改,单点求和

对原数组求差分数组,再用树状数组维护差分数组

区间修改,区间求和

对长为1e5的数组,执行1e5次操作,包括两种
1 l r x表示对区间[l,r]内所有值加上x
2 l r表示求区间[l,r]内值的和

记原数组为a,原数组的差分数组为b,则 b [ i ] = a [ i ] a [ i 1 ]
记区间求和的结果为ans,首先考虑求[1,r]的和

a n s = Σ i = 1 r Σ j = 1 i b [ i ] = Σ i = l r ( r i + 1 ) b [ i ] = r Σ i = 1 r b [ i ] Σ i = 1 r ( i 1 ) b [ i ]

观察上方的公式,出现了两个前缀和函数的形式,且这两个函数都只和 i 有关。

c [ i ] = ( i 1 ) b [ i ] ,通过维护 b 数组和 c 数组求解这道题:
操作1: b [ l ] + = x b [ r + 1 ] = x c [ l ] + = x ( l 1 ) c [ r + 1 ] = x r
操作2(1到r): s u m ( r ) = r Σ i = 1 r b [ i ] + Σ i = 1 r c [ i ]
操作2(l到r): s u m ( r ) s u m ( l 1 )

现在每次操作1就转变成了多个单点修改,操作2就转变成了多个区间求和,使用两个树状数组分别维护 b c 即可。

代码

class ExBinIdTree
{
    int n;
    BinIdTree bt_b, bt_c;
public:
    explicit ExBinIdTree(vector<ll> &src) : n(src.size() - 1) //由已知数组建立
    {
        vector<ll> b(n + 1), c(n + 1);
        for(int i = 1; i <= n; i++)
        {
            b[i] = src[i] - src[i - 1];
            c[i] = b[i] * (i - 1);
        }
        bt_b = BinIdTree(b), bt_c = BinIdTree(c);
    }
    inline void add(int l, int r, int x) //区间修改
    {
        bt_b.add(l, x);
        bt_b.add(r + 1, -x);
        bt_c.add(l, 1LL * (l - 1)*x);
        bt_c.add(r + 1, -1LL * r * x);
    }
    inline ll sum(int l, int r) //区间求和
    {
        return sum(r) - sum(l - 1);
    }
    inline ll sum(int p)
    {
        return p * bt_b.sum(p) - bt_c.sum(p);
    }
};

猜你喜欢

转载自blog.csdn.net/m0_37809890/article/details/82465184
今日推荐