树状数组

24 篇文章 0 订阅

我们知道要是一个数组的和C[i]=A[1]+A[2]+.......+A[i],那么我更改其中任意一项值A[i],那么C[i],C[i+1],C[i+2]....C[n]都会随之变化,所以我们要调整C[]当n非常大时就需要很长的时间,必然会导致超时。这样就可以引入树状数组,它的修改和求和的复杂度为nlogn.

树状数组是一种数组结构,能够高效地获取数组中连续n个数的和。

下面这张图是基本上大多数博客都有的一张图,主要为了让我们更容易理解树状数组。

红色的C[i]表示树状数组,且C[i] = A[i–2^k+ 1] +A[i-2^k+2] … + A[i] (i>=1)

其中,k为i在二进制下末尾0的个数,(可以利用位运算2^k=i&(i^(i-1))=i&(-i)),则我们称C为树状数组。由树状数组的定义可知:

C[1]=A[1];   C[2]=A[1]+A[2]=C[1]+A[2];  C[3]=A[3]; C[4]=A[1]+A[2]+A[3]+A[4]=C[2]+C[3]+A[4];

C[5]=A[5]; C[6]=A[5]+A[6]=C[5]+C[6]; C[7]=A[7]; C[8]=...=C[4]+C[6]+C[7]+A[8];

由图可知,k为这颗树的高度,且最高不会超过logn,这个时候我们改变A[i]的值后可以由C[i]往根节点上溯,调整这条路上的C[]即可。比方说改变A[1],即pos=1的值,需要更新的值为C[1],C[2],C[4],C[8],位置变化的规律为:pos+=LowBit(pos),复杂度为logn。模板为:

#define LowBit(num) num&(-num)
void Add(int pos,int value)
{   for(int i=pos;i<MAX;i+=LowBit(i))
        C[i]+=value;
}

要求统计某一个位置num的前num项和时:只要找到num前的所有的最大子树,然后把其根节点的C[]加起来即可。如:要求前5项的和,用Sum(5)表示,则Sum(5)=C[5]+C[4],需要加起来的C[i]中的i也有规律可循,i-=LowBit(i),模板为:

int Add(int num)
{   int sum=0;
    for(int i=num;i>0;i-=LowBit(i))
        sum+=C[i];
    return sum;    
}

树状数组能快速求任意区间的和:A[i] + A[i+1] + … + A[j],设sum(k) = A[1]+A[2]+…+A[k],则A[i] + A[i+1] + … + A[j] = sum(j)-sum(i-1)。

用法1:树状数组---插点问线,当只有某一点的数值变化,而查找的区间为一段时用这个方法。

直接上题目吧,例题1:HDU 1166(敌兵布阵)直接用暴力的话肯定会超时。

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int MAX=1000010;
#define CLR(arr,val) memset(arr,val,sizeof(arr))
int n,total,value,C[MAX];
char Find[10];
int LowBit(int num)//二进制后面为0的个数
{   return num&(-num);
}
void Add(int pos,int val)//将pos位置的值+val
{   while(pos<=n)
    {   C[pos]+=val;//则每个位置的C[]都要改变,看下图有哪些C[]改变了,比方我把A[4]+val,变化的C[]有C[4],C[8]..变化,C[5],C[6]..并未变化
        pos+=LowBit(pos);//求出下一个要改变的位置,回溯到根节点
    }
}
int Sum(int num)//求前num项的和
{   int sum=0;
    while(num>0)
    {   sum+=C[num];
        num-=LowBit(num);//找到num前的所有最大子树
    }
    return sum;
}
int main()
{   int sum=1;
    scanf("%d",&total);
    while(total--)
    {   CLR(C,0);//要记得初始化 
        printf("Case %d:\n",sum++); 
        scanf("%d",&n);
        for(int i=1;i<=n;i++)//要从1开始!!! 
        {   scanf("%d",&value);
            Add(i,value);
        }
          
        while(scanf("%s",Find))
        {   int End,Start;
            if(Find[0]=='E') break;
            scanf("%d%d",&Start,&End); 
            if(Find[0]=='A') Add(Start,End);
            else if(Find[0]=='S') Add(Start,-End);
            else printf("%d\n",Sum(End)-Sum(Start-1)); 
        }
    }
    return 0;
}

