【 HDU 1166】 敌兵布阵 树状数组从0到1

如果给你一个数组,让你求某个区间的和,你很自然会想到遍历一遍数组,复杂度是O(n),但是如果有多次询问呢,你也许会想到用前缀数组,通过O(n)的预处理,达到O(1)的查询,但是如果要更新某个元素的值呢,如果用前缀和的思想,每更新一个元素就会更新前缀数组,每次复杂度是O(n),如果有n次更改,复杂度为O(n^2)。有没有更快的呢,这时候树状数组就排上用场了,树状数组可以用来解决动态数组连续和的问题。

树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree):是一个查询和修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值;经过简单修改可以在log(n)的复杂度下进行范围修改,但是这时只能查询其中一个元素的值(如果加入多个辅助数组则可以实现区间修改与区间查询)。

树状数组大致就长下面这个模样。

那么树状数组到底是怎么把修改和求和都做到O(log(n))呢 

当我么求和的时候,c[1]=A[1],c[2]=c[1]+A[2],c[4]=c[2]+c[3]+A[4],当我们更新A[2]的时候,我们只需要更新c[2],c[4],c[8],所以相当与每层我们只操作一个结点,即O(log(n))。

当我们更新的时候c[i]的时候,就从c[i]开始往右走,边走边往上爬。比如我们更新c[2]的时候,我们先更新c[2],然后更新c[4]、c[8]、c[16]。

当我们要求和的时候,我们从c[i]开始往左走,边走边往上爬。比如我们求前7项和,先找到c[7],然后加上c[6],最后加上c[4]。

那我们怎么才能做到这种奇怪的走法呢,

图中灰色结点表示c[i],白色结点表示以i结尾的和,很神奇,灰色结点c[4]=A[1]+A[2]+A[3]+A[4]表示以4结尾的和,我们很好理解,但是为什么灰色结点c[6]表示的表示的只有A[5]+A[6] 我们怎么来控制个数呢,对于c[4],我们相当于从下标4开始,往前数了4个数,对于c[6],我们从下标6开始往前数两个数,有个公式c[i]=A[i-2^k+1]+A[i-2^k+2]......+A[i],其中k表示i的二进制末尾0的个数

      i  2进制     k下标取值范围 
     1   0001    0  i-2^k+1...i=1...1c[1]=A[1]
     2  0010    1  i-2^k+1...i=1...2c[2]=A[1]+A[2]=c[1]+A[2]
     3  0011    0  i-2^k+1...i=3...3c[3]=A[3]
     4  0100    2  i-2^k+1...i=1...4c[4]=A[1]+A[2]+A[3]+A[4]=c[2]+c[3]+A[4]
     5  0101    0  i-2^k+1...i=5...5c[5]=A[5]
     6  0110    1  i-2^k+1...i=5...6c[6]=A[5]+A[6]=c[5]+A[6]

所以我们求和的时候我们就先找i,然后往前数2^k个数,既然k表示i的二进制末尾0的个数,其实就是前一位对应的权值,比如4的二进制为0100,末尾有2个0,所以2^k=4,从后往前数第一位不为0的出现在第三位,权值为100,即4。

那我们我们怎么计算这个权值呢,lowbit(i)=2^k=x&-x,先给出代码

int lowbit(int x)
{
    return x&(-x);
}

还是以4为栗子:lowbit(4)=4,4二进制为0100,-4的二进制为4的二进制按位取反然后加1,-4=1100,所以4&(-4)=0100 即为4;

所以c[i]等于以i开始从后往前数i&(-i)个数。

当我们更新c[i]的时候,就从c[i]开始往右走,边走边往上爬。所以i逐渐变大但小于等于n,即更新时的下标变化:i+=lowbit(i)

当我们要求和的时候,我们从c[i]开始往左走,边走边往上爬。所以i逐渐变小但大于0,即求和时下标变化:i-=lowbit(i)

预处理方法:先把A和c数组都清空,然后执行n次add操作,所以时间复杂度为O(nlog(n))。

树状数组主要是用来维护动态数组连续和问题。

HDU 1166 敌兵布阵

题意:有n个营地,每个营地开始都有一定数量的敌军,然后有下面三种命令

