树状数组复习

树状数组复习


功能

  1. 快速求前缀和 O ( l o g n ) O(logn) O(logn)
  2. 快速修改某数 O ( l o g n ) O(logn) O(logn)

原理

二进制拆分

x = 2 i k + 2 i k − 1 + ⋯ + 2 i 1 x=2^{i_k}+2^{i_{k-1}}+\dots+2^{i_1} x=2ik+2ik1++2i1

i k ≥ i k − 1 ≥ ⋯ ≥ i 1 i_k \geq i_{k-1} \geq \dots \geq i_1 ikik1i1

k ≤ l o g 2 x k \leq log_2x klog2x

  1. ( x − 2 i 1 , x ] (x-2^{i_1},x] (x2i1,x]

  2. ( x − 2 i 1 − 2 i 2 , x − 2 i 1 ] (x-2^{i_1} -2^{i_2} , x-2^{i_1}] (x2i12i2,x2i1]

    .

    .

    .

  3. ( 0 , x − 2 i 1 − 2 i 2 − ⋯ − 2 i k − 1 ] (0,x-2^{i_1}-2^{i_2}-\dots-2^{i_k-1}] (0,x2i12i22ik1]

显然 2 i 2^i 2i就是1<<lowbit(i)

为了求 ( l , r ] (l,r] (l,r]的和

我们用C[R]表示sum[R-lowbit(R)+1][R]

所以我们最多有 n n n个区间

每次查询 ( l , r ] (l,r] (l,r]仅需查最多 l o g n logn logn个数

似乎不是很直观

放个图:

在这里插入图片描述

操作

初始化

for(int i=1;i<=n;i++) add(i,a[i])

复杂度 O ( l o g n ) O(logn) O(logn)

比较暴力…不过一般可以接受的

还有一种 O ( n ) O(n) O(n)的,不过比较麻烦

去求x的每个儿子是啥,利用儿子加边(只加树边)

for(int i=x-1;i;i-=lowbit(i)) c[x]+=c[i];

O ( n ) O(n) O(n)

还有一种比较简单的 O ( n ) O(n) O(n)的做法

从定义出发,c[x]=sum[x-lowbit(x)][x]

所以预处理一个前缀和

然后直接c[x]=s[x]-s[x-lowbit(x)]就OK了

也非常简单

修改

对于修改操作

我们需要找到被修改的那个数影响到了哪些区间

其实就是找到该数的说有父节点

找父节点的操作:

p=x+lowbit(x)

最多改 l o g n logn logn

for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;

查询

对于查询操作

每次累加lowbit(x)

最多加 l o g n logn logn

for(int i=x;i;i-=lowbit(i)) sum+=tr[i];

应用

楼兰图腾

思路

把所有情况分成几类:

最低点是纵坐标 1 , 2 , 3 , … , n 1,2,3,\dots,n 1,2,3,,n

用乘法原理就可以了

首先从左到右扫一遍,预处理处一个greater[k],即有多少点纵坐标比 y k y_k yk

同理,再求一个lower[k],有多少点比 y k y_k yk大就OK了

树状数组tr[i]保存的是坐标为i的点的个数

高度是1到n,因此不用离散化

代码

/*************************************************************************
    > File Name: p241楼兰图腾.cpp
    > Author: typedef
    > Mail: 1815979752@qq.com 
    > Created Time: 2020/11/21 21:00:02
 ************************************************************************/
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+3;
typedef long long ll;
int n;
int a[N];
int tr[N];
int great[N],lower[N];
int lowbit(int x){
	return x&-x;
}
void add(int x,int c){
	for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
	return;
}
int sum(int x){
	int res=0;
	for(int i=x;i;i-=lowbit(i)) res+=tr[i];
	return res;
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<=n;i++){
		int y=a[i];
		great[i]=sum(n)-sum(y);//有多少点纵坐标位于y~n之间
		lower[i]=sum(y-1);//有多少点纵坐标比y小
		add(y,1);
	}
	memset(tr,0,sizeof(tr));
	ll res1=0,res2=0;//v,^;
	for(int i=n;i>=1;i--){
		int y=a[i];
		res1+=great[i]*(ll)(sum(n)-sum(y));
		res2+=lower[i]*(ll)(sum(y-1));
		add(y,1);
	}
	printf("%lld %lld\n",res1,res2);
	system("pause");
	return 0;
}

