树状数组笔记

算法思想

树状数组,又教二进制索引树,通过二进制分解划分区间,使用一个数组来存取对应区间的值,当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],Lj=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()的理解和使用,树状数组相较于线段树对于最值的求解和更新更加简便,但有些问题还是线段树更为快捷

参考文献

  1. P5459 [BJOI2016]回转寿司 题解
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值