文章目录
引入问题:
我们要输入一个序列a,要求在线修改一个值或者求一个子串的所有元素的和。
1.朴素算法(说白了就是暴力)
修改值:直接改,时间复杂度:O(1)
求字串的所有元素的和:一个循环,时间复杂度:O(n)
有那么有点慢。
2.前缀和
求字串的所有元素的和:一个计算:sun[i]-sum[i-1],时间复杂度:O(1)
修改值:有点麻烦了,要从 i 循环到 n,时间复杂度:O(n)
也有那么有点慢。
综上所述,好像差不了太多呀!
于是,我们的树状数组闪亮登场了!
但在了解树状数组之前,我们先了解一下lowbit();
lowbit()函数
lowbit()函数所求的就是最低位1的位置,所以可以通过位运算来计算 。
计算方法1:
int lowbit(int x) {
return x&(x^(x-1));
}
首先设x=6,即其二进制为110 。于是我们使 x-1=101 可以发现,当我们将一个二进制数减一时,从最低位一(即lowbit)开始向后的部分与之前全部相反,因为减去的1对后面的每一位都有影响,同时因为是二进制,影响就是让每一位都取反了。
110就变成101
从最低位一(第二位)开始向后全部相反了 所以我们再与 x 异或一下,那么从lowbit开始往后全是1
110^101=011
然后我们再用x与新数按位与一下 因为 x lowbit 以前的部分是1或0,lowbit 是1,之后的部分都是0,新数 lowbit 之前的部分都是0,lowbit 是1,之后的部分都是1 所以与完之后他们的交集就是 lowbit。
110&011=010
如此,我们就求出了lowbit。
计算方法2:
int lowbit(int x) {
return x & -x;
}
原理与上面的方法不尽相同 这个式子运用了计算机的补码计算原理 补码计算简单来讲就是原码的反码反加一 如:
0110(2)=6
变为反码后为 0001
再加一为 0010 即它的补码
可以发现变为反码后 x 与反码数字位每一位都不同, 所以当反码加1后神奇的事情发生了,反码会逢1一直进位直到遇到0,且这个0变成了1,所以这个数最后面出现了一个 100… 串。 由于是反码,进位之后由于1的作用使进位的部分全部取反及与原码相同,所以可以发现 lowbit 以前的部分 x 与其补码即 -x 相反, lowbit x 与 -x 都是1,lowbit 以后 x 与 -x 都是0 所以 x&-x 后除了 lowbit 位是1,其余位都是0。
但lowbit与树状数组有神马关系呢?下面将会为你讲解。
树状数组
树状数组可以解决大部分基于区间上的更新以及求和问题。
树状数组可以解决的问题都可以用线段树解决,但这两者有区别:树状数组的系数要少很多,我们可以这样想:高精算法可以解决高精问题,也可以解决A+B这种简单的问题,但我敢保证没人会用高精算法来做A+B问题(除非他想装逼)。
树状数组像前缀和,但也有不同之处。请观下图:
有一些C[i]只有一部分的数组的和。
而且很有规律。
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]
…
其实,我们可以发现,C[i]是x个相邻元素的和,其中x=lowbit(x)。
豁然开朗了。
树状数组的操作:
1.修改值(update):
我们要把第k个元素增加x
我们就每一次相加了后,寻找这个元素的父亲,也就是加lowbit(k)
void update(int k,int x) {
for(;k<=n;k+=lowbit(k))
BIT[k]+=x;
return ;
}
2.求L~R的值(update):
这里的ask函数是求1~x的值。
那么L~R的值就是ask( r )-ask( l-1 )
这个ask函数感觉就和update函数互逆。
int ask(int x) {
int ans=0;
for(int i=x;i;i-=lowbit(i))
ans+=BIT[i];
return ans;
}
例题:
1.单点修改,区间查询:
你有一个长度为n的数组,你要进行q次操作。
操作有两类:
1. 1 i x:给定i,x,将a[i]加上x。
2. 2 l r:求l~r的和。
样例输入: 样例输出:
3 2 6
1 2 3
1 2 0
2 1 3
n,q小于等于1000000。
这道题就是我们的引入问题:
#include <bits/stdc++.h>
using namespace std;
int n,q,a[1000005];
long long BIT[1000005];
long long lowbit(int x) {
return x & -x;
}
void update(int k,int x) {
for(int i=k;i<=n;i+=lowbit(i))
BIT[i]+=x;
return ;
}
long long ask(int x) {
long long ans=0;
for(int i=x;i;i-=lowbit(i))
ans+=BIT[i];
return ans;
}
int main() {
scanf("%d %d",&n,&q);
for(int i=1;i<=n;i++) {
scanf("%d",&a[i]);
update(i,a[i]);
}
for(int i=1;i<=q;i++) {
int t,l,r;
scanf("%d %d %d",&t,&l,&r);
if(t==1)
update(l,r);
else if(t==2)
printf("%lld\n",ask(r)-ask(l-1));
}
return 0;
}
2.区间修改,单点查询:
你有一个长度为n的数组,你要进行q次操作。
操作有两类:
1. 1 l r x:给定l,r,x,将a[l~r]加上x。
2. 2 i:求a[i]的值。
样例输入: 样例输出:
3 2 2
1 2 3
1 1 3 0
2 2
n,q小于等于1000000。
啊,这题跟上一题非常非常非常像,如果你用上一题的代码改一改就交,啊,祝你WA。
解决方法:
对原数组a建一个差分数组c[i]=a[i]-a[i-1],那么A[i]=P[1]+P[2]+……+P[i]
也就是将差分数组P作为原数组,建立BIT,那么单点查询就是ask(i)了,区间修改就是update(l, x)和update(r+1, -x),这样修改后,BIT求前缀和就是区间修改后的单点查询了。
#include <bits/stdc++.h>
using namespace std;
int n,q,a[1000005];
long long BIT[1000005];
long long lowbit(int x) {
return x & -x;
}
void update(int k,int x) {
for(int i=k;i<=n;i+=lowbit(i))
BIT[i]+=x;
return ;
}
long long ask(int x) {
long long ans=0;
for(int i=x;i;i-=lowbit(i))
ans+=BIT[i];
return ans;
}
int main() {
scanf("%d %d",&n,&q);
for(int i=1;i<=n;i++) {
scanf("%d",&a[i]);
update(i,a[i]-a[i-1]);
}
for(int i=1;i<=q;i++) {
int t;
scanf("%d",&t);
if(t==1) {
int l,r,x;
scanf("%d %d %d",&l,&r,&x);
update(l,x),update(r+1,-x);
}
else if(t==2) {
int l;
scanf("%d",&l);
printf("%lld\n",ask(l));
}
}
return 0;
}
3.区间修改,区间查询:
你有一个长度为n的数组,你要进行q次操作。
操作有两类:
1. 1 l r x:给定l,r,x,将a[l~r]加上x。
2. 2 l r:求l~r的和。
样例输入: 样例输出:
5 10 15
2 6 6 1 1 34
2 1 4 32
1 2 5 10 33
2 1 3 50
2 2 3
1 2 2 8
1 2 3 7
1 4 4 10
2 1 2
1 4 5 6
2 3 4
n,q小于等于1000000。
解决方法:
c仍为a的差分数组,那么原数组的前缀和
a[1]+a[2]+……+ a[n]
=c[1]+(c[1]+c[2])+(c[1]+c[2]+c[3])+……+(c[1]+c[2]+……+c[n])
=n * c[1]+(n-1) * c[2]+(n-2) * c[3]+……+c[n]
=n * (c[1]+c[2]+c[3]+……+c[n])-(0 * c[1]+1 * c[2]+2 * c[3]+……+(n-1) * c[n])
观察减式两边,分别将P[i]和(i-1)p[i]建立两个树状数组BIT1和BIT2,BIT1就是差分数组,区间修改按上一例进行;BIT2的增量就不是x了,而是x*(i-1)。至于区间查询,我们已经知道原数组前缀和了,直接相减即可查询区间和。
#include <bits/stdc++.h>
using namespace std;
long long n,q,a[1000005];
long long BIT1[1000005],BIT2[1000005];
long long lowbit(long long x) {
return x & -x;
}
void update(long long k,long long x) {
for(long long i=k;i<=n;i+=lowbit(i))
BIT1[i]+=x,BIT2[i]+=x*(k-1);
return ;
}
long long ask(long long x) {
long long ans=0;
for(long long i=x;i;i-=lowbit(i))
ans+=BIT1[i]*x-BIT2[i];
return ans;
}
int main() {
scanf("%lld %lld",&n,&q);
for(long long i=1;i<=n;i++) {
scanf("%lld",&a[i]);
update(i,a[i]-a[i-1]);
}
for(long long i=1;i<=q;i++) {
int type;
scanf("%d",&type);
if(type==1) {
long long l,r,x;
scanf("%lld %lld %lld",&l,&r,&x);
update(l,x),update(r+1,-x);
}
else {
long long l,r;
scanf("%lld %lld",&l,&r);
printf("%lld\n",ask(r)-ask(l-1));
}
}
return 0;
}
啊,有一点疯,全开的long long,别介意。
4.求逆序对数:
对于一个包含n个非负整数的数组A[1,…,n],如果有i < j,且A[i]>A[j],则
称(A[i],A[j])为数组A中的一个逆序对。
求数组A中有多少个逆序对
样例输入: 样例输出:
5 4
3 1 4 5 2
n小于等于1000
啊,某些人一看数据,立马打暴力,真的是搞笑。
有些人说,使用归并排序。
但我们要讲的是树状数组。如果你对归并排序感兴趣,可以看一下 排序算法总结里有讲。
我们先了解一下离散化。
离散化
离散化是程序设计中一个常用的技巧,它可以有效的降低空间复杂度。
有些数据本身很大,自身无法作为数组的下标保存对应的属性。如果这时只是需要这堆数据的相对属性,那么可以对其进行离散化处理。当数据只与它们之间的相对大小有关,而与具体是多少无关时,可以进行离散化。比如当你数据个数n很小,数据范围却很大时(超过1e9)就考虑离散化更小的值,能够实现更多的算法。
离散化有两种方法:
1.数组离散化:
for(int i = 1; i <= n; i ++) {
cin >> a[i].val;
a[i].id = i;
}
sort(a + 1, a + n + 1); //定义结构体时按val从小到大排序
for(int i = 1; i <= n; i ++)
b[a[i].id] = i;
//将a[i]数组映射成更小的值,b[i]就是a[i]对应的rank(顺序)值
2.用STL+二分离散化:
#include<algorithm>
/*
n原数组大小
num原数组中的元素
lsh离散化的数组
cnt离散化后的数组大小
*/
int lsh[MAXN] , cnt , num[MAXN] , n;
for(int i=1; i<=n; i++) {
scanf("%d",&num[i]);
lsh[i] = num[i]; //复制一份原数组
}
sort(lsh+1 , lsh+n+1);
//排序,unique虽有排序功能,但交叉数据排序不支持,所以先排序防止交叉数据
//cnt就是排序去重之后的长度
cnt = unique(lsh+1 , lsh+n+1) - lsh - 1;
//unique返回去重之后最后一位后一位地址 - 数组首地址 - 1
for(int i=1; i<=n; i++)
num[i] = lower_bound(lsh+1 , lsh+cnt+1 , num[i]) - lsh;
求解:
实际上就是统计当前元素的前面有几个比它大的元素的个数,然后把所有元素比它大的元素总数累加就是逆序对总数。
#include <bits/stdc++.h>
using namespace std;
long long n,a[1005],lsh[1005],BIT[1005];
long long lowbit(long long x) {
return x & -x;
}
void update(long long k,long long x) {
for(long long i=k;i<=n;i+=lowbit(i))
BIT[i]+=x;
return ;
}
long long ask(long long x) {
long long ans=0;
for(long long i=x;i;i-=lowbit(i))
ans+=BIT[i];
return ans;
}
int main() {
scanf("%lld",&n);
for(long long i=1;i<=n;i++)
scanf("%lld",&a[i]),lsh[i]=a[i];
sort(lsh+1,lsh+n+1);
unique(lsh+1,lsh+n+1)-lsh-1;
for(long long i=1;i<=n;i++)
a[i]=lower_bound(lsh+1,lsh+n+1,a[i])-lsh;
long long ans=0;
for(long long i=1;i<=n;i++) {
update(a[i],1);
ans+=i-ask(a[i]);
}
printf("%lld",ans);
return 0;
}
5.冒泡排序:
clj 想起当年自己刚学冒泡排序时的经历,不禁思绪万千
当年,clj 的冒泡排序(伪)代码是这样的:
flag=false
while (not flag):
flag=true
for i = 0 to N-2:
if A[i+1] < A[i]:
swap A[i], A[i+1]
flag=false
现在的 clj 想知道冒泡排序究竟有多慢,所以在(伪)代码的第三行下面加入了这么一句:
printf("LJS NB\n");
但是随着需要排序的n个数越来越多,这个程序的速度已经不能满足 clj 的耐心了
他想请你帮忙算出这个程序到底能输出多少行LJS NB。
输入样例: 输出样例:
5 4
1 5 3 8 2
n小于等于2000000
这题经过我们的仔细分析:
逆序对最多的数的逆序对就是答案。
说白了,还是求逆序对。
代码:
#include <bits/stdc++.h>
using namespace std;
long long n,a[2000005],lsh[2000005],BIT[2000005];
long long lowbit(long long x) {
return x & -x;
}
void update(long long k,long long x) {
for(long long i=k;i<=n;i+=lowbit(i))
BIT[i]+=x;
return ;
}
long long ask(long long x) {
long long ans=0;
for(long long i=x;i;i-=lowbit(i))
ans+=BIT[i];
return ans;
}
int main() {
scanf("%lld",&n);
for(long long i=1;i<=n;i++)
scanf("%lld",&a[i]),lsh[i]=a[i];
sort(lsh+1,lsh+n+1);
unique(lsh+1,lsh+n+1)-lsh-1;
for(long long i=1;i<=n;i++)
a[i]=lower_bound(lsh+1,lsh+n+1,a[i])-lsh;
long long ans=0;
for(long long i=1;i<=n;i++) {
update(a[i],1);
ans=max(i-ask(a[i])+1,ans);
}
printf("%lld",ans);
return 0;
}