[谜一样的牛](244. 谜一样的牛 - AcWing题库)

思路

假设 a 1 , a 2 , … , a n a_1,a_2,\dots,a_n a1,a2,,an 为比第 i i i 头牛矮的牛的数量

容易得到 h n = a n + 1 h_n = a_n +1 hn=an+1

然后依次往前推

若当前推到了第 i i i 头牛

意味着该头牛的身高实在它前面 i − 1 i-1 i1 t头牛中排行第 a i + 1 a_i+1 ai+1

这是我们只需找前面第 a i + 1 a_i+1 ai+1小 的数

我们需要完成如下操作

  • 每次操作就是从剩余的数中找出第 k k k 小的数

  • 删除某个数

显然要可以平衡树维护

但是我只会树状数组/kk

我们在建树的时候把每个身高设为一

用树状数组维护这个全一数组的前缀和

这个 1 1 1 表示该身高还可被用 1 1 1

删除操作就是在该位置减一

很容易实现n

那么如何完成第一个操作呢?

单用树状数组是无法完成的

不过,剩余的数是可以查询前缀和求出来的

不难发现:

操作一 ⟺ \Longleftrightarrow 找到一个最小的 x x x ,满足sum(x)==k

前缀和是单调递增的,因此可以二分查找

那么接下来就很简单了

复杂度 O ( m l o g 2 n ) O(mlog^2n) O(mlog2n) ,常数不大,甚至能跑赢平衡树

代码

/*************************************************************************
    > File Name: p244谜一样的牛.cpp
    > Author: typedef
    > Mail: 1815979752@qq.com 
    > Created Time: 2020/11/24 22:09:46
 ************************************************************************/
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int n;
int h[N];
int ans[N];
int tr[N];
int lowbit(int x){
	return x&-x;
}
void add(int x,int c){
	for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
	return;
}
int sum(int x){
	int res=0;
	for(int i=x;i;i-=lowbit(i)) res+=tr[i];
	return res;
}
int main(){
	scanf("%d",&n);
	for(int i=2;i<=n;i++) scanf("%d",&h[i]);
	for(int i=1;i<=n;i++) tr[i]=lowbit(i);
	for(int i=n;i>=1;i--){
		int k=h[i]+1;
		int l=1,r=n;
		while(l<r){
			int mid=l+r>>1;
			if(sum(mid)>=k) r=mid;
			else l=mid+1;
		}
		ans[i]=r;
		add(r,-1);
	}
	for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
	system("pause");
	return 0;
}

进阶玩法ht

区间修改,单点查询

思路

为什么不用线段树?

其实很简单,利用差分思想

原数组a[N],树状数组(差分)b[N<<1]

操作 [ l , r ] + c [l,r]+c [l,r]+c ⟺ \Longleftrightarrow b[l]+=c,b[r+1]-=c

查询a[x] ⟺ \Longleftrightarrow b [ 1 ] + b [ 2 ] + ⋯ + b [ x ] b[1]+b[2]+\dots+b[x] b[1]+b[2]++b[x]

代码

/*************************************************************************
    > File Name: 树状数组pro.cpp
    > Author: typedef
    > Mail: 1815979752@qq.com 
    > Created Time: 2020/11/21 21:44:55
 ************************************************************************/
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+7;
typedef long long ll;
int n,m;//区间长度&操作个数
int a[N];
ll tr[N];
int lowbit(int x){
	return x&-x;
}
void add(int x,int c){
	for(int i=c;i<=n;i+=lowbit(i)) tr[i]+=c;
	return;
}
ll sum(int x){
	ll res=0;
	for(int i=x;i;i-=lowbit(i)) res+=tr[i];
	return res;
}
int main(){
	memset(tr,0,sizeof(tr));
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<=n;i++) add(i,a[i]-a[i-1]);
	while(m--){
		char op[2];
		int l,r,d;
		scanf("%s%d",op,&l);
		if(*op=='C'){
			scanf("%d%d",&r,&d);
			add(l,d),add(r+1,-d);
		}
		else{
			printf("%lld\n",sum(l));
		}
	}
	system("pause");
	return 0;
}

区间修改,区间查询

思路

线段树它不香吗?

修改:

操作 [ l . r ] + c [l.r]+c [l.r]+c ⟺ \Longleftrightarrow b[l]+=c,b[r+1]-=c

