【常用算法总结——树状数组】

  树状数组真的是个好东西啊(虽然只会套模板,没理解根本意思)在学它之前,我们要先知道为什么会有树状数组。

树状数组

  有这样一道题

  已知一个数列长度为n,你需要进行w次修改一个值和q次查询一个区间内的和。

  看起来很简单吧,但是这种题的数据范围往往很大,一般的做题,修改是O(1)的复杂度,而查询是O(n),所以综合就是O(qn)。你可能觉得会用前缀和做起来更快,但是它的查询是O(1),修改是O(n),所以是O(wn),还是差不多。我们想一想,这两种方式都是要不O(1),要不O(n)的极端情况,有没有一种方法能综合一下呢?此时,树状数组就出现了。

lowbit

  要想使用树状数组,lowbit函数是必不可少的,他具体是什么呢?它就是用来求一个数2进制中最低一位的1,例如7=(111),所以lowbit(7)=1,又例如8=(1000),所以lowbit(8)=8.那怎么求lowbit(x)呢?这需要我们自定义了,大家记一下就行,具体原理,上网查一下吧。

 

1 int lowbit(x) 
2 {   
3     return x - (x & (x - 1));
4 }
1 int lowbit(x) 
2 {   
3     return x & -x;
4 }

实现树状数组

【常用算法总结——树状数组】_第1张图片

 

 我们定义一个c数组(树状数组),再定义一个a数组存原本的值。

由这个图可得

c[1]=a[1]

c[2]=a[1]+a[2]

c[3]=a[3]

c[4]=a[1]+a[2]+a[3]+a[4]

c[5]=a[5]

..............

查询前缀和

  如果我们要查找a[3]到a[5]区间的和,我们就需要用前缀和来表示,就是a[5]+a[4]+a[3]+a[2]+a[1]-a[2]-a[1]

  但是怎么来求a[1]-a[5]的和呢?由图得,这段和=c[5]+c[4]问题又来了,怎么由5得到4呢?好巧不巧,lowbit(5)刚好等于一,减去它正好得到4,而lowbit(4)又等于4,所以就减下去,一直到0,每次又加上这一个下标的的值,就有了a[1]-a[5]的和。

  再来举一个例子,我们求a[1]-a[7],这段和=c[7]+c[6]+c[4],而怎么一步一步向下求到下标呢?lowbit(7)=1,  7-1=6, lowbit(6)=2,6-2=4    lowbit(4)=4,  4-4=0,这样,这段值就求出来了,所以这里的区间求和操作的代码也就出来了。

 1 int sum(int x)
 2 {
 3     int ans=0;
 4     while(x>0)
 5     {
 6         ans+=c[x];
 7         x-=lowbit(x);
 8     }
 9         return ans;
10 }
11     

然后查询区间里的值就很方便了,如果求2-9,就是sum(9)-sum(2-1)

更新后缀和(单点修改)

   我们看到修改单点值的操作,如果修改a[9],那么与之相关的c[9]c[10]c[12]c[16]....的值都要发生变化,所以我们一个一个的改变就行了,还是用lowbit来修改:(假设a[9]加上x,总共有16个点)先把c[9]

加上x,因为lowbit(9)=1,所以c[9+1]也加上x,因为lowbit(10)=2,所以c[12]加上x....一直到16.

 

1 void update(int x,int v)
2 {
3     while(x<=n)
4     {
5         c[x]+=v;
6         x+=lowbit(x);
7     }
8 }

 为了检验一下成果,我们来做一道题吧P3374 【模板】树状数组 1

这道题我就直接贴代码了

 1 #include
 2 #define N 500005
 3 using namespace std;
 4 int n,m; 
 5 int c[N];
 6 int lowbit(int x)
 7 {
 8     return x&(-x);
 9 }
10 void update(int x,int v)
11 {
12     while(x<=n)
13     {
14         c[x]+=v;
15         x+=lowbit(x);
16     }
17 }
18 int sum(int x)
19 {
20     int ans=0;
21     while(x>0)
22     {
23         ans+=c[x];
24         x-=lowbit(x);
25     }
26     return ans;
27 }
28 int main()
29 {
30     scanf("%d%d",&n,&m);
31     for(int i=1;i<=n;i++)
32     {
33         int y;
34         scanf("%d",&y);
35         update(i,y);
36     }
37     for(int i=1;i<=m;i++)
38     {
39         int k,x,y;
40         scanf("%d%d%d",&k,&x,&y);
41         if(k==1)
42         {
43             update(x,y);
44         }
45         else
46         {
47             cout<1)<<endl;
48         }
49      } 
50 } 

然后我们看到区间修改和单点查询操作,但在这之前,我们来看一看差分

差分

  假设有一数组a[]={2,5,9,6,4,8},那么查分数组b[]{2,3,4,-3,-2,4};所以b[i]=a[i]-a[i-1],

  所以可以得出a[i]=b[1]+b[2]+b[3]+...+b[i],(这个很容易理解吧)

  我么假设区间[2,4]都加上3,然后a[]={2,8,12,9,4,8},b[]={2,6,4,-3,-5,4}

  不难看出,b数组只有b[2]加上了3,然后b[4+1]减去了3,

  所以如果区间[x,y]加上z就是b[x]+z,b[y+1]-z,

  然后我们就把b数组设为树状数组,每次区间修改就等于只进行两次单点修改,每次单点查询只需要这个点树状数组的前缀和(差分)加上他原来的值就可以了。

  为了检验一下成果,我们来做一道水题吧P3368 【模板】树状数组 2

 1 #include
 2 using namespace std;
 3 int tree[500005];//差分数组(树状数组) 
 4 int num[500005];//原来的值 
 5 int n,m;
 6 int lowbit(int x)
 7 {
 8     return x&(-x);
 9 }
10 void update(int x,int v)
11 {
12     while(x<=n)
13     {
14         tree[x]+=v;
15         x+=lowbit(x);
16     }
17 }
18 int sum(int x)
19 {
20     int ans=0;
21     while(x>0)
22     {
23         ans+=tree[x];
24         x-=lowbit(x);
25     }
26     return ans;
27 }
28 int main()
29 {
30     scanf("%d%d",&n,&m);
31     for(int i=1;i<=n;i++)
32     {
33         scanf("%d",&num[i]);
34     }
35     while(m--)
36     {
37         int x,y,k,v;
38         scanf("%d",&k);
39         if(k==1)
40         {
41             scanf("%d%d%d",&x,&y,&v);
42             update(x,v);
43             update(y+1,-v);    
44         }
45         else
46         {
47             scanf("%d",&x);
48             cout<endl;
49         }
50     }
51         
52 }

好啦,学完了树状数组,又感觉自己变强了呢,多练习一下之后,可以去了解一下线段树(和树状数组差不多)

你可能感兴趣的:(【常用算法总结——树状数组】)