此篇文章为学习自
https://www.acwing.com/blog/content/513/
https://www.cnblogs.com/xenny/p/9739600.html
之后再三推敲打磨而来,个人水平有限,如有谬误,敬请指出,如觉不足,请移步上文继续研究
用途:解决大部分基于区间上的更新以及求和问题。
与线段树的区别:树状数组可以解决的问题都可以用线段树解决,树状数组的系数要少很多,
线段树就像 华为荣耀50, 而树状数组就像华为荣耀50的青春版,便宜了,但性能弱了,但对于一些问题性价比更高。
tree[N]从1开始而不是0
目录
一: 单点更新 区间求和的树状数组
(一)纯度*
单点更新:修改数组中的某一个数字
区间求和:求某一个区间的数字之和
上图纵轴是数据大小,横轴是纯度大小,以左、上为正向
由此引入一个理解树状数组很重要的概念:
纯度:数字对应二进制中最低的非0位的位置,tree[i]展开的项数
项数:假设一维数组为Ai,则与它对应的树状数组Ci是这样定义的
C1 = A1
C2 = A1 + A2
C3 = A3
C4 = A1 + A2 + A3 + A4
C5 = A5
C6 = A5 + A6
C7 = A7
C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
......
C16 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8 + A9 + A10 + A11 + A12 + A13 + A14 + A15 + A16
......
例如10=1010B,最低位是10B,如果我们提出10,就得到如下代码
inline int lowbit(int x) { //求取转化为二进制时最低位1+一串0的
return x&(-x);
}
为什么呢?
纯度是划分树状数组区间的关键要素,只有深刻理解了有关纯度的操作,才能透彻地把握树状数组这棵树的特征脉络。因此,我们有必要深研关于纯度的一系列操作。下面我们分别用柱状图、树状图两个维度理解纯度:
如上图所示,[1,16]是一个总区间,也是最大区间,下面将上面的柱状图转化为树的形式呈现 最底层纯度为1,往上逐层+1,右边为二进制
[1,16] 16:10000
[1,8] 8:1000
[1,4] [9,12] 4:100 12:1100
[1,2] [5,6] [9,10] [13,14] 2:10 6:110 10:1010 14:1110
[1] [3] [5] [7] [9] [11] [13] [15] 1:1 3:11 5:101 7:111 9:1001 11:1011 13:1101 15:1111
每一个标红的数字代表着这个区间是tree[i],比如[1,4]被4标红,那么tree[4]=[1,4]
由标红的数字和右边转化为二进制可以看出
- 一个数的纯度是几,他就在第几层。比如16纯度为5,他就在第五层。奇数第一位一定是1,所以奇数一定在底层,他不代表区间和,他只代表自己的数值
- 2^k一定可以沿着根节点的左子树找到
- 对于偶数来说,数值的大小关系与纯度的大小关系无关,例如12<14,P(12)>P(14);4<12,P(4)==P(12);12<16,P(12)<P(16)
根据这棵树按纯度排层数的原理,我们可以以此为基写出一系列算法。
(二)求前缀和
如果我们要求前缀和,tree[N]代表的是区间和,求前缀和就是累加区间和,由上面的树型结构可以看出,我们不能从小到大累加(因为奇数都在最底层,偶数代表区间和,tree[1]+tree[2]+tree[3]是什么?是a[1]+a[1]+a[2]+a[3],而不是a[1]+a[2]+a[3]),我们只能从大到小累加。
比如求[1,9],那么根据上面的特征,我们清楚地知道,9是奇数,一定在最底层,8是2的k次方,一定在某一层的最左边,在左子树意味着他从1开始。那么我们要从9开始升纯累加。即sum(9)=tree[9]+tree[8]
再举个例子,求[1,11],那么s(11)=tree[11] + tree[10] ([9,10]) + tree[8] ([1,8]) 这么说似乎有点逻辑不清晰,11=1011B,对11升纯得1010B=10,对10升纯得1000B=8
由树和实例可以很容易地归纳出求前缀和的算法:累加向下升纯
int sum(int idx)//求前缀和 累加升纯 Sum(1~idx)
/*将当前纯度tree[idx]的值加入到答案中
提高idx的纯度
重复上述操作直至idx<=0 */
{
int s = 0;
while(idx > 0) {
s += tree[idx];
idx -= (idx & -idx);// downpurity(idx);
}
return s;
}
升纯:使纯度+1,即使二进制最低位1变为0。从树状区间上表现为:以1为正方向,从一个区间到下一个相邻区间。
向上升纯:升纯后数值变大的操作,idx+=lowbit(idx)
向下升纯:升纯后数值变小的操作,idx-=lowbit(idx)
设操作数为x,转化为二进制数,则x由a10变为a00,或由a10变为a100(a是第一个1之前的一系列二进制数)。升纯是有方向的。以向下升纯为例,下一个区间与这一个区间紧密相邻,例如对11降纯是10,tree[11]在数轴上表示11,tree[10]在数轴上表示9,10。以1为正方向,把1~11划分成一个个区间就是[1,8][9,10][11]。从树形结构上来说,这会使得x更靠近根节点。
代码如下:
inline int uppurity(int &x){//向上升纯 return x+=lowbit(x); } inline int downpurity(int &x){//向下升纯 return x-=lowbit(x); }
(三)单点更新
如果我们要单点更新,修改一个元素后要往上把包含这个元素的区间也给修改掉,比如修改3,那么[1,4]包含3,也要被修改,往上[1,8],[1,16]也要被修改。3=11B,4=100B,8=1000B;也就是说,单点更新就是修改向上升纯的一个过程,一般题目给的修改都是加delta,也就是增加向上升纯
为了方便单点查询,我们也顺便更新原数组
void updata(int idx,int delta)// 单点更新 对原数组直接修改,对树状数组增加向上升纯 注意这里的delta是变化值而不是修改后的值
/*修改tree[idx] 的值
将idx值加上idx&(-idx) (提高纯度)
重复上述操作直至idx>n */
{
A[idx]+=delta;
while(idx <= n) {
tree[idx] += delta;
idx += (idx & -idx);
}
}
(四)单点查询
如果我们想获取数组元素A[idx]的值,由于我们更新的时候也维护了原数组,那么直接查询原数组即可,最快了
inline int query(int idx) {//单点查询 由于更新原数组,因此直接查询原数组即可
return A[idx];
}
(五) 区间求和
基于上述思想我们可以得到区间求和的算法:
当log(i-1)!=log(j)时,无法剪枝,直接sum(j)-sum(i-1)即可
当二者相等时,有可能可以剪枝
下面的代码在这一块已经很优秀了,没必要继续优化
int sumRange(int i,int j) { //区间求和sum[i,j] 虽然说有些情况下会多运算几次,但是已经很优秀了
int res = 0;
i --;
while(i != j) {
res = res + tree[j] - tree[i];
i -= lowbit(i);
j -= lowbit(j);
}
return res;
}
如果升纯过程中i-1与j相遇,就不必继续了
相遇不了就算了,优化之后特殊情况下最多少运算几次,普遍情况不值当,如果有更优秀的普适性优化算法请在评论区指教
(六)将数组元素缩放一个常数因子
对于一个数列,数列中的每一个元素乘以一个系数k,则数列任意区间和也要乘以一个系数k
void scale(int c,int MaxIdx){
for (int i = 1 ; i <= MaxIdx ; i++)
tree[i] /= c;
A[i]/=c;
}
(七)求前缀和恰好等于target的index
如果数组中的元素有正有负我们只能遍历所有的idx,使用read(idx)函数求解,时间复杂度O(nlogn)。
如果数组中的元素只有非负数,那么我们可以利用二分的思想来求解,时间复杂度O(lognlogn)
(八)例题
https://blog.youkuaiyun.com/qq_54886579/article/details/119469202
(九)模板
#include<cstdio>
#include<iostream>
using namespace std;
const int N=1e6+1,M=1e6+1;
int tree[N],A[N];//A[]为原数组
int A[M][N],Tree[M][N];//二维树状数组
int n;//n为要用到的规模
int m;//m,n为二维数组要用到的x,y规模
inline int read() {
int s=0,w=1;
char c=getchar();
while(c<'0' || c>'9') if(c=='-') w*=-1,c=getchar();
while(c>='0' && c<='9') s+=(s<<3)+(s<<1)+c-'0';
return s;
}
inline int lowbit(int x) { //求取转化为二进制时最低位1+一串0的数
return x&(-x);
}
void updata(int idx,int delta)// 单点更新 对原数组直接修改,对树状数组增加向上升纯 注意这里的delta是变化值而不是修改后的值
/*修改tree[idx] 的值
将idx值加上idx&(-idx) (提高纯度)
重复上述操作直至idx>n */
{
A[idx]+=delta;
while(idx <= n) {
tree[idx] += delta;
idx += (idx & -idx);
}
}
inline void init() { //初始化
for(int i=1; i<=n; i++) {
int delta=read();
updata(i,delta);
}
}
int sum(int idx)//求前缀和 累加向下升纯 Sum(1~idx)
/*将当前纯度tree[idx]的值加入到答案中
提高idx的纯度
重复上述操作直至idx<0 */
{
int s = 0;
while(idx > 0) {
s += tree[idx];
idx -= (idx & -idx);
}
return s;
}
inline int query(int idx) {//单点查询 由于更新原数组,因此直接查询原数组即可
return A[idx];
}
int sumRange(int i,int j) { //区间求和sum[i,j] 虽然说有些情况下会多运算几次,但是已经很优秀了
int res = 0;
i --;
while(i != j) {
res = res + tree[j] - tree[i];
i -= lowbit(i);
j -= lowbit(j);
}
return res;
}
inline void scale(int c,int MaxIdx) { //将数组元素缩放一个常数因子
for (int i = 1 ; i <= MaxIdx ; i++)
tree[i] = tree[i] / c;
}
//二维树状数组 A[M][N]定义在全局变量 执行两种操作:修改(x,y)的值 求以(x,y)为右上角,(1,1)为左下角的矩形区域总和
void updata_(int x,int y,int delta) { //单点更新
A[x][y]+=delta;
for(int i=x; i<=m; i+=lowbit(i))
for(int j=y; j<=n; j+=lowbit(j))
Tree[i][j]+=delta;
}
void init_() { //初始化
for(int x=1; x<=m; x++)
for(int y=1; y<=n; y++) {
int delta=read();
updata_(x,y,delta);
}
}
void sum_(int x,int y) { //求和
int sum = 0;
for(int i = x; i >= 1; i -= lowbit(i))
for(int j = y; j >= 1; j -= lowbit(j))
sum += Tree[i][j];
return sum;
}
int main() {
return 0;
}
二 区间修改 单点查询的树状数组
在这个问题中我们需要完成的任务是:
- 将一个区间内的数字增加同样一个值
- 求某一个位置的值。
(一)区间修改
在这里我们可以利用差分的思想,假设初始数组为A,我们首先构造一个关于A的差分数组C,其中C[1] = A[1],C[i] = A[i] - A[i -1]。
那么我们想将区间[L,R]内的数字增加同样一个值,那么我们只需要修改差分数组中两个位置的元素C[L]=C[L]+delta,C[R+1]=C[R+1]−delta。
推导:这里以1为起始位
A[i]=A[i-1]+C[i](i>1)
A[1]=C[1]
若C[L]+=delta 则A[L]+=delta
∵A[L+1]=A[L]+C[L+1],∴A[L+1]+=delta
∴A[L+2]~A[R+1]+=delta
若C[R+1] - =delta
则A[R+1]=A[R]+C[R+1],由于A[R]大了delta,C[R+1]小了delta,所以A[R+1]不变
由于C[R+2]~C[N]不变,所以A[R+2]~A[N]不变,区间修改完成
由此,我们将区间修改转化为了单点更新
(二)单点查询
如果我们想要求某一个位置的值A[idx],那么A[idx]=sumRange(C[1,idx])。
推导:
A[1]=C[1],A[2]=A[1]+C[2]=C[1]+C[2]
现要证A[i]=A[i-1]+C[i]=ΣC[j] (1<=j<=i)
数学归纳法,当i=1,2时上式成立
假设当i=n-1时上式成立,即有A[n-1]=ΣC[j] (1<=j<=n-1)
现要证i=n时上式成立,A[n]=A[n-1]+C[n]=ΣC[j]+C[n] (1<=j<=n-1)=ΣC[q] (1<=q<=n)显然成立
证毕
由此,我们将单点查询转化为了求前缀和
基于上述思想,我们这里的树状数组tree是关于差分数组C的。我们用树状数组维护差分数组C。
(三)模板
//————————————区间修改 单点查询——————————————————
#include<cstdio>
#include<iostream>
using namespace std;
const int N=1e6+10;
int C[N],A[N],tree[N];//C[]为差分数组 A[]为原数组 tree[]是关于差分数组C的而不是A (也就是说用tree管理C) 初始化之后A,C就没用了,之后全部操作用tree完成
int n;//n为要用到的数组规模
inline int read(){
int s=0,w=1;char c=getchar();
while(c<'0' || c>'9') if(c=='-') w*=-1,c=getchar();
while(c>='0' && c<='9') s+=(s<<3)+(s<<1)+c-'0';
return s;
}
inline int lowbit(int x){
return x&-x;
}
inline void updata(int idx,int delta){//单点更新 易错:这不是读入数据!
while(idx<=n){
tree[idx]+=delta;
idx+=lowbit(x);
}
}
inline void init(){//初始化
for(int i=1;i<=n;i++){
A[i]=read();
C[i]=A[i]-A[i-1];
updata(i,C[i]);
}
}
/*区间修改:在这里我们可以利用差分的思想,假设初始数组为A,我们首先构造一个关于A的差分数组C,其中C[1] = A[1],C[i] = A[i] - A[i -1]。
那么我们想将区间[L,R]内的数字增加同样一个值,那么我们只需要修改差分数组中两个位置的元素C[L]=C[L]+delta,C[R+1]=C[R+1]-delta。
由此,我们将对原数组的区间修改转化为对差分数组的单点更新 */
void updataRange(int L,int R,int delta){
updata(L,delta);
updata(R+1,-delta);
}
/*单点查询:如果我们想要求某一个位置的值A[idx],那么A[idx]=sumRange(C[1,idx])。由此,我们将单点查询转化为求前缀和*/
int query(int idx){
int s=0;
while(idx>0){
s+=tree[idx];
idx-=lowbit(idx);
}
return s;
}
int main(){
return 0;
}
(四)例题
https://blog.youkuaiyun.com/qq_54886579/article/details/119493374
三 区间修改 区间查询
在这个任务中,我们需要解决以下问题:
- 将一个区间内的值添加同样的数值
- 查询一个区间的和
上述问题可以使用线段树解决,但是稍微改进我们的树状数组也是可以完成上述任务的。同样的我们使用上一节中定义的差分数组C。我们知道,如果我们想求解一个S=A[1:n]前缀和的话,有:
因此我们可以使用另一个辅助数组D来维持D[i]=i∗C[i]的值,整体代码和上述代码相似。
先查询前缀和,再r-(l-1)即可
模板
//区间修改、区间查询
#include<cstdio>
#include<iostream>
using namespace std;
const int N=1e6+10;
typedef long long ll
ll A[N],tree1[N],tree2[N];//C[]为差分数组,我们用树状数组维护差分数组 A[]为原数组 D[N]为辅助数组i*C[i] ,tree[1]维护C,tree2维护D
int n;//n为要用到的数组规模
inline int read(){
int s=0,w=1;char c=getchar();
while(c<'0' || c>'9'){
if(c=='-') w*=-1;
c=getchar();
}
while(c>='0' && c<='9') s=(s<<3)+(s<<1)+c-'0',c=getchar();//易错:不能写成s+=
return s*w;
}
inline int lowbit(int x){
return x&-x;
}
void update(int x,int delta){
int k=x;//易错点
while(x<=n){
tree1[x]+=delta;
tree2[x]+=k*delta;//易错点:更新时是k*delta,系数不能变
x+=lowbit(x);
}
}
inline void init(){
for(int i=1;i<=n;i++){
A[i]=read();
update(i,A[i]-A[i-1]);//不要写成A[i]
}
}
void updateRange(int l,int r,int delta){
update(l,delta);
update(r+1,delta);
}
ll query(int x){/*前缀和S=(n + 1)*(C[1] + C[2]+...+C[n]) - (1*C[1]+2*C[2]+...+n*C[n])
=(n+1)*ΣC[n]-ΣD[i]
注意,用tree1和tree2进行计算! */
ll s=0;
int n=x+1;
while(x>0){
s+=n*tree1[x]-tree2[x];
x-=lowbit(x); //易错点 忽略该句话
}
return s;
}
inline ll queryRange(int l,int r){
return (query(r)-query(l-1));
}
int main(){
return 0;
}
例题
https://blog.youkuaiyun.com/qq_54886579/article/details/119495056
四 二维树状数组
例题https://www.luogu.com.cn/problem/solution/P4514
二维数组前缀和https://blog.youkuaiyun.com/justidle/article/details/103754960
二维树状数组详解https://blog.youkuaiyun.com/zzti_xiaowei/article/details/81053094
定义
C[x][y] = ∑ a[i][j], 其中,
x-lowbit(x) + 1 <= i <= x,
y-lowbit(y) + 1 <= j <= y.
例:举个例子来看看C[][]的组成。
设原始二维数组为:
A[][]={
{a11,a12,a13,a14,a15,a16,a17,a18,a19},
{a21,a22,a23,a24,a25,a26,a27,a28,a29},
{a31,a32,a33,a34,a35,a36,a37,a38,a39},
};
那么它对应的二维树状数组C[][]呢?
记:
B[2]={a21,a21+a22,a23,a21+a22+a23+a24,a25,a25+a26,...} 这是第二行的一维树状数组
B[3]={a31,a31+a32,a33,a31+a32+a33+a34,a35,a35+a36,...} 这是第三行的一维树状数组
B[4]={a41,a41+a42,a43,a41+a42+a43+a44,a45,a45+a46,...} 这是第四行的一维树状数组
那么:
C[1][1]=a11,C[1][2]=a11+a12,C[1][3]=a13,C[1][4]=a11+a12+a13+a14,c[1][5]=a15,C[1][6]=a15+a16,...
这是A[][]第一行的一维树状数组
C[2][1]=a11+a21,C[2][2]=a11+a12+a21+a22,C[2][3]=a13+a23,C[2][4]=a11+a12+a13+a14+a21+a22+a23+a24,
C[2][5]=a15+a25,C[2][6]=a15+a16+a25+a26,...
这是A[][]数组第一行与第二行相加后的树状数组
C[3][1]=a31,C[3][2]=a31+a32,C[3][3]=a33,C[3][4]=a31+a32+a33+a34,C[3][5]=a35,C[3][6]=a35+a36,...
这是A[][]第三行的一维树状数组
C[4][1]=a11+a21+a31+a41,C[4][2]=a11+a12+a21+a22+a31+a32+a41+a42,C[4][3]=a13+a23+a33+a43,...
这是A[][]数组第一行+第二行+第三行+第四行后的树状数组
从展开式的角度抽象地说,C[i][j]展开为A[m][n]的和,其中集合[m][n]为C[i]的展开式各项所形成的集合和C[j]的展开式各项所形成的集合的笛卡尔积
也就是说,二维集合就是两个一维的集合做笛卡尔积
拓展开来,n维集合就是n个一维的集合做笛卡尔积
如果用坐标图来表示,那么就是从每个轴上的对应点做平行于坐标面的关于坐标轴的垂线,每条线段的交点即为展开式中的一项
上述思想源于线性代数,不仅仅适用于树状数组,认清楚上述性质,n维树状数组手到擒来
例如C[4][2],【4】展开为(1,2,3,4),【2】展开为(1,2),两个集合做笛卡尔积运算,便得到
C[4][2]=a11+a12+a21+a22+a31+a32+a41+a42
(一)单点修改 求前缀和
void update_(int x,int y,int delta,int m,int n)//二维树状数组 A[M][N]定义在全局变量 m,n为需要用到的规模
{
A[x][y]+=delta;
for(int i = x ; i <= m ;i += lowbit(i))
{
for(int j = y;j <= n;j += lowbit(j))
{
Tree[i][j] += delta;
}
}
}
求和操作:
二维数组前缀和定义:从(1,1)到(x,y)的和,比如S(2,3)=(1,1)+(1,2)+(1,3)+(2,1)+(2,2)+(2,3)
void sum_(int x,int y)//求和
{
int sum = 0;
for(int i = x;i > 0 ; i -= lowbit(i))
{
for(int j = y;j > 0 ;j -= lowbit(j))
{
sum += Tree[i][j];
}
}
return sum;
}
比如:
Sun(1,1)=C[1][1]=A[1][1]; Sun(1,2)=C[1][2]=A[1][2]+A[1][1]; Sun(1,3)=C[1][3]+C[1][2]=A[1][3]+A[1][2]+A[1][1];...
Sun(2,1)=C[2][1]; Sun(2,2)=C[2][2]=A[2][2]+A[1][2]+A[2][1]+A[1][1]; Sun(2,3)=C[2][3]+C[2][2];...
Sun(3,1)=C[3][1]+C[2][1]; Sun(3,2)=C[3][2]+C[2][2]
(二) 区间修改 单点查询
https://www.cnblogs.com/RabbitHu/p/BIT.html
也就是说当前点的差分数组的值等于当前点的值减去(左+上-左上)的值
例如下面这个矩阵
1 4 8
6 7 2
3 9 5
对应的差分数组就是
1 3 4
5 -2 -9
-3 5 1
当我们想要将一个矩阵加上x时,怎么做呢?
下面是给最中间的3*3矩阵加上x时,差分数组的变化:
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 +x 0 0 -x
0 0 0 0 0
0 -x 0 0 +x
0 0 0 0 0
这样给修改差分,造成的效果就是:
0 0 0 0 0 x轴
0 0 0 0 0
0 0 0 0 0
0 x x x 0 (a,b)=(2,4)
0 x x x 0 (c,d)=(4,5)
0 0 0 0 0
0 0 0 0 0
y
轴
也就是说,给(a,b)~(c,d)加上x,相当于给差分数组(a,b)和(c+1,d+1)加上x,给(c+1,b)和(a,d+1)减去x
//tree是C的树状数组
void update(int x,int y,int delta){//单点修改,数组大小tree[n][m]
for(int i=x;i<=n;i+=lowbit(i))
for(int j=y;j<=m;j+=lowbit(j))
tree[x][y]+=delta;
void updateRange(int a,int b,int c,int d,int delta){//区间修改
//给(a,b)~(c,d)加上x,相当于给差分数组(a,b)和(c+1,d+1)加上x,给(c+1,b)和(a,d+1)减去x
update(a,b,delta);
update(c+1,d+1,delta);
update(a,d+1,-delta);
update(c+1,b,-delta);
}
int query(int x,int y){//单点查询
int ans=0;
for(int i=x;i<=n;i-=lowbit(i))
for(int j=y;j<=m;j-=lowbit(j))
ans+=tree[i][j];
return ans;
}
(三)区间修改 区间查询
不难想象那一行不要看上面三行去思考,看第一行,再把a拆成第二行的d去思考,可以采用定一维思考另一维的方式理解
(i+1-x)*(j+1-y)
=(i+1-x)*(j+1)-(i+1-x)*y
=(i+1)(j+1)-x*(j+1)-(i+1)*y+x*y
关于前缀和:https://blog.youkuaiyun.com/justidle/article/details/103754960
相当于下图中S(x1,x2)=S绿-X蓝-S黄+S灰
(S色是从色域顶点到坐标原点的全部区域面积,可不仅仅是涂色面积!!也就是说S(x1,x2)是纯绿色区域,S绿是绿蓝黄灰面积,S黄是黄灰面积,S灰是灰色面积,也就是说【绿色面积】=【绿+蓝+黄+灰】-【蓝+灰】-【黄+灰】+【灰】)
/*我们需要在原来 C1[i][j] 记录 d[i][j] 的基础上,再添加三个树状数组:
C2[i][j] 记录 d[i][j]*i
C3[i][j] 记录 d[i][j]*j
C4[i][j] 记录 d[i][j]*i*j
前缀和S(i,j)=(i+1)*(j+1)*Σd[x][y] - (j+1)*Σ(d[x][y]*x)-(i+1)*Σ (d[x][y]*y) +Σ(d[x][y]*x*y) Σx:1~i y:1~j
=(i+1)*(j+1)*ΣC1[x][y] - (j+1)*Σ(C2[x][y])-(i+1)*Σ (C3[x][y]) +Σ(C4[x][y])
最后,易知(x1,y1)到(x2,y2)的矩阵和就等于sum[x2][y2]-sum[x2][y1-1]-sum[x1-1][y2]+sum[x1-1][y1-1]。
*/
const int NN=1e3+100,M=1e3+100;
int m;
ll C1[NN][M],C2[NN][M],C3[NN][M],C4[NN][M];//二维差分辅助数组
void update(int x,int y,ll delta) { //单点修改,(x,y)+delta
for(int i=x; i<=n; i+=lowbit(i))
for(int j=y; j<=m; j+=lowbit(j)) {
C1[i][j]+=delta;
C2[i][j]+=delta*x;
C3[i][j]+=delta*y;
C4[i][j]+=delta*x*y;
}
}
void updateRange(int a,int b,int c,int d,ll delta) { //区间修改,(a,b)~(c,d)+delta
update(a,b,delta);
update(a,d+1,-delta);
update(c+1,b,-delta);
update(c+1,d+1,delta);
}
ll sum(int x,int y) { //查询左上角为(1,1)右下角为(x,y)的矩阵和 s=(i+1)*(j+1)*ΣC1[x][y] - (j+1)*Σ(C2[x][y])-(i+1)*Σ (C3[x][y]) +Σ(C4[x][y])
ll s=0;
for(int i=x; i>0; i-=lowbit(i)) {
for(int j=y; j>0; j-=lowbit(j)) {
s+=(x+1)*(y+1)*C1[i][j];
s-=(y+1)*C2[i][j]+(x+1)*C3[i][j];
s+=C4[i][j];
}
}
return s;
}
ll sumRange(int a,int b,int c,int d) { //查询左上角为(a,b)右下角为(c,d)的矩阵和
return sum(c,d)-sum(a-1,d)-sum(c,b-1)+sum(a-1,b-1);
}
(四) 例题
https://www.luogu.com.cn/problem/P4514
WA了几遍发现我把NM范围看错了,100%是小于2048,我以为是1024o(╥﹏╥)o
#include<cstdio>
#include<iostream>
using namespace std;
typedef long long ll;
const int NN=2e3+100,M=2e3+100;
int n,m;
ll C1[NN][M],C2[NN][M],C3[NN][M],C4[NN][M];//二维差分辅助数组
inline int lowbit(int x) {
return x&-x;
}
void update(int x,int y,ll delta) { //单点修改,(x,y)+delta
for(int i=x; i<=n; i+=lowbit(i))
for(int j=y; j<=m; j+=lowbit(j)) {
C1[i][j]+=delta;
C2[i][j]+=delta*x;
C3[i][j]+=delta*y;
C4[i][j]+=delta*x*y;
}
}
void updateRange(int a,int b,int c,int d,ll delta) { //区间修改,(a,b)~(c,d)+delta
update(a,b,delta);
update(a,d+1,-delta);
update(c+1,b,-delta);
update(c+1,d+1,delta);
}
ll sum(int x,int y) { //查询左上角为(1,1)右下角为(x,y)的矩阵和 s=(i+1)*(j+1)*ΣC1[x][y] - (j+1)*Σ(C2[x][y])-(i+1)*Σ (C3[x][y]) +Σ(C4[x][y])
ll s=0;
for(int i=x; i>0; i-=lowbit(i)) {
for(int j=y; j>0; j-=lowbit(j)) {
s+=(x+1)*(y+1)*C1[i][j];
s-=(y+1)*C2[i][j]+(x+1)*C3[i][j];
s+=C4[i][j];
}
}
return s;
}
ll sumRange(int a,int b,int c,int d) { //查询左上角为(a,b)右下角为(c,d)的矩阵和
return sum(c,d)-sum(a-1,d)-sum(c,b-1)+sum(a-1,b-1);
}
int a,b,c,d,delta,S;
int main() {
char Z;
while(1){
Z=getchar();
if(Z=='X') break;
}
scanf(" %d%d",&n,&m);
while(scanf("%c",&Z)!=EOF){
if(Z=='L'){
scanf(" %d%d%d%d%d",&a,&b,&c,&d,&delta);
updateRange(a,b,c,d,delta);
}else if(Z=='k'){
scanf(" %d%d%d%d",&a,&b,&c,&d);
S=sumRange(a,b,c,d);
printf("%d\n",S);
}
}
return 0;
}
五 求逆序数
https://www.cnblogs.com/xiongmao-cpp/p/5043340.html
在一个排列中,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序。一个排列中逆序数的总数就是这个排列的逆序数。
该背景下树状数组的含义
我们假设一个数组A[n],当A[n]=0时表示数字n在序列中没有出现过,A[n]=1表示数字n在序列中出现过。A对应的树状数组为c[n],则c[n]对应维护的是数组A[n]的内容,即树状数组c可用于求A中某个区间的值的和。
树状数组的插入函数(假设为 void insert(int i,int x) )的含义:在求逆序数这个问题中,我们的插入函数通常使用为insert( i , 1 ),即将数组A[i]的值加1 (A数组开始应该初始化为0,所以也可以理解为设置A[ i ]的值为1,即将数字i 加入到序列的意思 ),同时维护c数组的值。
树状数组中区间求和函数(假设函数定义为: int getsun(int i ) )的含义:该函数的作用是用于求序列中小于等于数字 i 的元素的个数。这个是显而易见的,因为树状数组c 维护的是数组A的值,则该求和函数即是用于求下标小于等于 i 的数组A的和,而数组A中元素的值要么是0要么是1,所以最后求出来的就是小于等于i的元素的个数。
所以要求序列中比元素a大的数的个数,可以用i - getsum(a)即可( i 表示此时序列中元素的个数)。
开一个验证存在性数组,求和得到比a小的数的数目s,用序列数i-s得到a的逆序数,利用状态转移方程累加逆序数得到总逆序
如何使用树状数组求逆序数总数
首先来看如何减小问题的规模:
要想求一个序列 a b c d,的逆序数的个数,可以理解为先求出a b c的逆序数的个数k1,再在这个序列后面增加一个数d,求d之前的那个序列中值小于d的元素的个数k2,则k1+k2即为序列a b c d的逆序数的个数。
举个例子加以说明:
假设给定的序列为 4 3 2 1,我们从左往右依次将给定的序列输入,每次输入一个数temp时,就将当前序列中大于temp的元素的个数计算出来,并累加到ans中,最后ans就是这个序列的逆序数个数。
代码很简单
const int N=5e3+10;
int c[N];
int n;
inline int lowbit(int i)
{
return i&(-i);
}
void update(int i,int x)//通常使用为update( i , 1 ),即将数组A[i]的值加1
{
while(i<=n){
c[i]+=x;
i+=lowbit(i);
}
}
int sum(int i)//求前缀和
{
int s=0;
while(i>0){
s+=c[i];
i-=lowbit(i);
}
return s;
}
例题
https://blog.youkuaiyun.com/qq_54886579/article/details/119537412