和这个题目一样的是:NYOJ 116(士兵杀敌)~~~~~~
用法2:插线问点,当有某一段的数值都有变化时,而查询只有一个位置时用插线问点。NYOJ 123(士兵杀敌4)

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int MAX=1000010;
#define CLR(arr,val) memset(arr,val,sizeof(arr))
int n,m,num,C[MAX];
char Find[10];
int LowBit(int num)
{   return num&(-num);
}
void Add(int pos,int val)//前n项每项增加val 
{   while(pos>0)
    {   C[pos]+=val;
        pos-=LowBit(pos);    
    }
}
int Sum(int pos)
{   int sum=0;
    while(pos<=n)
    {   sum+=C[pos];
        pos+=LowBit(pos);
    }
    return sum;
}
int main()
{   CLR(C,0);
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {   scanf("%s",Find);
        if(Find[0]=='A')
        {   int Start,End;
            scanf("%d%d%d",&Start,&End,&num);
            /****下面两行Nice****/
            Add(Start-1,-num);
            Add(End,num);
            /********************/
        }
        else 
        {   scanf("%d",&num);
            printf("%d\n",Sum(num));
        }
    }
    return 0;
}

用法3:多维树状数组,增加模板:

#define LowBit(num) num&(-num)
void Add(int x,int y,....,int val)
{   for(int i=x;i<MAX;i+=LowBit(i))
        for(int j=y;j<MAX;j+=LowBit(j))
            .......//是几维的就几层
            C[i][j][...]+=val;
}
void Sum(int x,int y,.....)
{   int sum=0;
    for(int i=x;i>0;i-=LowBit(i))
        for(int j=y;j>0;j-=LowBit(j))
            ......
            sum+=C[i][j][...];
    return sum;        
}

例题3:HDU 1892(See you~~),代码:

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int MAX=1010;
#define min(a,b) a<b?a:b 
#define LowBit(num) num&(-num)
#define CLR(arr,val) memset(arr,val,sizeof(arr))
int total,n,Fx,Fy,Tx,Ty,value,sum,Count=1,C[MAX][MAX];
void Add(int x,int y,int val)
{   for(int i=x;i<MAX;i+=LowBit(i))
        for(int j=y;j<MAX;j+=LowBit(j))
            C[i][j]+=val;
}
int Sum(int x,int y)//求从位置(1,1)到(x,y)的元素之和
{   int sum=0;
    for(int i=x;i>0;i-=LowBit(i))
        for(int j=y;j>0;j-=LowBit(j))
            sum+=C[i][j];
    return sum;
}   
int main()
{   scanf("%d",&total);
    while(total--)
    {   CLR(C,0);
        for(int i=1;i<MAX;i++)//记得要初始化为1 
            for(int j=1;j<MAX;j++)
                Add(i,j,1);
        printf("Case %d:\n",Count++);
        scanf("%d",&n);
        for(int i=0;i<n;i++)
        {   char c[5];
            scanf("%s",c);
            switch(c[0])
            {   case 'A':
                    scanf("%d%d%d",&Fx,&Fy,&value);//在(Fx,Fy)处增加value本书 
                    Add(Fx+1,Fy+1,value);
                    break;
                case 'D':
                    scanf("%d%d%d",&Fx,&Fy,&value);//在(Fx,Fy)处移除value本书,如果这里的书不足,则只移除原有的书
                    sum=Sum(Fx+1,Fy+1)+Sum(Fx,Fy)-Sum(Fx+1,Fy)-Sum(Fx,Fy+1);//求出(Fx+1,Fy+1)位置的书的数目,可以用容斥定理解决
                    value=min(value,sum);
                    Add(Fx+1,Fy+1,-value); 
                    break;
                case 'M':
                    scanf("%d%d%d%d%d",&Fx,&Fy,&Tx,&Ty,&value);//从(Fx,Fy)处移value本书到(Tx,Ty)处,同上面不足的话移除所有的书 
                    sum=Sum(Fx+1,Fy+1)+Sum(Fx,Fy)-Sum(Fx+1,Fy)-Sum(Fx,Fy+1);
                    value=min(sum,value);
                    Add(Fx+1,Fy+1,-value);
                    Add(Tx+1,Ty+1,value);
                    break;
                default:   
                    scanf("%d%d%d%d",&Fx,&Fy,&Tx,&Ty);
                    if(Fx>Tx) swap(Fx,Tx);
                    if(Fy>Ty) swap(Fy,Ty);
                    sum=Sum(Tx+1,Ty+1)+Sum(Fx,Fy)-Sum(Fx,Ty+1)-Sum(Tx+1,Fy);  
                    printf("%d\n",sum);        
            }
        }
    }
    return 0;
}

