前言
介绍树状数组的原理以及一些应用。
非常不建议参考本文的参考代码,因为长,慢,丑。
预备知识
前缀和数组,差分数组
树状数组基础
问题:对长为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)
,同前缀和。
线段树基础
群论
半群:若非空集合
上有二元运算
满足封闭性、结合律,则称
为半群。
群:若半群
上的运算
满足有幺元且每个元素都有逆元,则称
为群。
树状数组原理
树状数组一般用于维护一个群上的运算,以较小的时空代价执行单点修改,区间求和操作。
树状数组又被称作二叉索引树(Binary Index Tree,BIT),核心思想是将原序列的信息构建为一棵树,然后通过修改和查询这棵树来完成目的。树的节点数目与原序列长度恰好相同,且节点之间存在很强的逻辑关系,所以仍以数组的方式存储。
树上每一个节点都对应一个原位置,设原数组为a,树状数组为c,下标均为从1到n。
每一个树节点的值,都等于它所有子节点的值+原数组中对应位置的值。反过来看的话,每一个树节点的值,都会传递给它的父节点即
,使用这种方法即可O(n)地从原数组构建树状数组。
观察树状数组的存储方式,发现 ,当需要求 到 处的前缀和时,只需要求 ,即递归 直到p为0.
当需要修改
时,在树上需要修改
直到p大于n。
单点修改和区间求和操作都可以在O(logn)内完成。
再次注意:树状数组优化求取前缀和的方法,再根据前缀和之差来获得任意区间和。
树状数组与群
对长为1e5的数组,执行1e5次操作,包括单点乘一个数与区间求积。
同上,只不过将加换为了乘,最后将前缀和之积取商。
对长为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(logn)失效,需要用 来进行重新求值。
对长为1e5的数组,执行1e5次操作,包括两种
1 p x 表示将p位置的数【经过一系列奇奇怪怪的变换之后】变成x
2 l r 表示求区间[l,r]的和,模1e9+7
修改:对p及递归p+lowbit(p)加上
再取模。
求和:求前缀和,相减,取模。
可以证明,这【一系列奇奇怪怪的变换】总能转换为所需要的运算(模意义加),因为变换后的值可以算得,所以这个运算就转化为差量的加法。
模意义加是群上的运算,其他群上的运算也具有这个特点。
综上所述,树状数组最适合于维护群上的运算。当用树状数组维护半群上的运算时,修改操作可能退化到
,且只能求前缀和而无法求任意区间和。
9.10upd:树状数组实际上可以
求任意区间最大值,但不推荐这种做法。
树状数组与线段树
两者的联系:线段树把所有右子节点都删掉后,就是一个树状数组,这也是lyf曾讲到的“左线段树”理论。
两者的差别:
- 线段树是二叉树,而树状数组不是。
- 由1. 导致了 线段树节点数比树状数组多.
- 由1. 导致了 无逆元运算的修改操作需要对节点重新求值,树状数组是多叉树,一个节点需要O(logn),线段树是二叉树,只需要O(1)
- 由2. 导致了 树状数组只能求前缀和,再用大区间减去小区间来获得任意区间和。而线段树通过求若干个小区间的和来求任意区间和,不会受到逆元的限制。
- 由2. 导致了 线段树递推步数更多,时空常数更大。
- 由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,则
记区间求和的结果为ans,首先考虑求[1,r]的和
观察上方的公式,出现了两个前缀和函数的形式,且这两个函数都只和 有关。
记
,通过维护
数组和
数组求解这道题:
操作1:
操作2(1到r):
操作2(l到r):
现在每次操作1就转变成了多个单点修改,操作2就转变成了多个区间求和,使用两个树状数组分别维护 和 即可。
代码
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);
}
};