树状数组

前言:

设有一个包含n个数的数组a[ ] = { 9,2,5,6,3,12...... },现要求前i个数的和值,即前缀和sum[ i ] = a[ 1 ]+a[ 2 ]+a[ 3 ]+...+a[ i ](i = 1,2,3...n),可以怎样求?

首先想到的肯定是累加前i个数,但若对a[ i ]进行修改,则sum[ i ],sum[ i +1 ]...sum[ n ]都需要修改,最多需要修改n次,如果数据过大,则所需时间更长。因此,有人提出了树状数组,以便更高效的求解前缀和与数值的修改。

那么,树状数组是如何巧妙实现的呢?

 

树状数组:

树状数组引入了分级管理的思想,设置一个管理小组,每个管理员管理一个或多个连续的元素。如下图:

 管理数组是c[ ],c[ i ]分别管理对应元素,如c[ 2 ]管理a[ 1 ]和a[ 2 ],因为管理数组c[ ]是树状的,所以称为树状数组。

树状数组通过二进制划分管理空间,那么,c[ i ]的管理空间应如何划分呢?

1)区间长度:

若 i 的二进制表示末尾有连续 k 个0,则c[ i ]的区间长度为2k,即c[ i ]管理从a[ i ]起向前数2k个元素,则c[ i ]表示为a[ i-2k+1 ]+a[ i-2k+2 ]+a[ i-2k+3 ]+...+a[ i ]。如图:

 

例如 i =20,其二进制为10100,则区间长度为22=4,也就是其二进制最低位的1与其后面的0构成的二进制数值100,其十进制为4。

当然,这样写起来同样不方便,但在一些大佬的推导下得出了一个公式:

c[ i ]的区间长度为 i 的二进制取反(~)加1在进行与运算(&)。

 下面我们来看 i =20时的例子:

大家会发现, i 取反加1的二进制与 -i 相等,因此可以得出公式:

c[ i ]的区间长度:lowbit(i) = ( -i )& i 。

2)前驱与后继:

直接前驱:c[ i ]的直接前驱为c[ i - lowbit(i) ],即c[ i ]左侧紧邻的子树的根;

直接后继:c[ i ]的直接后继为c[ i +lowbit(i) ],即c[ i ]的父节点;

前驱:c[ i ]左侧的所有子树的根;

后继:c[ i ]的所有祖先。

以c[ 6 ]为例:

所以,sum[ i ]的值为c[ i ]及其所有前驱的和。如:sum[ 7 ] = c[ 7 ] + c[ 6 ] + c[ 4 ]。

a[ i ]到a[ j ]的区间和为c[ j ] - c[ i-1 ]。

了解了这些以后,就可以开写代码啦~

 

首先,写好主函数:

 1 int main()
 2 {
 3     int k;//共k组数据
 4     cin >> k;//输入
 5     while(k--)//循环k次
 6     {
 7         cin >> n >> m;//输入
 8         for(int i=1; i<=n; i++)//循环n次输入
 9         {
10             cin >> data[i];//先输入数据
11             lj(i,data[i]);//再建树
12             //也可以直接这么写
13             /*
14             int temp;//data[]数组是非必要的
15             cin >> temp;
16             lj(i,data[i]);
17             */ 
18         }
19         while(m--)//操作m次
20         {
21             int t,a,b;//操作指令,要操作的数,b
22             cin >> t >> a;//输入,若t==1或t==2,再输入b
23             if(t==1)//t==1,则第a个数加b
24             {
25                 cin >> b;//输入b
26                 lj(a,b);//累加
27             }
28             else if(t==2)//t==2,则求第a个数到第b个数的区间值
29             {
30                 cin >> b;//输入b
31                 cout << qjh(a-1,b) << endl;//求区间和
32             }
33             else//t==3,输出第1个数到第a个数的前缀和
34             {
35                 cout << qzh(a) << endl;//输出
36             }
37         }
38     }
39     return 0;
40 }

接着,写计算区间长度的函数:

 

1 int lowbit(int i)//区间长度
2 {
3     return (-i)&i;//该操作为i取反加一再进行与运算
4 }

(一定要是int类型,要反回值的)

再把累加c[ ]数组的函数写好(我代码里的c[ ]数组写成sz[ ]了):

