树状数组
前言:
设有一个包含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 }
就没有啦,谢谢观看~