树状数组——在高阶前缀和上的应用
HDU中超第五场打完后补题,被一个高阶前缀和折磨了两天
记录一下想法
树状数组——高阶前缀和
首先强化一下概念:树状数组的基本(根本)操作是单点修改,单点查询前缀和
通过对前缀和做小学减法我们做到了区间查询
现在要求对 \(a\) 数组做单点修改,查询 \(a\) 数组的二维前缀和
设 \(b,c\) 两数组分别为一阶、二阶前缀和
\[\begin{align}
b_i &= \sum_1^i a_i,\quad c_i = \sum_1^ib_i\\
c_i &= ia_1 + (i-1)a_2+...+2a_{i-1} + a_i\\
&= i(a_1+a_2+...+a_i) - [(i-1)a_i + (i-2)a_{i-1}+....+a2+0]
\end{align}
\]
这样就将一个二阶前缀和问题转化成了两个一阶前缀和问题
用两个树状数组,分别处理 \(a_i\) ,\({(i-1)a_i}\) 这两个序列即可
struct BitTree{//4行树状数组
ll t[2*maxn];
ll lb(int x) {return x&-x;}
void add(int p, ll ad){for(int i=p;i<=n;i+=lb(i)) t[i]+=ad;}
ll sum(int p){ return p?t[p] + sum(p-lb(p)):0;}
};
struct BitTree2{//二阶前缀和的树状数组
BitTree bt1, bt2;
void add(int p, ll ad){
bt1.add(p, ad);
bt2.add(p, ad * (p-1));
}
ll sum(int p){
return bt1.sum(p) * p - bt2.sum(p);
}
};
练习(模板题)
线段树最基础的模板题,区间修改区间查询,可以用二阶前缀和的树状数组做
具体而言,建立差分数组 \(d\) ,区间修改可以改为在 \(d\) 上的单点修改,那么区间查询就相当于查询 \(d\) 的二阶前缀和
BitTree2 bt;
ll d[maxn], n, m;
void solve(){
cin >> n >> m;
for(int i=1;i<=n;i++){
cin >> d[i];
bt.add(i, d[i]);
bt.add(i+1, -d[i]);
}
for(int i=1;i<=m;i++){
int opt,x,y,k;
cin >> opt >> x >> y;
if(opt == 1){
cin >> k;
bt.add(x, k);
bt.add(y+1, -k);
}else{
cout << bt.sum(y) - bt.sum(x-1) << '\n';
}
}
}
更高阶呢?
以三阶前缀和为例
\[\begin{align}
b_i &= \sum_1^i a_i,
\quad c_i = \sum_1^ib_i,
\quad d_i = \sum_1^ic_i
\\
d_k &= \frac{k(1+k)}{2}a_1 + \frac{(k-1)k}{2}a_2+...+\frac{(2+k-i)*(1+k-i)}{2}a_i+...+\frac{3}{2}a_{k-1} + a_k
\\
&= \frac{(k+1)(k+2)}{2}(a_1+...a_k)-\frac{2k+3}{2}(a_1+2a_2+...+ka_k)+\frac{1}{2}(a_1+4a_2+...+k^2a_k)
\end{align}
\]
推方程的时候,对于一般项 \(\frac{(2+k-i)*(1+k-i)}{2}a_i\),要注意把 \(k\) 和 \(i\) 分开,目的是使某个\(f(i)a_i\) 形式的东西独立出来,方便单独用树状数组维护。
这里的过程是:
\[\frac{(2+k-i)*(1+k-i)}{2}a_i = \frac{(k+1)(k+2)}{2}a_i-\frac{2k+3}{2}ia_i+\frac{1}{2}i^2a_i
\]
由此也成功转化成三个一阶前缀和问题
如果继续推下去,会发现 \(n\) 个树状数组就可以解决 \(n\) 阶前缀和问题
练习题(比赛原题)
三阶前缀和
题解见自己写的 2021暑假 HDU中超 第五场 1009