数据结构Part 1
0.树状数组
lowbit
运算
定义一个二进制数 最低位的 与后面的 组成的 为 。
其求法为
树状数组
树状数组每个节点维护的是原数组的一段和(不一定是加法的和),先看一张图:
这张图就是树状数组一个节点维护的和。
那到底怎么维护的呢?实际上, 维护的是 的信息。
区间查询
我们发现, 的前缀和可以在树状数组上在 的复杂度内得出。因为我们可以把其拆成几段,然后用树状数组求出。
比如 ,所以我们可以将 拆成 。此时,我们可以发现,下标每次都是减去了其的 lowbit
值,所以我们可以这么写:
12345678
inline int query(int pos)
{
int res = 0;
for(int i = pos;i;i -= lowbit(i))
res += c[i];
return res;
}
#define seqQuery(l,r) (query(r) - query(l - 1))
然后这个查出来就是 。
所以任意的 就可以通过查询 得出。
单点修改
前面说了,树状数组维护的是一段区间。所以单点修改需要对涉及的区间全部修改一下。
又因为每次都是按 lowbit
值来跳,所以很容易的就可以得出代码。
12345
inline void modify(int pos,int x)
{
for(int i = pos;i <= n;i++)
c[i] += x;
}
区间修改
这个需要结合差分的知识。
因为
所以我们可以维护两个树状数组,分别维护 与 。
区间修改时,我们先对 加上 , 减去 ,再对 加上 , 减去 ,这样就好了。
代码如下
123456789101112
inline void modify(int pos,int k)
{
for(int i = pos;i <= n;i += lowbit(i)) //a[1...pos] += k
c1[i] += k,c2[i] += pos * k;
}
inline int query(int pos) //a[1]...a[pos]
{
int res = 0;
for(int i = pos;i;i -= lowbit(i))
res += (i + 1) * c[i] - c2[i];
return res;
}
复杂度
空间复杂度 ,时间复杂度 ,并且常数大大优于线段树,码量大大少于线段树。
实质
就像线段树维护的是一个幺半群,树状数组同理,事实上,树状数组可维护的信息是线段树的子集。
所以,树状数组也可以维护 乘法等信息。
二维树状数组
别名树状数组套树状数组。
对于每行的树状数组,就是一个一维树状数组。
然后,外层的树状数组管辖区间是一致的。
二维差分
对于一个子矩阵的修改,相当于 (记忆方法:右下角一定加一,然后错排)
又因为有
所以可以考虑维护四个树状数组(我真 tm 不想写),来求子矩阵和。
123456789101112131415161718192021222324252627282930313233343536
#define lowbit(x) (x & -x)
inline void add(int x,int y,int val)
{
for(int i = x;i <= n;i += lowbit(i))
{
for(int j = y;j <= m;j += lowbit(j))
{
c1[i][j] += val;
c2[i][j] += val * x;
c3[i][j] += val * y;
c4[i][j] += val * x * y;
}
}
}
inline void modify(int a,int b,int c,int d,int val)
{
add(a,b,val);
add(a,d + 1,-val);
add(c + 1,b,-val);
add(c + 1,d + 1,val);
}
inline int pre_query(int x,int y)
{
int res = 0;
for(int i = x;i;i -= lowbit(i))
{
for(int j = y;j;j -= lowbit(j))
res += (x + 1) * (y + 1) * c1[i][j] - (y + 1) * c2[i][j] - (x + 1) * c3[i][j] + c4[i][j];
}
return res;
}
inline int query(int a,int b,int c,int d)
{
return pre_query(c,d) - pre_query(c,b - 1) - pre_query(a - 1,d) + pre_query(a - 1,b - 1);
}
权值树状数组
这个东西可以解决全局 小值,二维偏序问题
其实就是对权值数组建树状数组,然后就好了。
关于逆序对,可以枚举 ,然后求权值的前缀和, 小值就是枚举 ,然后每次查询前缀和,若 则扩展,最后 即为答案。
然后树状数组就差不多了,剩下的线段树那讲。
Addition. Tricks
动态开点
其实就是一个存储的 Trick,在点分树里面,可能需要对每个节点都开树状数组,如果直接开是 的空间复杂度。
这个时候,我们就可以把静态数组换成 vector
,根据需要来开。
你甚至可以直接继承 vector<int>
来封装一个动态开点树状数组。
线性建树
因为树状数组管辖的是 的信息,所以我们可以预处理前缀数组 ,然后用 来计算树状数组每一个位置的值。
树状数组套树
如果你学过主席树,那你应该知道,线段树是可以进行前缀和的。
能方便的维护前缀和的数据结构有什么?树状数组!
具体就是树状数组每个节点存的是你需要的一种树,修改就像普通树状数组一样,在对应点位修改。
查询可以先把所有的根节点存下来,然后在内层树操作的时候同时操作所有节点。
里面什么都能套,个人经验:里面套线段树空间开销会大一点,套平衡树时间开销会大一点。
维护后缀的树状数组
可以直接把普通树状数组的修改和查询的枚举方式互换一下。