求点(Fx+1,Fy+1)的书的数目:

可知:由面积关系可知,B点的值为:Sum(Fx+1,Fy+1)+Sum(Fx,Fy)-Sum(Fx+1,Fy)-Sum(Fx,Fy+1);

最后的求(Fx,Fy)到(Tx+1,Ty+1)的元素之和同样可求,即求ABCD的面积,只是C(Fx,Fy),B(Tx+1,Ty+1),A(Fx,Ty+1),D(,Tx+1,Fy),这个时候面积为:

Sum(Tx+1,Ty+1)+Sum(Fx,Fy)-Sum(Fx,Ty+1)-Sum(Tx+1,Fy)

三维树状数组:关键是点的值的变化,如下图,只标了几个点的坐标,O不一定指原点,只是为了立体感强点。

例题4:HDU 3584(Cube)

 

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int MAX=110;
#define LowBit(num) num&(-num)
#define CLR(arr,val) memset(arr,val,sizeof(arr))
int n,m,C[MAX][MAX][MAX]; 
void Add(int x,int y,int z,int val)
{   for(int i=x;i<=n;i+=LowBit(i))
        for(int j=y;j<=n;j+=LowBit(j))
            for(int k=z;k<=n;k+=LowBit(k))
                C[i][j][k]+=val; 
}
int Sum(int x,int y,int z)
{   int sum=0;
    for(int i=x;i>0;i-=LowBit(i))
        for(int j=y;j>0;j-=LowBit(j))
            for(int k=z;k>0;k-=LowBit(k))
                sum+=C[i][j][k];
    return sum;             
}
int main()
{   while(scanf("%d%d",&n,&m)!=EOF)
    {   CLR(C,0);
        for(int i=0;i<m;i++)
        {   int comp,Sx,Sy,Sz,Ex,Ey,Ez;
            scanf("%d",&comp);
            switch(comp)
            {   case 0:
                    scanf("%d%d%d",&Sx,&Sy,&Sz);
                    printf("%d\n",Sum(Sx,Sy,Sz)&1);
                    break;
                default:
                    scanf("%d%d%d%d%d%d",&Sx,&Sy,&Sz,&Ex,&Ey,&Ez);
                    //变成立方体了~~~~ 
                    Add(Sx,Sy,Sz,1);  
                    Add(Sx,Ey+1,Ez+1,1);
                    Add(Ex+1,Sy,Ez+1,1);
                    Add(Ex+1,Ey+1,Sz,1);
                    Add(Ex+1,Sy,Sz,1);
                    Add(Sx,Ey+1,Sz,1);
                    Add(Sx,Sy,Ez+1,1);
                    Add(Ex+1,Ey+1,Ez+1,1);   
            }
        }
    }
    return 0;
}
涉及到树状数组的题目有: HDOJ 1116,1394,1541,1556,1640,1867,2227,,2275,2430,2492,2642,2668,2689,2838,2852,3047,3450,3743,3887,4031, POJ1195,1990,2029,2155,2299,2352,2464,2481,3067,3321,3468,

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值