1 void lj(int a,int b)//累加,构建树状数组(英语不好,拼音来凑)
2 {
3     for(int i=a; i<=n; i+=lowbit(i))//从第i个开始,i的所有后继加b
4     {
5         sz[i]+=b;//加b
6     }
7 }

然后就可以计算前缀和和区间值啦:

 1 int qzh(int a)//前缀和
 2 {
 3     int s=0;//定义一个int变量
 4     for(int i=a; i>0; i-=lowbit(i))//从i开始,i的所有前驱一一累加即为第一个数到i的前缀和
 5     {
 6         s+=sz[i];//累加
 7     }
 8     return s;//返回前缀和
 9 }
10 
11 int qjh(int a,int b)//区间和
12 {
13     return qzh(b)-qzh(a);//用第b个数的前缀和减去第a个数的前缀和即为第a个数到第b个数的区间和
14 }

然后就没了。

上代码~

 

代码:

 1 #include<bits/stdc++.h>
 2 
 3 using namespace std;
 4 
 5 const int maxn=10005;//定义一个大点的数
 6 
 7 int n,m;//每组数据的数字数,操作数
 8 int data[maxn],sz[maxn];//数据数组,树状数组
 9 
10 int lowbit(int i)//区间长度
11 {
12     return (-i)&i;//该操作为i取反加一再进行与运算
13 }
14 
15 void lj(int a,int b)//累加,构建树状数组(英语不好,拼音来凑)
16 {
17     for(int i=a; i<=n; i+=lowbit(i))//从第i个开始,i的所有后继加b
18     {
19         sz[i]+=b;//加b
20     }
21 }
22 
23 int qzh(int a)//前缀和
24 {
25     int s=0;//定义一个int变量
26     for(int i=a; i>0; i-=lowbit(i))//从i开始,i的所有前驱一一累加即为第一个数到i的前缀和
27     {
28         s+=sz[i];//累加
29     }
30     return s;//返回前缀和
31 }
32 
33 int qjh(int a,int b)//区间和
34 {
35     return qzh(b)-qzh(a);//用第b个数的前缀和减去第a个数的前缀和即为第a个数到第b个数的区间和
36 }
37 
38 int main()
39 {
40     int k;//共k组数据
41     cin >> k;//输入
42     while(k--)//循环k次
43     {
44         cin >> n >> m;//输入
45         for(int i=1; i<=n; i++)//循环n次输入
46         {
47             cin >> data[i];//先输入数据
48             lj(i,data[i]);//再建树
49             //也可以直接这么写
50             /*
51             int temp;//data[]数组是非必要的
52             cin >> temp;
53             lj(i,data[i]);
54             */ 
55         }
56         while(m--)//操作m次
57         {
58             int t,a,b;//操作指令,要操作的数,b
59             cin >> t >> a;//输入,若t==1或t==2,再输入b
60             if(t==1)//t==1,则第a个数加b
61             {
62                 cin >> b;//输入b
63                 lj(a,b);//累加
64             }
65             else if(t==2)//t==2,则求第a个数到第b个数的区间值
66             {
67                 cin >> b;//输入b
68                 cout << qjh(a-1,b) << endl;//求区间和
69             }
70             else//t==3,输出第1个数到第a个数的前缀和
71             {
72                 cout << qzh(a) << endl;//输出
73             }
74         }
75     }
76     return 0;
77 }

 

练习:

再来两道练习吧~

1.P3374

代码:

 这个题写起来极其简单,完全就是上边树状数组的默写,尽量不看代码默写。

 1 #include<bits/stdc++.h>
 2 
 3 using namespace std;
 4 
 5 const int maxn=500005;//根据题目定义一个大点的数
 6 
 7 int n,m;//每组数据的数字数,操作数
 8 int data[maxn],sz[maxn];//数据数组,树状数组
 9 