(1) Add i j,i和j为正整数,表示第i个营地增加j个人(j不超过30)
(2)Sub i j ,i和j为正整数,表示第i个营地减少j个人(j不超过30);
(3)Query i j ,i和j为正整数,i<=j,表示询问第i到第j个营地的总人数

题解:由于n=50000比较大,而且命令多达40000,所以可以用树状数组来解决,当然也可以用线段树来维护区间和,只不过线段树代码量比较大,所以对于这种单点更新求区间和问题最好使用树状数组,写起来节约时间,也不容易出错。下面分别给出树状数组和线段树的代码。

#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=50000+7;
int tree[maxn<<1];       //树状数组
int n;
char s[10];
int lowbit(int x)
{
    return x&(-x);
}
void add(int x,int d)
{
    while(x<=n){
        tree[x]+=d;
        x+=lowbit(x);
    }
    return ;
}
int sum(int x)
{
    int s=0;
    while(x>0){
        s+=tree[x];
        x-=lowbit(x);
    }
    return s;
}
int main()
{
    int T;
    scanf("%d",&T);
    for(int t=1;t<=T;t++){
        memset(tree,0,sizeof(tree));
        scanf("%d",&n);
        for(int i=1;i<=n;i++){
            int num;
            scanf("%d",&num);
            add(i,num);              //开始默认所有的点都是0 每读到一个数就开始往上更新
        }
        printf("Case %d:\n",t);
        int x,y;
        while(scanf("%s",s)==1&&strcmp(s,"End")){
            if(!strcmp(s,"Query")){
                scanf("%d %d",&x,&y);
                printf("%d\n",sum(y)-sum(x-1));
            }
            else if(!strcmp(s,"Add")) {
                scanf("%d %d",&x,&y);
                add(x,y);
            }
            else {
                scanf("%d %d",&x,&y);
                add(x,-y);
            }
        }
    }
    return 0;
}
#include<cstdio>
#include<cstring>
using namespace std;

const int maxn =1e5+7;

long long int segtree[maxn<<2],a[maxn];
int n,x,y;
char s[10];

void pushup(int now)                   //维护的信息 维护区间和
{
    segtree[now]=segtree[now<<1]+segtree[(now<<1)|1];
    return ;
}
void build_tree(int l,int r,int now)
{
    if(l==r){                   //递归到叶子结点赋值
            segtree[now]=a[l];
            return ;          //记得递归一定要renturn 不然就会疯狂递归
    }
    int mid=(l+r)>>1;
    build_tree(l,mid,now<<1);
    build_tree(mid+1,r,(now<<1)|1);
    pushup(now);
}

long long int query(int ql,int qr,int l,int r,int now)
{
    if(ql<=l&&r<=qr){     //如果当前区间完全包含在查询区间 就return 当前递归子函数不再向下递归
        return segtree[now];
    }
    long long int ans=0;
    int mid=(l+r)>>1;
    if(ql<=mid)ans+=query(ql,qr,l,mid,now<<1);
    if(qr>mid)ans+=query(ql,qr,mid+1,r,(now<<1)|1);
    return ans;
}
void update(int pos ,int c,int l,int r,int now)
{
    if(l==r){
        segtree[now]+=c;
        return ;
    }
    int mid=(l+r)>>1;
    if(pos<=mid)update(pos,c,l,mid,now<<1);
    else update(pos,c,mid+1,r,(now<<1)|1);
    pushup(now);
}
int main()
{
    int T;
    scanf("%d",&T);
    for(int t=1;t<=T;t++){
        scanf("%d",&n);
        memset(segtree,0,sizeof(segtree));
        for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
        build_tree(1,n,1);
        printf("Case %d:\n",t);
        while(scanf("%s",s)==1&&strcmp(s,"End")){
            if(!strcmp(s,"Query")){
                scanf("%d %d",&x,&y);
                printf("%lld\n",query(x,y,1,n,1));
            }
            else if(!strcmp(s,"Add")){
                scanf("%d %d",&x,&y);
                update(x,y,1,n,1);
            }
            else {
                scanf("%d %d",&x,&y);
                update(x,-y,1,n,1);
            }
        }
    }
    return 0;
}

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值