基础知识
转载请注明出处:bestsort.cn
我们先回想一下二叉树(什么你说你不知道什么是二叉树?)
也就是类似这张图的一种数据结构,我们不难看出,最底层是按照 2 的 n 次方不断增加的。
那如果我们把第一层设为 [1,N] ,然后开始不断往下下放,什么你问我 N 一定会被分成不同的 2
的 x(x∈Z)的 m 个数之和?
这个问题问的好,要不咱仔细想想计算机为什么是 2 进制的
然后我们变一下形(其实不变也可以只不过这样好理解)如图
接着讲,我们于是就可以不断在 2 的 x 次方内下放,最后我们可以把 [1,X] 分成 O(log x)个小区间,这些小区间的共同特点是:若区间结尾为 R ,则区间长度就等于 R 的“二进制分解”下最小的 2 的次幂,即 lowbit(R)
int lowbit(int x){
return x&(-x);
}
例如:x = 7 = 2^2 + 2^1 + 2^0,区间 [1,7] 可以分成 [1,4] [5,6] [7,7] 三个小区间
什么?你还是不理解?你连lowbit()也不知道?
lowbit 就是找出整数在二进制表示下所有等于 1 的位。
它实现的操作是类似于 x&(-x) 的操作
对于一个数的负数就等于对这个数取反+1
也就是说补码和原码必然相反,所以原码有0的部位补码全是1,补码再+1之后由于进位那么最末尾的1和原码最右边的1一定是同一个位置(当遇到第一个1的时候补码此位为0,由于前面会进一位,所以此位会变为1)
所以我们只需要进行 x&(-x) 就可以取出最低位的 1 了
然后我们再来看两张具体的图
注意:其中灰色的节点已经被上层覆盖,并没有实际意义
记住了吧
然后我们再来看看二进制转换后的这个图
是不是好理解一些了,如果还是有些想不通,可以自己模拟一下
(101 + 1 -> 110 + 10 -> 1000 + 1000 -> 10000)
常用操作
单点更新:
void update(int x,int z){//x指位置,z指改变的值
while(x<=n){
a[x]+=z;
x+=lowbit(x);
}
/*
for(int i=x;i<=n;i+=lowbit(x)){//这个会更快
a[i]+=z;
}
*/
}
区间查询:
int getsum(int x){
int ans=0;
while(x){
ans+=a[x];
x-=lowbit(x);
}
return ans;
}
什么你问我怎么区间查询?
可以利用前缀和的思路,用 getsum(y) - getsum(x-1) 求出来
接下来我们尝试理解一些进阶的思路
求逆序对:
首先我们对输入的 a 数组进行离散化
什么你问我什么是离散化?
有时候我们输入一个数组,它不一定是连续的,有可能跨度非常大,但是我们用树状数组求逆序对,就需要我们把它变成一个个连续的点(可以随便排列)所以我们要怎么来求离散化后的数组呢
我们在加一个数组 b 存下 a 数组,然后我们排序 b 数组,再用 lower_bound() 找到 b 数组中对应 a 数组的位置,这样我们就实现了离散化
什么你问我 lower_bound() 怎么用?
lower_bound(ForwardIter first, ForwardIter last,const _Tp& val)算法返回一个非递减序列[first, last)中的第一个大于等于值val的位置。
upper_bound(ForwardIter first, ForwardIter last, const _Tp& val)算法返回一个非递减序列[first, last)中的第一个大于值val的位置。
然后我们模拟一次
a = [3,4,5,2147483647,1] = b
b = [1,3,4,5,2147483647]
在 for 循环里,3 -> 2 4->3 5 ->4 2147483647 -> 5 1 -> 1
然后 a 数组就变成了 a = [2,3,4,5,1]
for(int i=1;i<=n;i++){
cin>>a[i];
b[i]=a[i];
}
sort(b+1,b+1+n);
for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+1+n,a[i])-b;
解决完离散化的问题,我们就来思考怎么求逆序对呢
我们还用刚才的那个例子
a = [3,4,5,2147483647,1]
b = [2,3,4,5,1]
注意:我们以下的操作都要遵循树状数组的运算规则,即 lowbit() 运算
这里有点难理解,不如尝试自己画图理解一下
我们按顺序插入 b 数组中的值,插入每个值我们都往它“上面的值”进行 +1 的操作,这样我们就可以知道如果从这个点开始往下下放会有几个值(也就时此时这个点前面有几个值已经在 a 数组中了),之后求它对应的逆序对数时需要用到,并且在插入一个值后我们直接就进行求逆序对的操作,那么问题来了,我们怎么求逆序对呢
我们已经知道,getsum(int x) 函数可以求 [1,x] 的和,那如果我们加到其中一个 x’ 时,我们加上的正是它本身在当前树状数组里向下下放会有多少个值(因为我们之前有 +1 的操作),这时我们可以用代换的思路,我们既然求出它的顺序时 getsum(x) 那么它的逆序对就是 i - getsum(x) 我们用 ans 加上它的和就可以了
这里因为树状数组用的是 lowbit() 操作,所以我们不能直接用正常的思路来理解 a 数组的值,要联系上树状数组的存储规则,如果还是不理解我们用一下刚才的样例
b = [2,3,4,5,1]
2 3 4 5 1
1 2 3 4 5
Case 1 // 将 2 插入到 a 数组中
0 1 0 1 0
Case 2 // 将 3 插入到 a 数组中
0 1 1 2 0
Case 3 // 将 4 插入到 a 数组中
0 1 1 3 0
Case 4 // 将 5 插入到 a 数组中
0 1 1 3 1
Case 5 // 将 1 插入到 a 数组中
1 2 1 4 1
怎么样是不是更好理解一些了,我们存进来的值还要往上 +1 就是要知道之后插入被加过的值时,它前面已经有几个值了,只不过用的是树状数组的存储规律比较超出常理,
#include<iostream>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int n;
int a[100100];
int b[100100];
int lowbit(int x){
return x&(-x);
}
void update(int x){
while(x<=n){
a[x]++;
x+=lowbit(x);
}
}
int getsum(int x){
int cnt=0;
while(x){
cnt+=a[x];
x-=lowbit(x);
}
return cnt;
}
int main(){
cin>>n;
int z;
for(int i=1;i<=n;i++){
cin>>a[i];
b[i]=a[i];
}
sort(b+1,b+1+n);
for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+1+n,a[i])-b;
for(int i=1;i<=n;i++){
b[i]=a[i];
a[i]=0;
}
long long ans=0;
for(int i=1;i<=n;i++){
update(b[i]);
ans+=i-getsum(b[i]);
}
cout<<ans<<endl;
return 0;
}
这就是完整的求一个 n 序列的逆序对问题,很显然我把单点修改变成了 +1 操作
求区间最大值:
其实只要我们理解了刚才逆序对的思路,就比较好理解这个了
刚才说 a 数组存的是 i 对应的前面有几个值是正序的,那我们可以改变一下它存的值,变成存储前几个值最大的值
void update(int x,int v){
while(x<=n){
a[x]=max(a[x],v);
x+=lowbit(x);
}
}
int query(int x){
int ans=0;
while(x){
ans=max(ans,a[x]);
x-=lowbit(x);
}
return ans;
}
区间修改:
直接去学一下线段树
那么我们怎么用树状数组实现区间更新呢
我们可以考虑一下利用前缀和差分来实现,假如我们要修改 [L,R] 加上 x ,这时我们可以在 a[L]+=x,a[R+1]-=x;
然后当单点查询时,我们把这个下放就可以了
什么你想知道怎么区间查询?
区间修改后怎么实现区间查询:
该部分内容转自胡小兔的OI博
我们刚才说了,区间修改就是实现前缀和差分,而之前单点修改后区间查询也是用的前缀和差分的思路,那么这就很有意思了,我们面对的是一个前缀和套前缀和的东西
位置p的前缀和是
在等式最右侧的式子中,被用了p次,被用了次……那么我们可以写出:位置p的前缀和 =
那么我们可以维护两个数组的前缀和:
一个数组是
另一个数组是
查询
位置p的前缀和即:数组中p的前缀和 - sum2数组中p的前缀和。
区间[l, r]的和即:位置r的前缀和 - 位置l的前缀和。
修改
对于sum1数组的修改同问题2中对d数组的修改。
对于sum2数组的修改也类似,我们给 sum2[l] 加上 l * x,给 sum2[r + 1] 减去 (r + 1) * x。
void add(int p, int x){
for(int i=p;i<=n;i+=lowbit(i)){
sum1[i]+=x;
sum2[i]+=x*p;
}
}
void range_add(int l,int r,int x){
add(l,x);
add(r+1,-x);
}
int ask(int p){
int res=0;
for(int i=p;i;i-=lowbit(i))
res+=(p+1)*sum1[i]-sum2[i];
return res;
}
int range_ask(int l, int r){
return ask(r)-ask(l-1);
}
二维树状数组
声明:大部分摘抄的他人的博客,只供学习使用
这个说实话我目前也没用过,只是听说有这么个东西,这就是你抄别人博客的理由,害,读书人的事儿能叫抄吗。顾名思义,它可以用到类似于矩阵的操作
其实就是在一维树状数组的一个点处又建了一个一维树状数组
在一维树状数组中,a[x](树状数组中的那个“数组”)记录的是右端点为x、长度为lowbit(x)的区间的区间和。
那么在二维树状数组中,可以类似地定义a[x][y]记录的是右下角为(x, y),高为lowbit(x), 宽为 lowbit(y)的区间的区间和。
单点修改+区间查询
void add(int x, int y, int z){ //将点(x, y)加上z
int memo_y = y;
while(x <= n){
y = memo_y;
while(y <= n)
tree[x][y] += z, y += y & -y;
x += x & -x;
}
}
void ask(int x, int y){//求左上角为(1,1)右下角为(x,y) 的矩阵和
int res = 0, memo_y = y;
while(x){
y = memo_y;
while(y)
res += tree[x][y], y -= y & -y;
x -= x & -x;
}
}
区间修改 + 单点查询
我们对于一维数组进行差分,是为了使差分数组前缀和等于原数组对应位置的元素。
那么如何对二维数组进行差分呢?可以针对二维前缀和的求法来设计方案。
二维前缀和:
那么我们可以令差分数组表示与 的差。
例如下面这个矩阵
1 4 8
6 7 2
3 9 5
对应的差分数组就是
1 3 4
5 -2 -9
-3 5 1
当我们想要将一个矩阵加上x时,怎么做呢?
下面是给最中间的3*3矩阵加上x时,差分数组的变化:
0 0 0 0 0
0 +x 0 0 -x
0 0 0 0 0
0 0 0 0 0
0 -x 0 0 +x
这样给修改差分,造成的效果就是:
0 0 0 0 0
0 x x x 0
0 x x x 0
0 x x x 0
0 0 0 0 0
void add(int x, int y, int z){
int memo_y = y;
while(x <= n){
y = memo_y;
while(y <= n)
tree[x][y] += z, y += y & -y;
x += x & -x;
}
}
void range_add(int xa, int ya, int xb, int yb, int z){
add(xa, ya, z);
add(xa, yb + 1, -z);
add(xb + 1, ya, -z);
add(xb + 1, yb + 1, z);
}
void ask(int x, int y){
int res = 0, memo_y = y;
while(x){
y = memo_y;
while(y)
res += tree[x][y], y -= y & -y;
x -= x & -x;
}
}
区间修改 + 区间查询
类比之前一维数组的区间修改区间查询,下面这个式子表示的是点(x, y)的二维前缀和:
(d[h][k]为点(h, k)对应的“二维差分”(同上题))
这个式子炒鸡复杂( 复杂度!),但利用树状数组,我们可以把它优化到!
首先,类比一维数组,统计一下每个出现过多少次。出现了次,出现了次……出现了 次。
那么这个式子就可以写成:
把这个式子展开,就得到:
那么我们要开四个树状数组,分别维护:
,,,
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#include <iostream>
using namespace std;
typedef long long ll;
ll read(){
char c; bool op = 0;
while((c = getchar()) < '0' || c > '9')
if(c == '-') op = 1;
ll res = c - '0';
while((c = getchar()) >= '0' && c <= '9')
res = res * 10 + c - '0';
return op ? -res : res;
}
const int N = 205;
ll n, m, Q;
ll t1[N][N], t2[N][N], t3[N][N], t4[N][N];
void add(ll x, ll y, ll z){
for(int X = x; X <= n; X += X & -X)
for(int Y = y; Y <= m; Y += Y & -Y){
t1[X][Y] += z;
t2[X][Y] += z * x;
t3[X][Y] += z * y;
t4[X][Y] += z * x * y;
}
}
void range_add(ll xa, ll ya, ll xb, ll yb, ll z){ //(xa, ya) 到 (xb, yb) 的矩形
add(xa, ya, z);
add(xa, yb + 1, -z);
add(xb + 1, ya, -z);
add(xb + 1, yb + 1, z);
}
ll ask(ll x, ll y){
ll res = 0;
for(int i = x; i; i -= i & -i)
for(int j = y; j; j -= j & -j)
res += (x + 1) * (y + 1) * t1[i][j]
- (y + 1) * t2[i][j]
- (x + 1) * t3[i][j]
+ t4[i][j];
return res;
}
ll range_ask(ll xa, ll ya, ll xb, ll yb){
return ask(xb, yb) - ask(xb, ya - 1) - ask(xa - 1, yb) + ask(xa - 1, ya - 1);
}
int main(){
n = read(), m = read(), Q = read();
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
ll z = read();
range_add(i, j, i, j, z);
}
}
while(Q--){
ll ya = read(), xa = read(), yb = read(), xb = read(), z = read(), a = read();
if(range_ask(xa, ya, xb, yb) < z * (xb - xa + 1) * (yb - ya + 1))
range_add(xa, ya, xb, yb, a);
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++)
printf("%lld ", range_ask(i, j, i, j));
putchar('\n');
}
return 0;
}
再次声明,本博客仅作学习使用