Flag:八月底开学前写完!!!
介绍
树状数组(Binary Indexed Tree)其实是一种简单的数据结构,因为简单易懂经常代替线段树来求数列的前缀和、区间和等
原理
很久很久以前,有一个聪明绝顶的人,想到每一个十进制数都可以二进制表示(相当于由几个次数不同的2的幂相加得到),那我们前缀和是否也可以按照相似的方法划分成几个子序列的和?——于是,树状数组就这样诞生!!!
从一个a[1]~a[8]的前缀和入手分析:
黑色矩形为A[i] (即原先的数列)
红色矩形为C[i](即我们维护的树状数组)
下面到了找规律的时间了~ 也许你会说:我只看到的只是起起落落(/doge)
下方括号里的两个数分别为十进制和二进制下的表示
C[1(0001)]=A[1]
C[2(0010)]=A[1]+A[2]
C[3(0011)]=A[3]
C[4(0100)]=A[1]+A[2]+A[3]+A[4]
C[5(0101)]=A[5]
C[6(0110]=A[5]+A[6]
C[7(0111)]=A[7]
C[8(1000)]=A[1]+A[2]+……+A[7]+A[8]
规律结论:
- C[i]=A[j]+……+A[i](树状数组是一段连续的累加且末尾为A[i])
- C[i]数组中累加A[]的个数为 2 k 2^k 2k 个,我们发现k的数值与C[i]在二进制下末尾的0的个数相等,而 2 k 2^k 2k等于末尾的第一个1的权位大小
- C[i]数组 由k个C[j]数组贡献得到
- C[i]=C[i- 2 k 1 2^{k1} 2k1]+C[(i- 2 k 1 2^{k1} 2k1)- 2 k 2 2^{k2} 2k2]+……+C[(i- 2 k 1 2^{k1} 2k1)- 2 k 2 2^{k2} 2k2)-…… 2 k n 2^{kn} 2kn](n为2、3条的k,ki表示从后往前数第i个0的位置ki)
- s u m i sum_i sumi(1到i的前缀和)也等于k 个C[]相加:C[i]+C[i- 2 k 1 2^{k1} 2k1]+C[(i- 2 k 1 2^{k1} 2k1)- 2 k 2 2^{k2} 2k2]+……+C[(i- 2 k 1 2^{k1} 2k1)- 2 k 2 2^{k2} 2k2)-…… 2 k n 2^{kn} 2kn](ki表示此时从后往前数的0个数)
既然我们发现k在树状数组中有着非同寻常的作用:那我们怎么快速推算出一个数的 2 k 2^k 2k是多少?
引入算法的关键:lowbit=i&(-i)
前置芝士:负数的补码是原码取反加一 ,而整数的补码与原码相同
i&(-i)变成原来第一个1的权位大小=
2
k
2^k
2k(不太清楚的话可以手捏几个二进制数)
算法实现
Part one;我们如何利用A[]建树状数组C[]
由图得:A[i]必然对C[i]有贡献,而C[i]对C[i+ 2 k 2^k 2k]有贡献,
void add(int x,int k)//x为当前下标,y为数值
{
while(x<=n)
{
tree[x]+=k;
x+=lowbit(x);
}
return;
}
Part two:如何前缀和或区间和查询?
查找前缀和的方法,我们可以从前面的结论得出
int sum(int x)
{
int ans=0;
while(x!=0)
{
ans+=tree[x];
x-=lowbit(x);
}
return ans;
}
而区间和怎么求?
a
n
s
i
,
j
ans_{i,j}
ansi,j=
s
u
m
j
sum_j
sumj-
s
u
m
i
−
1
sum_{i-1}
sumi−1 学过前缀和的同学基本都会的
算法进阶
树状数组主要的操作或用法:
单点修改+区间查询
这不是上面的板子题???
luogu板子
#include <iostream>
#include <stdio.h>
using namespace std;
int n,m,tree[2000010],ans,a,b,c;
int lowbit(int x){return x&(-x);}
void add(int x,int k)
{
while(x<=n)
{
tree[x]+=k;
x+=lowbit(x);
}
return;
}
int sum(int x)
{
ans=0;
while(x!=0)
{
ans+=tree[x];
x-=lowbit(x);
}
return ans;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
scanf("%d",&a);
add(i,a);
}
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&a,&b,&c);
if(a==1)
add(b,c);
if(a==2)
cout<<sum(c)-sum(b-1)<<endl;
}
}
区间修改+单点查询
这里要利用差分的知识
举个栗子:n=5:1 3 5 3 2
当[
a
3
a_3
a3,
a
4
a_4
a4]+3时怎么维护呢?1、一个一个点地维护,但是时间复杂度太大了 。 2、利用前缀和的性质在树状数组上操作,从询问入手:单点查询,而差分数组的前缀和等于的第i个点的大小
差分数组b[]=1,2,2,-2,-1
s
u
m
i
sum_i
sumi=
a
i
a_i
ai
如何修改呢?当我们发现
b
i
b_i
bi+x时,
a
j
a_{j}
aj+x(j>=i)
[
a
i
a_i
ai,
a
j
a_j
aj]+x等价于
b
i
+
x
b_i+x
bi+x、
b
j
+
1
b_{j+1}
bj+1-x
板子题
#include <iostream>
#include <cstdio>
using namespace std;
long long tree[500005];
int n, m;
long long lowbit(long long x)
{
return x & -x ;
}
void add(int x, long long num) {
while (x <= n) {
tree[x] += num;
x += lowbit(x);
}
}
long long query(int x)
{
long long ans = 0;
while (x) {
ans += tree[x];
x -= lowbit(x);
}
return ans;
}
int main()
{
scanf("%d%d", &n, &m);
long long last = 0, now;
for (int i = 1; i <= n; i++) {
scanf("%lld", &now);
add(i, now - last);
last = now;
}
int flg;
while (m--) {
scanf("%d", &flg);
if (flg == 1) {
int x, y;
long long k;
scanf("%d%d%lld", &x, &y, &k);
add(x, k);
add(y + 1, -k);
} else if (flg == 2) {
int x;
scanf("%d", &x);
printf("%lld\n", query(x));
}
}
return 0;
}
区间修改+区间查询
因为在洛谷找不到对应的树状数组的例题所以只好,拿线段树的板子题
对于区间查询,可以像前面例题那样,由两个前缀和相减得到
转换思路:求某个前缀:
s
u
m
i
sum_i
sumi=a[1]+……+a[i],如果我们代换成差分数组的话:b[1]+(b[1]+b[2])+……+(b[1]+……b[i])=b[1]* i + b[2]*(i-2) + …… + b[i]*1
差分数组的每一项贡献次数i-j+1(i为前缀和一共的项数,j为当前项的项数)
用式子表示就是
s
u
m
n
sum_{n}
sumn =
∑
i
=
1
n
\sum\limits_{i=1}^n
i=1∑n
∑
j
=
1
i
\sum\limits_{j=1}^i
j=1∑i
b
j
b_j
bj =
∑
i
=
1
n
\sum\limits_{i=1}^n
i=1∑n * (n-i+1) *
b
j
b_j
bj
有两种方法:
1、我们维护一个数组C=(i-1) *
b
j
b_j
bj
ans=
∑
i
=
1
n
\sum\limits_{i=1}^n
i=1∑n n *
b
i
b_i
bi -
∑
i
=
1
n
\sum\limits_{i=1}^n
i=1∑n
c
j
c_j
cj
2、我们维护一个数组C=i *
b
j
b_j
bj
ans=
∑
i
=
1
n
\sum\limits_{i=1}^n
i=1∑n (n+1) *
b
i
b_i
bi -
∑
i
=
1
n
\sum\limits_{i=1}^n
i=1∑n
c
j
c_j
cj
下面展示第二种的代码:
#include<iostream>
#include<cstdio>
#define N 100010
using namespace std;
int b[N],c[N],a[N],n,m,opk,x,y,k;
int lowbit(int x){ return x&(-x);}
void update(int x,int y){
for (int i=x;i<=n;i+=lowbit(i)){
b[i]+=y;
c[i]+=y*x;//不要把x写成i(这里x才是式子中的i)
}
}
int query(int x){
int ans=0;
for (int i=x;i>=1;i-=lowbit(i)){
ans+=(x+1)*b[i]-c[i];//不要把x写成i(这里x才是式子中的i)
}
return ans;
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++){
scanf("%d",&a[i]);
update(i,a[i]-a[i-1]);
}
// for (int i=1;i<=n;i++)printf("%d:%d %d\n",i,b[i],c[i]);
for (int i=1;i<=m;i++){
scanf("%d",&opk);
if (opk==1){
scanf("%d%d%d",&x,&y,&k);
update(x,k);
update(y+1,-k);
}else{
scanf("%d%d",&x,&y);
printf("%d\n",query(y)-query(x-1));
}
}
return 0;
}
区间最值
这里只讨论单点修改+区间查询最值(这里讲最大值)
建树:既然求区间最值,那树状数组的建立操作从求和变成取最值。
查询:等等,这时我们发现前缀和和差分在这里失去了原有的作用,那从树状数组控制的范围入手(前面的结论1、2条得知:C[i]表示:A[i- 2 k 2^k 2k+1],A[i- 2 k 2^k 2k+2],……,A[i]中最值)所以我们要判边界!!
而修改的话不能直接加lowbit来处理,因为直接取max的话,可能原来位置上的值为最大值,把当前值变小,无法判断,我们把C[i- 2 k 1 2^{k1} 2k1]+C[(i- 2 k 1 2^{k1} 2k1)- 2 k 2 2^{k2} 2k2]+……+C[(i- 2 k 1 2^{k1} 2k1)- 2 k 2 2^{k2} 2k2)-…… 2 k n 2^{kn} 2kn]取max
int query(int x,int y){
int ans=0;
while (x<=y){
ans=max(ans,a[y]);
for (--y;y-x>=lowbit(x);y-=lowbit(y)){
ans=max(ans,b[y]);
}
}
}
int update(int x,int y){
a[x]+=y;
while (x<=y){
b[x]=a[x];
for (int i=1;i<lowbit(x);i<<=1){
b[x]=max(b[x],b[x-i]);
}
x+=lowbit(x);
}
}
逆序对
题目:逆序对
定义当
a
i
a_i
ai>
a
j
a_j
aj 且 i<j 就称(
a
i
a_i
ai,
a
j
a_j
aj)为一组逆序对
普及芝士:离散化
利用类似下标计数的方式,出现
a
i
a_i
ai时,把jsq[
a
i
a_i
ai]+=1,用树状数组c来维护数组jsq
前缀和
j
s
q
i
jsq_i
jsqi表示小于等于i的数的个数,我们反向思路:大于
a
i
a_i
ai的数等于一共的数减去小于等于i的数
我们按输入顺序来维护就可以了
#include<iostream>
#include<cstdio>
#include<algorithm>
#define N 500010
#define int long long
using namespace std;
int n,a[N],ansh,b[N],c[N],num;
int lowbit(int x){return x&(-x);}
void update(int x,int y){
for (int i=x;i<=num;i+=lowbit(i)){
c[i]+=y;
}
}
int query(int x){
int ans=0;
for (int i=x;i>=1;i-=lowbit(i)){
ans+=c[i];
}
return ans;
}
signed main()
{
scanf("%lld",&n);
for (int i=1;i<=n;i++){
scanf("%lld",&a[i]);
b[i]=a[i];
}
sort(b+1,b+1+n);
num=unique(b+1,b+1+n)-b-1;
for (int i=1;i<=n;i++){
a[i]=lower_bound(b+1,b+1+num,a[i])-b;
ansh+=i-1-query(a[i]);
// printf("%lld %lld\n",a[i],ansh);
update(a[i],1);
}
printf("%lld",ansh);
return 0;
}
二维:
由于找不到单点修改+区间修改和区间修改+单点查询的板子
推荐大佬博客一同食用!!!
所以我直接代入一个区间修改+区间查询的板子题来口胡
我们把树状数组从一维扩展成二维,类似于一行数,变成一个矩阵数(变成二维差分)
前置芝士:二维前缀和
sum[ i ] [ j ]= sum[ i-1 ][ j ]+sum[ i ][ j-1 ]-sum[ i-1 ][ j-1 ]+a[ i ][ j ]
结合图来了解:sum[i][j]=(黄+蓝)+(橙+蓝)- 蓝 + 绿。
- 由差分数组的前缀和等于这个数的值的性质可以轻易单点查询
我们如何建二维树状数组呢
结合板子题上帝造题的七分钟来讲
题单:
- [SDOI2009]HH的项链 (思路转化,维护一端单调来用前缀和)
- [NOIP2013 提高组] 火柴排队(离散化+排序不等式)
- P6225 [eJOI2019]异或橙子
- CF1311F Moving Points(离散化+二维偏序(类似逆序对))
参考:
[洛谷日报第22期]可以代替线段树的树状数组?
树状数组维护区间最大值
树状数组从入门到弃疗(示例代码)
“高级”数据结构——树状数组!