1 lowbit运算
2 树状数组
2.1 需解决的问题
- 设计函数getSum(x),返回前x个数之和A[1]+......+A[x]。
- 设计函数update(x, v),实现将第x个数加上一个数v的功能,即A[x]+=v。
2.2 树状数组
树状数组 C[i] 其实仍然是一个数组,是一个用来记录和的数组,下标必须从1开始,它存放的是在i号位之前(含i号位,下同)lowbit(i)个整数之和。(并不是前i个整数之和)
![](https://i-blog.csdnimg.cn/blog_migrate/f06939ed9abff3823bf2e3a0de64ac98.png)
2.3 树状数组如何实现getSum(x)
记SUM(1, x) = A[1]+...+A[x]
由于C[x]的覆盖长度是lowbit(x),因此 C[x] = A[x-lowbit(x)+1]+...+A[x]
于是可以得到SUM(1, x)=SUM(1, x-lowbit(x)) + C[x]
int lowbit(int x){
return x & (-x);
}
// getSum函数返回前x个整数之和
int getSum(int x){
int sum=0;
for(int i=x;i>0;i-=lowbit(i)){ // 注意边界是 i>0
sum+=C(i);
}
return sum;
}
getSum 时间复杂度分析:由于lowbit(i) 的作用是定位i的二进制中最右边的1,因此i = i - lowbit(i)事实上是不断把i的二进制中最右边的1置为0的过程。所以getSum函数的for循环执行次数为x的二进制中1的个数,也就是说,getSum函数的时间复杂度为 O(logN)。
如果要求数组下标在区间[x, y]内的数之和,即A[x] + A[x+1] + ... + A[y],可以转换成getSum(y) - getSum(x-1)来解决。
2.4 树状数组如何实现update(x, v)
要让A[x]加上v,就是要寻找树状数组C中能覆盖A[x]的那些元素,让它们都加上v。
![](https://i-blog.csdnimg.cn/blog_migrate/f06939ed9abff3823bf2e3a0de64ac98.png)
从图上直观来看,只需要总是寻找离当前的“矩形” C[x]最近的“矩形” C[y],使得C[y]能够覆盖C[x]即可。
由于lowbit(y) > lowbit(x),问题等价于求一个尽可能小的整数a,使得lowbit(x+a) > lowbit(x)。
显然,由于lowbit(x)是取x的二进制最右边的1的位置,因此如果lowbit(a)<lowbit(x), lowbit(x+a)就会小于lowbit(x)。为此lowbit(a)必须不小于lowbit(x)。
当a取lowbit(x)时,由于x和a的二进制最右边的1的位置相同,因此x+a会在这个1的位置上产生进位,使得进位过程中的所有连续的1变成0,直到把它们左边第一个0置为1时结束。于是lowbit(x+a) > lowbit(x)显然成立,最小的a就是lowbit(x)。
于是update函数的做法就很明确了,只要让x不断加上lowbit(x),并让每步的C[x]都加上v,直到x超过给定的数据范围为止(因为在不给定数据范围的情况下,更新操作是无上限的)。
int lowbit(int x){
return x & (-x);
}
// update函数将第x个整数加上v
void update(int x,int v){
for(int i=x;i<=N;i+=lowbit(i)){ // 注意i必须能取到N
C[i]+=v; // 让C[i]加上v,然后让C[i+lowbit(i)]加上v
}
}
显然,这个过程是从右至左不断定位x的二进制最右边的1的左边的0的过程,因此update函数的时间复杂度为O(logN)。
![](https://i-blog.csdnimg.cn/blog_migrate/6734ee5883ec12660ed9ce1ff2d82105.png)
2.5 离散化
离散化,把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率。
通俗的说,离散化是在不改变数据相对大小的条件下,对数据进行相应的缩小。例如:
原数据:1,999,100000,15;处理后:1,3,4,2;
原数据:{100,200},{20,50000},{1,400};处理后:{3,4},{2,6},{1,5};
- 一般来说,离散化只适用于离线查询,因为必须知道所有出现的元素之后才能方便进行离散化。
- 不过对于在线查询来说,可以先把所有操作都记录下来,然后对其中出现的数据进行离散化,之后再按照记录下来的操作顺序正常进行“在线”查询即可。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN=1e5+10;
int lowbit(int i) {
return i & (-i);
}
struct Node { // 辅助离散化的实现
int val; // 序列元素的值
int pos; // 原始序号
bool operator< (const Node& y) const {
return val < y.val;
}
} temp[MAXN]; // temp数组临时存放输入数据
int A[MAXN]; // 离散化后的原始数组
int c[MAXN]; // 树状数组
// update函数将第x个整数加上v
void update(int x,int v) {
for(int i=x; i<MAXN; i+=lowbit(i)) {
c[i]+=v;
}
}
// getSum函数返回前x个整数之和
int getSum(int x) {
int sum=0;
for(int i=x; i>0; i-=lowbit(i)) {
sum+=c[i];
}
return sum; // 返回和
}
int main() {
int n,x;
cin>>n;
memset(c,0,sizeof(c)); // 树状数组初始值为0
for(int i=0; i<n; i++) {
cin>>temp[i].val; // 输入序列元素
temp[i].pos = i; // 原始序号
// update(x,1); // x的出现次数加1
// cout<<getSum(x-1)<<endl; // 查询当前小于x的数的个数
}
// 离散化
sort(temp,temp+n); // 按val从小到大排序
for(int i=0; i<n; i++) {
// 与上一个元素值不同时,赋值为元素个数
if(i==0 || temp[i].val!=temp[i-1].val)
A[temp[i].pos] = i+1; // [注意]这里必须从1开始
else // 与上一个元素值相同时,直接继承
A[temp[i].pos] = A[temp[i-1].pos];
}
// 正式进入更新、求和操作
for(int i=0; i<n; i++) {
update(A[i],1); // A[i]的出现次数加1
cout<<getSum(A[i]-1)<<endl; // 查询当前小于A[i]的数的个数
}
return 0;
}
3 树状数组的应用
3.1 统计序列中在元素左边比该元素小的元素个数
问题描述:给定一个有N个正整数的序列A(N<=10^5,A[i]<=10^5),对序列中的每个数,求出序列中它左边比它小的个数。(其中“小”的定义根据题目而定,并不一定必须是数值的大小)
问题分析:
先来看使用hash数组的做法,其中hash[x]记录整数x当前出现的次数。接着,从左到右遍历序列A,假设当前访问的是A[i],那么就令hash[A[i]]加1,表示当前整数A[i]的出现次数增加了一次;同时,序列中在A[i]左边比A[i]小的数的个数等于hash[1]+hash[2]+...+hash[A[i]-1],这个和需要输出。但是很显然,这两个工作可以通过树状数组的update(A[i], 1)和getSum(A[i]-1)来解决。
使用树状数组时,不必真的建一个hash数组,因为它只存在于解法的逻辑中,并不需要真的用到,只需用一个树状数组来代替它即可。
#include <iostream>
#include <cstring>
using namespace std;
const int MAXN=1e5+10;
int lowbit(int i) {
return i & (-i);
}
int c[MAXN]; // 树状数组
void update(int x,int v) {
for(int i=x; i<MAXN; i+=lowbit(i)) {
c[i]+=v;
}
}
int getSum(int x) {
int sum=0;
for(int i=x; i>0; i-=lowbit(i)) {
sum+=c[i];
}
return sum; // 返回和
}
int main() {
int n,x;
cin>>n;
memset(c,0,sizeof(c)); // 树状数组初始值为0
for(int i=0; i<n; i++) {
cin>>x; // 输入序列元素
update(x,1); // x的出现次数加1
cout<<getSum(x-1)<<endl; // 查询当前小于x的数的个数
}
return 0;
}
3.2 求序列第K大
这个问题等价于寻找第一个满足条件“getSum(i)>=K”的i(即i值最小)
针对这个问题,由于getSum(i)值随着i递增,可以令left=1、right=MAXN,然后在[left, right]范围内进行二分,对当前的mid,判断getSum(mid)>=K是否成立:如果成立,说明所求位置不超过mid,因此令right=mid;如果不成立,说明所求位置大于mid,因此令left=mid+1。如此二分,直到left<right不成立为止。显然二分的时间复杂度是O(logn),求和的时间复杂度也是O(logn),因此总复杂度是O(logn*logn)。
// 求序列元素第K大
int findKthElement(int K) {
int left=1,right=MAXN,mid;
while(left<right) { // 循环,直到[left, right]能锁定单一元素
mid = (left+right)/2;
if(getSum(mid)>=K)
right = mid;
else
left = mid+1;
}
return left; // 返回二分夹出来的元素
}
4 拓展——高维树状数组
如果给定一个二维整数矩阵A,怎样求A[1][1] ~ A[x][y]这个子矩阵中所有元素之和,以及怎样给单点A[x][y]加上整数v?
事实上只需把树状数组推广到二维即可。具体做法是,直接把update函数和getSum函数中的for循环改为两重。
如果想求A[a][b] ~ A[x][y]这个子矩阵的元素之和,只需计算getSum(x, y) - getSum(x-1, y) - getSum(x, y-1) + getSum(x-1, y-1)即可。
更高维的情况只需把for循环改为相应的重数即可。
int c[MAXN][MAXN]; // 二维树状数组
// 二维update函数位置为(x, y)的整数加上v
void update(int x,int y,int v){
for(int i=x;i<MAXN;i+=lowbit(i)){
for(int j=y;j<MAXN;j+=lowbit(j)){
c[i][j] += v;
}
}
}
// 二维getSum函数返回(1, 1)到(x, y)的子矩阵中元素之和
int getSum(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; // 返回和
}
5 拓展——区间更新、单点查询
到这里为止,前面都是在对树状数组进行单点更新、区间查询,下面将讨论区间更新、单点查询的问题。
5.1 需解决的问题
- 设计函数getSum(x),返回A[x]
- 设计函数update(x, v),将A[1] ~ A[x]的每个数都加上一个数v
5.2 解题思路
首先,树状数组C中每个“矩形”C[i]仍然保持和之前一样的长度,即lowbit(i)。只不过C[i]不再表示这段区间的元素之和,而是表示这段区间每个数当前被加了多少。
如下图,C[16] = 0表示A[1] ~ A[16]都被加了0,C[8] = 5表示A[1] ~ A[8]都被加了5,C[6]=3表示A[5] ~ A[6]都被加了3,C[5]=6表示A[5]被加了6。
显然,对A[5]来说,它被C[5]加了6,被C[6]加了3,被C[8]加了5,因此实际上的A[5]的值应当是C[5]+C[6]+C[8] = 14
很快就会发现,A[x]的值实际就是覆盖它的若干个“矩形”C[i]的值之和,而这显然是之前“单点更新、区间查询”问题中update函数的做法。
![](https://i-blog.csdnimg.cn/blog_migrate/a14d793ded466975696f76f2e760d589.png)
// getSum函数返回第x个整数的值
int getSum(int x){
int sum=0;
for(int i=x;i<MAXN;i+=lowbit(i))
sum+=c[i];
return sum;
}
接着来看update函数。此处的update需要把A[1] ~ A[x]的每个数都加上v,它等价于让C[x]加上v,然后执行UPDATE(1, x-lowbit(x))
如下图,让A[1] ~ A[14]的每个数都加上6,等价于让C[8]、C[12]、C[14]加上6
![](https://i-blog.csdnimg.cn/blog_migrate/a850b9a73e3ca4fcdef92aa1124e676c.png)
// update函数将前x个整数都加上v
void update(int x,int v){
for(int i=x;i>0;i-=lowbit(i))
c[i]+=v;
}
显然,如果需要让A[x] ~ A[y]的每个数加上v,只要先让 A[1] ~ A[y]的每个数加上v,然后让A[1] ~ A[x-1]的每个数加上(-v)即可,即先后执行update(y, v)、update(x-1, -v)。