10 int lowbit(int i)//区间长度
11 {
12     return (-i)&i;//该操作为i取反加一再进行与运算
13 }
14 
15 void lj(int a,int b)//累加,构建树状数组
16 {
17     for(int i=a; i<=n; i+=lowbit(i))//从第i个开始,i的所有后继加b
18     {
19         sz[i]+=b;//加b
20     }
21 }
22 
23 int qzh(int a)//前缀和
24 {
25     int s=0;//定义一个int变量
26     for(int i=a; i>0; i-=lowbit(i))//从i开始,i的所有前驱一一累加即为第一个数到i的前缀和
27     {
28         s+=sz[i];//累加
29     }
30     return s;//返回前缀和
31 }
32 
33 int qjh(int a,int b)//区间和
34 {
35     return qzh(b)-qzh(a);//用第b个数的前缀和减去第a个数的前缀和即为第a个数到第b个数的区间和
36 }
37 
38 int main()
39 {
40     cin >> n >> m;//输入
41     for(int i=1; i<=n; i++)//循环n次输入
42     {
43         cin >> data[i];//先输入数据
44         lj(i,data[i]);//再建树
45     }
46     while(m--)//操作m次
47     {
48         int t,a,b;//操作指令,要操作的数,b
49         cin >> t >> a >> b;//输入
50         if(t==1)//t==1,则第a个数加b
51         {
52             lj(a,b);//累加
53         }
54         else//t==2,则求第a个数到第b个数的区间和
55         {
56             cout << qjh(a-1,b) << endl;//求区间和
57         }
58     }
59     return 0;
60 }

 

2.P3368

讲解:

 这个题如果直接在区间内加k的话会超时,所以要引入一个新的东西——差(cha,一声)分。

简单地举个例子:

设数组a[ ] = {1,6,8,5,10},那么差分数组b[ ] = {1,5,2,-3,5},即差分数组b[ i ] = 数据数组a[ i ] - a[ i -1 ],而由b[ i ]还原a[ i ]也不难,a[ i ] = b[ i ] + b[ i -1 ] + ...... + b[ 1 ](可以证)。

如果让a[ 2 ]到a[ 4 ]间都加2,数据数组a变为a[ ] = {1,8,10,7,10},差分数组b变为b[ ] = {1,7,2,-3,3},可以发现,只有b[ 2 ]和b[ 5 ]变了,这样就只用修改两个数。

所以,这个题中需要把树状数组由数值变为数值间的差分进行计算,节省时间。

代码:

 1 #include<bits/stdc++.h>
 2 
 3 using namespace std;
 4 
 5 const int maxn=500005;//根据题目定义一个大点的数
 6 
 7 int n,m;//每组数据的数字数,操作数
 8 int data[maxn],sz[maxn];//数据数组,树状数组(且是差分数组)
 9 
10 int lowbit(int i)//区间长度
11 {
12     return (-i)&i;//该操作为i取反加一再进行与运算
13 }
14 
15 void lj(int a,int b)//累加,构建差分树状数组
16 {
17     for(int i=a; i<=n; i+=lowbit(i))//从第i个开始,i的所有后继加b
18     {
19         sz[i]+=b;//加b
20     }
21 }
22 
23 int qzh(int a)//前缀和(可通过证明验证差分树状数组前i个的前缀和为第i个数的值)
24 {
25     int s=0;//定义一个int变量
26     for(int i=a; i>0; i-=lowbit(i))//从i开始,i的所有前驱一一累加即为第一个数到i的前缀和
27     {
28         s+=sz[i];//累加
29     }
30     return s;//所以这里返回的是树状数组的前缀和,数据数组的第i个数
31 }
32 
33 int qjh(int a,int b)//区间和(写上没用,删了吧)
34 {
35     return qzh(b)-qzh(a);//用第b个数的前缀和减去第a个数的前缀和即为第a个数到第b个数的区间和
36 }
37 
38 int main()
39 {
40     cin >> n >> m;//输入
41     for(int i=1; i<=n; i++)//循环n次输入
42     {
43         cin >> data[i];//先输入数据
44         lj(i,data[i]-data[i-1]);//再建树(因为是从data[1]开始,所以data[1-1]一定为0,sz[1]==data[1])
45     }
46     while(m--)//操作m次
47     {;
48         int a,b;//操作指令
49         cin >> a >> b;//输入
50         if(a==1)//a==1,则第b个数到第c个数加d
51         {
52             int c,d;//
53             cin >> c >> d;//
54             lj(b,d);//
55             lj(c+1,0-d);//
56             
57         }
58         else//a==2,输出第b个数的值(因为前面有部分数已经加上了别的数,所以data[]数组与修改后的sz[]数组不同步,无法继续使用)
59         {
60             cout << qzh(b) << endl;//输出第b个数
61         }
62     }
63     return 0;
64 }

 

 就没有啦,谢谢观看~

 

posted on 2022-09-03 10:51  kkk05  阅读(43)  评论(0编辑  收藏  举报

导航