树状数组总结

此篇文章为学习自

https://www.acwing.com/blog/content/513/

https://www.cnblogs.com/xenny/p/9739600.html 

之后再三推敲打磨而来,个人水平有限,如有谬误,敬请指出,如觉不足,请移步上文继续研究

用途:解决大部分基于区间上的更新以及求和问题。
与线段树的区别:树状数组可以解决的问题都可以用线段树解决,树状数组的系数要少很多,
线段树就像 华为荣耀50, 而树状数组就像华为荣耀50的青春版,便宜了,但性能弱了,但对于一些问题性价比更高。

tree[N]从1开始而不是0

目录

一: 单点更新 区间求和的树状数组

(一)纯度*

(二)求前缀和

(三)单点更新

(四)单点查询

(五) 区间求和

(六)将数组元素缩放一个常数因子

(七)求前缀和恰好等于target的index 

(八)例题

(九)模板

二 区间修改 单点查询的树状数组

(一)区间修改

(二)单点查询

(三)模板

(四)例题

三 区间修改 区间查询

模板

例题

四 二维树状数组

定义

(一)单点修改 求前缀和

 (二) 区间修改 单点查询

(三)区间修改 区间查询

(四) 例题

五 求逆序数

该背景下树状数组的含义       

如何使用树状数组求逆序数总数

例题


一: 单点更新 区间求和的树状数组

(一)纯度*


单点更新:修改数组中的某一个数字
区间求和:求某一个区间的数字之和 

上图纵轴是数据大小,横轴是纯度大小,以左、上为正向

由此引入一个理解树状数组很重要的概念: 

 纯度:数字对应二进制中最低的非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

二维树状数组详解https://www.cnblogs.com/dilthey/p/9366491.html#c

定义

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值