数据结构之树状数组
目录
洛谷P3608 [USACO17JAN]Balanced Photo G
1:引子~~~
假如现在需要你维护一种数据结构,需要能支持区间求和和单点
更新,那么该怎么办?
方法一:暴力枚举
不用想,TLE妥妥的
方法二:线段树
额,本篇不予讨论
那该怎么办呢?
当当当当,树状数组闪亮登场。
2:树状数组的基本概念
你可能会好奇,树状数组到底是树还是数组?
树状数组,顾名思义是用数组模拟的树形结构,其本质是一个一维数组。
为什么不用树维护呢?
用不着嘛,你肯定不会用动规,二分,模拟退火写A+B吧(貌似还真有用树状数组写的)
咳咳,不多说了额
树状数组可以解决大部分基于区间上的更新以及求和问题
树状数组可以解决的问题都可以用线段树解决,这两者的区别在哪里呢?
树状数组区间修改和查询的复杂度都是O(logN),相比线段树系数要少很多,比传统数组要快,而且容易写。
但是遇到复杂的区间问题还是不能解决,功能还是有限。
发个图来更好的理解一下树状数组
如上图,其中A是普通数组,C是树状数组(此图只是方便理解,真正存的样子也是一条线)
这个图是什么意思呢?
我们先看编号是奇数的
很显然:
C[1]=A[1]
C[3]=A[3]
C[5]=A[5]
C[7]=A[7]
再看看编号是偶数的
C[2]=C[1]+A[2]=A[1]+A[2]
C[4]=C[2]+C[3]+A[4]=A[1]+A[2]+A[3]+A[4]
C[6]=C[5]+A[6]=A[5]+A[6]
C[8]=C[4]+C[6]+C[7]+A[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8]
你可能会好奇为什么是这样的关系,不着急,慢慢来。
其实,树状数组的核心也是二进制
不信?来,继续上图
细心的同学不难发现红色数字就是编号的数字对应的二进制数
那跟树状数组有什么关系?这个父子关系是怎么来的?
来,我们把每个数的二进制数加上他二进制表示法中最后一个1(例如1100要加上100,101要加上1,10要加上10)
1:1+1=10=2
2:10+10=100=4
3:11+1=100=4
4:100+100=1000=8
5:101+1=110=6
6:110+10=1000=8
7:111+1=1000=8
怎么样~~~是不是很神奇,这个关系就是这样推出来的
3:树状数组的实现及基本操作
这种取出最后一个1的操作叫做lowbit,lowbit(X)就是取出(十进制数)X的最后一个1;
那么问提又来了,这个操作怎么实现?
其实也很简单
lowbit(X)=X&-X
为什么?
假设x的二进数是10001100
那么-x就是把x取反再加1=01110100
再相与=000001000=100
看100,就是10001100的最后一个1
代码实现:
int void(int x)
{
return x&-x;
}
好了那么树状数组的基本操作也可以开始啦!
题目描述
如题,已知一个数列,你需要进行下面两种操作:
-
将某一个数加上 x
-
求出某区间每一个数的和
输入格式
第一行包含两个正整数 n,m分别表示该数列数字的个数和操作的总个数。
第二行包含 n个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。
接下来 m 行每行包含 3 个整数,表示一个操作,具体如下:
-
1 x k
含义:将第 x个数加上k -
2 x y
含义:输出区间 [x,y]内每个数的和
输出格式
输出包含若干行整数,即为所有操作 2 的结果。
输入输出样例
输入
5 5
1 5 4 2 3
1 1 3
2 2 5
1 3 -1
1 4 2
2 1 4
输出
14
16
对于 100% 的数据,1≤n,m≤5×105。
我们用树状数组维护区间和,单点修改很简单,但是注意不能只修改一个点,要把他的父节点也修改
代码如下
void update(int x,int z)
{
for(int i=x;i<=n;i+=lowbit(i))
c[i]+=z;
}
区间求和其实也很简单利用前缀和思想,我们必须求前Y项和和前X-1项的和
例如求前六项和:
其实就等于C[6]+C[4]
这两个数有什么关系?(往二进制想)
其实就是减掉末尾的1,和更新正好相反
每次都减掉末尾的1,直到都减没了(也就是等于0),同时把数组的值加上去
看一下代码:
int query(int x)
{
int ans=0;
for(int i=x;i;i-=lowbit(i))
int+=c[i];
return int;
}
完整代码如下:
#include<bits/stdc++.h>
using namespace std;
long long n,q,c[1000500],a,w,s;
int lowbit(int x)
{
return x & -x;
}
void update(int x,int z)
{
for(int i=x;i<=n;i+=lowbit(i))
c[i]+=1ll*z;
}
long long query(int x)
{
long long ans=0;
for(int i=x;i;i-=lowbit(i))
ans+=c[i];
return ans;
}
int main()
{
cin>>n>>q;
for(int i=1;i<=n;i++)
{
scanf("%lld",&a);
update(i,a);
}
while(q--)
{
scanf("%lld%lld%lld",&a,&w,&s);
if(a==1)
{
update(w,s);
}
else
printf("%lld\n",query(s)-query(w-1));
}
return 0;
}
4:树状数组的应用
1.数星星
题目描述
天文学家经常要检查星星的地图,每个星星用平面上的一个点来表示,每个星星都有坐标。我们定义一个星星的“级别”为给定的星星中不高于它并且不在它右边的星星的数目。天文学家想知道每个星星的“级别”。
5
*
4
*
1 2 3
* * *
例如上图,5号星的“级别”是3(1,2,4这三个星星),2号星和4号星的“级别”为1。
给你一个地图,你的任务是算出每个星星的“级别”。
输入格式
输入的第一行是星星的数目N(1<=N<=60000),接下来的N行描述星星的坐标(每一行是用一个空格隔开的两个整数X,Y,0<=X,Y<=32000)。
星星的位置互不相同。星星的描述按照Y值递增的顺序列出,Y值相同的星星按照X值递增的顺序列出。
输出格式
输出包含N行,一行一个数。第i行是第i个星星的“级别”
input
5
1 1
5 1
7 1
3 3
5 5
output
0
1
2
1
3
由于题目对输入x,y的限制,所以我们不需要管用,直接用树状数组维护比x小的个数就好了,但要注意x可能=0,出现卡死的情况,所以维护树状数组的时候需要加1.
话不多说,上代码;
#include<bits/stdc++.h>
using namespace std;
int n,x,y,c[35000];
int lowbit(int x)
{
return x & -x;
}
void update(int x)
{
for(int i=x;i<=32000;i+=lowbit(i))
c[i]++;
}
int query(int x)
{
int ans=0;
for(int i=x;i;i-=lowbit(i))
ans+=c[i];
return ans;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d%d",&x,&y);
printf("%d\n",query(x+1));
update(x+1);
}
return 0;
}
2.求逆序对
题目描述
猫猫 TOM 和小老鼠 JERRY 最近又较量上了,但是毕竟都是成年人,他们已经不喜欢再玩那种你追我赶的游戏,现在他们喜欢玩统计。
最近,TOM 老猫查阅到一个人类称之为“逆序对”的东西,这东西是这样定义的:对于给定的一段正整数序列,逆序对就是序列中 a_i>a_j 且 i<j的有序对。知道这概念后,他们就比赛谁先算出给定的一段正整数序列中逆序对的数目。注意序列中可能有重复数字。
Update:数据已加强。
输入格式
第一行,一个数 n,表示序列中有 n个数。
第二行 n 个数,表示给定的序列。序列中每个数字不超过 10^9。
输出格式
输出序列中逆序对的数目。
输入输出样例
输入 #1复制
6
5 4 2 6 3 1
输出 #1复制
11
说明/提示
对于 25% 的数据,n≤2500
对于 50% 的数据,n≤4×104。
对于所有数据,n≤5×105
请使用较快的输入输出
首先暴力不说了
第一种正解是归并排序,但在此我们不做讨论
第二种方法是用树状数组维护在1~i-1中小于等于a[i]的个数
再用总个数(i-1)减去这个这个个数,再加起来,就是逆序对数量
注意,数字的大小是1e9,数组开不了这么大,需要离散化
代码如下
#include<bits/stdc++.h>
using namespace std;
const int maxn=5e5+10;
int n,a[maxn],p[maxn];//p是离散化数组
int c[maxn];
long long ans=0;
bool cmp(int x,int y)
{
return a[x]<a[y];
}
int lowbit(int x)
{
return x&-x;
}
void update(int x,int v)
{
for(long long i=x;i<=n;i+=lowbit(i))
c[i]+=v;
}
long long query(long long x)
{
long long ans=0;
for(long long i=x;i;i-=lowbit(i))
ans+=c[i];
return ans;
}
int main()
{
scanf("%d",&n);
for(int i=1; i<=n; i++)
{
scanf("%d",&a[i]);
p[i]=i;
}
stable_sort(p+1,p+n+1,cmp);//离散化处理
for(int i=1; i<=n; i++)
a[p[i]]=i;
for(int i=1; i<=n; i++)
{
ans=ans+i-1-query(a[i]);
update(a[i],1);
}
printf("%lld\n",ans);
return 0;
}
5:树状数组的拓展
<1> :区间修改,单点查询
我们都知道差分是D[i]=a[i]-a[i-1]
然后修改只需要让D[L]+X,D[R]-X(为什么?我不想解释)
查询只要求个前缀和就行了
那么这种思想也可以在树状数组应用
我们用树状数组维护差分数组
代码如下
#include<bits/stdc++.h>
using namespace std;
int n,m,a[50005],c[50005]; //对应原数组和树状数组
int lowbit(int x)
{
return x&-x;
}
void updata(int k,int x) //区间修改
{
for(int i=k;i<=n;i+=lowbit(i))
c[i]+=x;
}
int query(int k) //单点求和
{
int ans = 0;
for(int i=k;i;i-=lowbit(i))
ans+=c[i];
return ans;
}
int main()
{
int f,x,y,k;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
updata(i,a[i]-a[i-1]); //构造树状数组
}
for(int i=1;i<=m;i++)
{
cin>>f;
if(f==1)//区间修改
{
cin>>x>>y>>k;
updata(x,k); //类似于差分的修改
updata(y+1,-k);
}
else
{
cin>>k;
cout<<query(k)<<endl;
}
}
return 0;
}
<2>区间修改,区间查询
和上面类似,这里不说了,可以自己尝试写写
实在不会可以去搜一搜