算法思想
树状数组,又教二进制索引树,通过二进制分解划分区间,使用一个数组来存取对应区间的值,当i的二进制末尾有k个连续的0,则数组c[i]存储值区间长度为2k(不证明),则获得这个区间长度的代码如下:
int lowbit(int i)//注意i不能为0
{
return (-i)&i;
}
对于c[i],其直接前驱为c[i-lowbit(i)],直接后继为c[i+lowbit(i)],如图
获得前缀和,即将c[i]左侧所有子树的根相加,直接前驱和直接前驱的直接前驱等
int sum(int i)
{
int s=0;
for(;i>0;i-=lowbit(i))
s+=c[i];
return s;
}
对点更新,只需更新本身与直接后继,直接后继的后继等
void add(int i,int z)
{
for(;i<=n;i+=lowbit(i))
c[i]+=z;
}
查询区间的和,即求前缀和之差
int Sum(int i,int j)
{
return sum(j)-sum(i);
}
当遇到多维问题时可采用多维树状数组,m维树状数组就需要多出m-1个循环,以二维为例子
前缀和代码如下
int sum(int x,int y)
{
int s=0;
for(int i=x;i>0;i-=lowbit(i))
for(int j=y;j>0;j-=lowbit(j))
s+=c[i][j];
return s;
}
更新代码如下
void add(int x,int y,int z)
{
for(int i=x;i<=n;i+=lowbit(i))
for(int j=y;j<=n;j+=lowbit(j))
c[i][j]+=z;
}
查询代码如下
int Sum(int x1,int y1,int x2,int y2)
{
return sum(x2,y2)-sum(x1-1,y2)-sum(x2,y1-1)+sum(x1-1,y1-1);
}
解释如图
训练
POJ2352
题目大意:平面直角坐标系上有许多点,每个点有一个等级,等级为横纵坐标均不超过自己的点数量(不包括自己),计算给定地图上每个级别星星数量,输入按照y坐标升序输入,y相等按照x升序输入
思路:由于输入数据已经经过排序处理,所以每次只需要计算x小于当前输入点坐标x的个数即可(y是升序的),问题本质为统计区间内的小于x的数量,而计算出的数量也就是对应的等级
代码
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <cmath>
using namespace std;
int N,x,y,c[32001],ans[32001],m;
int lowbit(int t) {
return (-t)&t;
}
int sum(int t) {
int s=0;
while(t>0) {
s+=c[t];
t-=lowbit(t);
}
return s;
}
void add(int t) {
while(t<=32001) {//边界为x的最大值,不为N,因为x可以取大于N的值
//必须是32001以上的值,不然32000取不到
c[t]++;
t+=lowbit(t);
}
}
int main() {
scanf("%d",&N);
for(int i=1; i<=N; i++) {
scanf("%d%d",&x,&y);
x++;
ans[sum(x)]++;
add(x);
}
for(int i=0; i<N; i++)
printf("%d\n",ans[i]);
return 0;
}
POJ3067
题目大意:N个点在左,M个点在右,都从上到下编号1~n,现在有K条边,每条边为直线,连接左右两点,询问有多少边交叉
思路:求逆序对问题,当N个点与M个点顺序相连,如1-1,2-2…或1-2,2-3…时是没有交叉的,只有出现了逆序对,如1-3,2-1…才会出现交叉,将问题转换为以左边为基准,判断右边有多少逆序对,只需要求出总逆序对数即可
代码
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
using namespace std;
typedef struct node {
int x,y;
bool operator<(node a)const {
if(x==a.x)
return y<a.y;
return x<a.x;
}
} node;
int T,N,M,K,c[1212];
node edge[12121212];
int lowbit(int x) {
return (-x)&x;
}
int sum(int t) {//判断有多少个小于t的值已经出现
int s=0;
while(t>0) {
s+=c[t];
t-=lowbit(t);
}
return s;
}
void add(int t) {
while(t<=1001) {
c[t]++;
t+=lowbit(t);
}
}
int main() {
scanf("%d",&T);
for(int j=1;j<=T;j++) {
scanf("%d%d%d",&N,&M,&K);
long long ans=0;
memset(c,0,sizeof(c));
for(int i=0; i<K; i++)
scanf("%d%d",&edge[i].x,&edge[i].y);
sort(edge,edge+K);//按照左边排序
for(int i=0; i<K; i++) {
ans+=i-sum(edge[i].y);//总值减小于y的值得到的即是放错位置的大值
add(edge[i].y);
}
printf("Test case %d: %lld\n",j,ans);
}
return 0;
}
POJ3321
题目大意:一棵树,树上n个叉(编号1~n),每个叉只能长一个或不长,操作者可能摘一个,有两种操作:C x 改变叉x上的苹果状态,01互换,Q x查询x叉上方子树中苹果数量,输出每个查询的答案
思路:题目所给的是一树形结构,但是树状数组操作的实质上序列,所以要将这个树形结构转换成序列,很容易能想到用DFS序来讲树形结构序列化,将一棵树DFS,记录遍历当前节点进来和出去时的序号,两个序号间的节点为当前节点的子树节点,通过DFS序将子树转换成序列,求解区间和
代码
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <cmath>
using namespace std;
struct node {
int to,next;
} edge[212121];
int head[212121],N,u,v,M,cnt,c[212121],L[212121],R[212121],dfn=1;
bool apple[212121];
void AddEdge(int from,int to) {//链式前向星
edge[++cnt].to=to;
edge[cnt].next=head[from];
head[from]=cnt;
}
void DFS(int u,int f) {//构造DFS序列
L[u]=dfn++;
for(int i=head[u]; i; i=edge[i].next) {
int v=edge[i].to;
if(v==f)
continue;
DFS(v,u);
}
R[u]=dfn-1;//防止回溯的时候对同一个点序号不一样(比如无子树,同一个点的L和R应该相同)
}
void add(int t,int v) {
while(t<=N) {
c[t]+=v;
t+=(-t)&t;
}
}
int sum(int t) {
int s=0;
while(t>0) {
s+=c[t];
t-=(-t)&t;
}
return s;
}
int main() {
scanf("%d",&N);
for(int i=1; i<N; i++) {
scanf("%d%d",&u,&v);
AddEdge(u,v);
}
DFS(1,1);
for(int i=1; i<=N; i++) {
apple[i]=1;//一开始全是苹果
add(i,1);
}
scanf("%d",&M);
while(M--) {
char ch;
cin >>ch;
scanf("%d",&u);
if(ch=='Q')
printf("%d\n",sum(R[u])-sum(L[u]-1));//区间长度为R-L+1
else {
if(apple[u])
add(L[u],-1);//1~L[u]
else
add(L[u],1);
apple[u]=!apple[u];
}
}
return 0;
}
POJ1195
题目大意:给出一个矩阵,每个块有自己的数值,有三种操作,清零,更新,查询,输出每次查询的结果
思路:二维树状数组,下标为了避免0的出现要+1
代码
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <cmath>
using namespace std;
int S,A,x,t[2121][2121],a,b,c,x1,x2,y1,y2;
int sum(int x,int y) {//二维前缀和
int s=0;
for(int i=x; i>0; i-=(-i)&i)
for(int j=y; j>0; j-=(-j)&j)
s+=t[i][j];
return s;
}
void add(int x,int y,int z) {//更新
for(int i=x; i<=S; i+=(-i)&i)
for(int j=y; j<=S; j+=(-j)&j)
t[i][j]+=z;
}
int main() {
scanf("%d%d",&x,&S);
while(1) {
scanf("%d",&x);
switch(x) {
case 1:
scanf("%d%d%d",&a,&b,&c);
add(a+1,b+1,c);//下标增加
break;
case 2:
scanf("%d%d%d%d",&x1,&x2,&y1,&y2);
printf("%d\n",sum(y1+1,y2+1)+sum(x1,x2)-sum(x1,y2+1)-sum(y1+1,x2));//下标增加,类似求面积
break;
case 3:
return 0;
break;
}
}
return 0;
}
LuoguP5459
题目大意:略
思路:和下题类似,对于每个下标 i i i,求解的目的是存在多少个 x ∈ [ 1 , i ] , L ≤ ∑ j = x i a [ i ] ≤ R x\in [1,i],L\le \sum_{j=x}^ia[i]\le R x∈[1,i],L≤∑j=xia[i]≤R,先统计前缀和,对于一个位置 i i i,有 p r e [ i ] pre[i] pre[i],那么,对于已知的 p r e [ i ] pre[i] pre[i],只需要找到前缀和在 [ p r e [ i ] − R , p r e [ i ] − L ] [pre[i]-R,pre[i]-L] [pre[i]−R,pre[i]−L]且下标在i之前的前缀和位置即可
代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=3e5+5;
int l,r,n,s[maxn],sum[maxn],tr[maxn<<2],len,ans,cnt;
int getsum(int x) {
int res=0;
for(int i=x; i; i-=i&(-i))
res+=tr[i];
return res;
}
void update(int x) {
for(int i=x; i<=len; i+=i&(-i))
tr[i]++;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >>n>>l>>r;
s[++cnt]=0;//录入一个0,方便离散化,存在负数
for(int i=1; i<=n; i++) {
int x;
cin >>x;
sum[i]=sum[i-1]+x;
s[++cnt]=sum[i];
s[++cnt]=sum[i]-l;
s[++cnt]=sum[i]-r-1;//为了方便,直接全部处理
}
sort(s+1,s+cnt+1);
len=unique(s+1,s+1+cnt)-s-1;
for(int i=0; i<=n; i++) {
int ll=lower_bound(s+1,s+1+len,sum[i]-r-1)-s;
int rr=lower_bound(s+1,s+1+len,sum[i]-l)-s;
ans+=getsum(rr)-getsum(ll);//统计满足条件的数量
int pos=lower_bound(s+1,s+1+len,sum[i])-s;//获得更新前缀和对应位置
update(pos);
}
cout <<ans;
return 0;
}
Codeforces Round #780 (Div.3) F2
题目大意:略
思路:对于一对下标 i , j i,j i,j,如果对应位置前缀和后者大于前者,并且差值为3的倍数,那么这个区间就可行,等价于在模3情况下,i,j对应前缀和相等,那么可以使用树状数组统计前缀和,对于一个位置i,前面是否存在已经出现的同余项前缀和,如果存在,代表可以凑出一个合法区间,由于树状数组下标不能为0,所以需要将坐标整体进行偏移,具体见代码
代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e6+10;
int n,sum[maxn],t;
int tree[maxn][3];
char s[maxn];
void update(int x,int c) {
for(int i=x; i<=maxn; i+=i&(-i))
tree[i][c]++;
}
int query(int x,int c) {
int res=0;
for(int i=x; i>=1; i-=i&(-i))
res+=tree[i][c];
return res;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >>t;
while(t--) {
cin >>n;
cin >>s+1;
int ans=0,mn=0;
sum[0]=0;
for(int i=1; i<=n; i++) {
if(s[i]=='+')sum[i]=sum[i-1]-1;//遇到+,-1
else sum[i]=sum[i-1]+1;
mn=min(mn,sum[i]);//获得最小值,便于偏移
}
for(int i=0; i<=n-mn+10; i++)//清空
tree[i][0]=tree[i][1]=tree[i][2]=0;
for(int i=0; i<=n; i++)sum[i]-=mn-1;
for(int i=0; i<=n; i++) {
int c=sum[i]%3;//找同余项
ans+=query(sum[i],c);//统计在其前面出现的同余项的个数
update(sum[i],c);//更新
}
cout <<ans<<endl;
}
return 0;
}
总结
树状数组的关键是对lowbit()的理解和使用,树状数组相较于线段树对于最值的求解和更新更加简便,但有些问题还是线段树更为快捷