树状数组复习笔记
前置知识
树状数组,前缀和,差分
区间修改区间查询
首先我们要明白树状数组维护的是前缀和,这也是普通树状数组只能区间修改单点查询或者单点修改区间查询的原因。
其实树状数组也是可以支持区间修改区间查询的。
我们设数组 \(t\) 为数组 \(a\) 的差分数组,那么显然差分数组的前缀和数组就是原数组 \(a\),设数组 \(sum\) 为 \(a\) 的前缀和数组。
那么就有
将 \(t\) 代入 \(sum\) 得
其实\(sum_r=t_i*(r-i+1)\) \((1\le i \le r)\)
在下面为了方便,我们直接用数学符号 \(\sum_{i=j}^{n}{a_i}\) 表示从 \(j\) 到 \(n\) 所有 \(a_i\) 的和。
所以
因为这种形式不太好维护,所以我们就可以将里面的里面的式子拆开
然后就可以发现这两个东西都可以用树状数组来维护,所以我们需要维护两个树状数组一个是 \(\sum_{i=1}^{n}t_i\) 另一个是 \(\sum_{i=1}^{n}t_i*(i-1)\) 。
那么第一个树状数组修改就和原来的一样,第二个数组修改需要改为将增加的数乘上 \(pos-1\) 。
\(Code\)
#include<bits/stdc++.h>
#define ll long long
#define pii pair<ll,ll>
#define mp make_pair
#define one first
#define two second
using namespace std;
const int N=2e5+100;
int n,m;
pii t[N];
int a[N];
inline pii operator +(pii a,pii b) {return mp(a.one+b.one,a.two+b.two);}
inline int lowbit(int x) {return x&(-x);}
inline void update(int pos,ll k)
{
pii now=mp(k,k*(pos-1));
for (int i=pos;i<=n;i+=lowbit(i)) t[i]=t[i]+now;
return ;
}
inline ll query(int pos)
{
pii ans=mp(0,0);
for (int i=pos;i;i-=lowbit(i)) ans=ans+t[i];
return ans.one*pos-ans.two;
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++) t[i]=mp(0,0);
for (int i=1;i<=n;i++)
{
scanf("%d",a+i);
update(i,a[i]-a[i-1]);
}
for (int i=1;i<=m;i++)
{
int opt,l,r;
scanf("%d",&opt);
if (opt==1)
{
ll k;
scanf("%d%d%lld",&l,&r,&k);
update(l,k);update(r+1,-k);
}
else
{
scanf("%d%d",&l,&r);
printf("%lld\n",query(r)-query(l-1));
}
}
return 0;
}
维护前缀最小值和最大值
我们发现正常树状数组维护的是前缀和,其实树状数组可以维护前缀和是因为前缀和满足结合律,因为区间最值不满足结合律树状数组不能维护区间最值。虽然可以比线段树多一个 \(\log\),但是为什么不写线段树呢。
其实如果不带修的话,直接数组就可以维护就可以维护前缀最值。
但其实树状数组维护前缀最值大值的时候只能将一个数修改为比它大的数,因为树状数组不像线段树一样是真的把值进行修改,树状数组只是修改的前缀。所以当一个数要加一个数的时候,只能在原数组里先找到这个数,然后加完时候直接修改为新数。
其实维护前缀最小也是只能维护修改为比原数小的数。
至于维护后缀最值,只需要将数组的下标翻转一下就可以了,其实就是用最大的下标 \(+1\) 减去当前下标。
值域树状数组
树状数组最基础的操作是单点修改查询前缀,这个操作你会发现和权值线段树很像,而且在对值域建树状数组之后,你会发现现在对于一个数的前缀和其实就是小于等于当前数的个数,也就是这个数的排名。我们也可以通过翻转值域,其实就是用值域 \(\max+1\) 减去当前的数,来实现维护大于等于当前数的个数的数。
当然可以和维护前缀最值结合起来,用来优化 \(DP\) 。值域怎样维护比当前值大的值的后缀,我们还需要翻转值域。
至于怎样维护第 \(k\) 大,我们可以用倍增树状数组来实现。
先咕着...