还算简单吧…

查询:

首先考虑 a 1 + a 2 + ⋯ + a x a_1+a_2+\dots+a_x a1+a2++ax

原式 = = = ∑ i = 1 x \sum^{x}_{i=1} i=1x = = = ∑ i = 1 x ∑ j = 1 i b i \sum^{x}_{i=1} \sum^{i}_{j=1} bi i=1xj=1ibi

for(int i=1;i<=x;i++)
    for(int j=1;j<=i;j++)
        +bj

还比较好理解吧…

①:

numvalue
1 b 1 b_1 b1
2 b 1 + b 2 b_1+b_2 b1+b2
3 b 1 + b 2 + b 3 b_1+b_2+b_3 b1+b2+b3
x b 1 + b 2 + ⋯ + b x b_1+b_2+\dots+b_x b1+b2++bx

这玩意像个三角形

直接求不好求

运用补集思想

②:

numvalue
0 b 1 + b 2 + ⋯ + b n b_1+b_2+\dots+b_n b1+b2++bn
1 b 2 + b 3 + ⋯ + b n b_2+b_3+\dots+b_n b2+b3++bn
2 b 3 + b 4 + ⋯ + b n b_3+b_4+\dots+b_n b3+b4++bn
3 b 4 + b 5 + ⋯ + b n b_4+b_5+\dots+b_n b4+b5++bn
x 0 0 0

③:

numvalue
0 b 1 + b 2 + ⋯ + b n b_1+b_2+\dots+b_n b1+b2++bn
1 b 1 + b 2 + ⋯ + b n b_1+b_2+\dots+b_n b1+b2++bn
2 b 1 + b 2 + ⋯ + b n b_1+b_2+\dots+b_n b1+b2++bn
3 b 1 + b 2 + ⋯ + b n b_1+b_2+\dots+b_n b1+b2++bn
x b 1 + b 2 + ⋯ + b n b_1+b_2+\dots+b_n b1+b2++bn

我们让 ③-② 可得 ①

因此①= ( b 1 + ⋯ + b x ) × ( x + 1 ) − ( b 1 + 2 b 2 + 3 b 3 + ⋯ + x b x ) (b_1+\dots+b_x)\times(x+1)-(b_1+2b_2+3b_3+\dots+xb_x) (b1++bx)×(x+1)(b1+2b2+3b3++xbx)

前面用 b i b_i bi 的前缀和求,后面用 i × b i i \times b_i i×bi 的前缀和

因此

我们不得不用

tr1[]表示 b i b_i bi 的前缀合

tr2[]表示 i × b i i \times b_i i×bi 的前缀合

然后就很简单了

真TM的恶心

代码

这玩意儿写起来真上头

小心爆int

/*************************************************************************
    > File Name: 树状数组promax.cpp
    > Author: typedef
    > Mail: 1815979752@qq.com 
    > Created Time: 2020/11/22 21:18:03
 ************************************************************************/
#include<bits/stdc++.h>
using namespace std;
const int N=100007;
typedef long long ll;
ll tr1[N],tr2[N];//b数组前缀和,bi乘i的前缀和;
int a[N];
int n,m;
int lowbit(int x){
	return x&-x; 
}
void add(ll tr[],int x,ll c){
	for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
	return;
}
ll sum(ll tr[],int x){
	ll res=0;
	for(int i=x;i;i-=lowbit(i)) res+=tr[i];
	return res;
}
ll prefix_sum(int x){
	return sum(tr1,x)*(x+1)-sum(tr2,x);
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",a+i);
	for(int i=1;i<=n;i++){
		int b=a[i]-a[i-1];
		add(tr1,i,b);
		add(tr2,i,(ll)i*b);
	}
	while(m--){
		char op[2];
		int l,r,d;
		scanf("%s%d%d",op,&l,&r);
		if(*op=='Q'){
			printf("%lld\n",prefix_sum(r)-prefix_sum(l-1));
		}
		else{
			scanf("%d",&d);
			add(tr1,l,d);
			add(tr1,r+1,-d);
			add(tr2,l,l*d);
			add(tr2,r+1,(r+1)*-d);
		}
	}
	return 0;
}
posted @ 2020-11-21 22:48  actypedef  阅读(25)  评论(0编辑  收藏  举报