树状数组
前置知识: lowbit 函数
lowbit函数用于求一个非负整数n在二进制表示下最低位1及其后面的0所构成的数值
Decimal | Binary | Lowbit(Binary) | Lowbit(Decimal) | |
---|---|---|---|---|
1 | 001 | 1 | 1 | |
3 | 011 | 1 | 1 | |
4 | 100 | 100 | 4 | |
8 | 1000 | 1000 | 8 |
对于一个数6,要求其lowbit,可以先将其按位取反再加1,再与原数按位与
[egin{align}
&110 \
&010 (按位取反再加1) \
&010 (按位与: lowbit(6) )
end{align}
]
在计算机中非负整数的取反加1就是这个数的相反数,因此可以O(1)求出lowbit(x)
inline int lowbit(int x){
return x & -x;
}
树状数组
树状数组的基本思想是,每一个结点x都只存储包含x的前lowbit(x)的元素的区间和.
这样构建的数据结构如下图:
前缀和查询
这张图将lowbit相同的结点置于同一层.由图可以看出,当求结点7的前缀和时,需要经历如下步骤
- 求7后(包含7)的前lowbit(7)个元素,即求[7,7]区间和
- 求6后(包含6)的前lowbit(6)个元素,即求[5,6]区间和
- 求4后(包含4)的前lowbit(4)个元素,即求[1,4]区间和
上述求解方法,实际上是将[1,7]区间和转化成[1,4];[5,6];[7]的区间和之
易知,若x为2的幂,则lowbit(x) = x,因此,采用树状数组来建树,对于长度为n的数列,所构建的树状数组的树高为 log(n).
因此可知,查询前缀和操作的最坏复杂度为log(n)
inline int query(int x){
int ans = 0;
while(x >= 1){
ans += c[x];
x = x - lowbit(x);
}
return ans;
}
单点修改
对于树状数组,修改操作是查询操作的逆过程.
对于这个图,可以看出
- 第n层元素的lowbit均为(2^{n-1})
- 第n层元素之间的差值为(2^{n}),刚好为第n+1层元素的lowbit
因此,修改位置x的值,直接影响到的只有上一层的x+lowbit(x).即对于上一层的元素只有一个元素需要修改.
inline void add(int x,int val,int n){
while(x <= n){
c[x] += val;
x = x + lowbit(x);
}
}
同样,单点修改操作的最坏复杂度为log(n)
完整代码
#include <cstdio>
using namespace std;
#define N 500000 + 5
int c[N];
inline int read(){
int x = 0;
int flag = 0;
char c = getchar();
while(c<'0'||c>'9'){
if(c == '-'){
flag = 1;
}
c = getchar();
}
while(c>='0'&&c<='9'){
x = (x<<1)+(x<<3)+(c^48);
c = getchar();
}
if(flag){
return -x;
}else{
return x;
}
}
inline int lowbit(int x){
return x & -x;
}
inline void add(int n,int pos,int val){
while(pos <= n){
c[pos] += val;
pos = pos + lowbit(pos);
}
}
inline int query(int pos){
int ans = 0;
while(pos >= 1){
ans += c[pos];
pos = pos - lowbit(pos);
}
return ans;
}
int main(){
int n,m;
int cmd,x,y;
n = read();
m = read();
for(int i = 1; i <= n; i++){
x = read();
add(n,i,x);
}
while(m--){
cmd = read();
x = read();
y = read();
if(cmd == 1){
add(n,x,y);
}else{
printf("%d
",query(y)-query(x-1));
}
}
return 0;
}
变式
区间修改,单点查询
朴素的树状数组能够做到单点修改,区间查询.本质上,这样的树状数组是利用: 存储前缀,查询差分的思想.
即维护前缀和数组,利用前缀和数组的差分来查询区间和.
换一种思路,我们还可以维护差分数组,利用差分数组的前缀和来查询单点,即
// 原数组 a[1],a[2],a[3],...,a[n]
// 差分数组 b[i] = a[i] - a[i-1] (预处理)
// 差分前缀和 b[1] + ... + b[x] = a[0] + ... + a[x] = a[x]
#include <cstdio>
using namespace std;
#define N 500000 + 5
int a[N];
int c[N];
inline int read(){
int x = 0;
int flag = 0;
char c = getchar();
while(c<'0'||c>'9'){
if(c == '-'){
flag = 1;
}
c = getchar();
}
while(c>='0'&&c<='9'){
x = (x<<1)+(x<<3)+(c^48);
c = getchar();
}
if(flag){
return -x;
}else{
return x;
}
}
inline int lowbit(int x){
return x & -x;
}
inline void add(int n,int pos,int val){
while(pos <= n){
c[pos] += val;
pos = pos + lowbit(pos);
}
}
inline int query(int pos){
int ans = 0;
while(pos >= 1){
ans += c[pos];
pos = pos - lowbit(pos);
}
return ans;
}
int main(){
int n,m;
int cmd,x,y,val;
n = read();
m = read();
for(int i = 1; i <= n; i++){
a[i] = read();
add(n,i,a[i]-a[i-1]);
}
while(m--){
cmd = read();
if(cmd == 1){
x = read();
y = read();
val = read();
add(n,x,val);
add(n,y+1,-val);
}else{
x = read();
printf("%d
",query(x));
}
}
return 0;
}
求逆序对
#include <iostream>
#include <algorithm>
using namespace std;
#define N 100000 + 5
struct node{
int val;
int pos;
node(){};
node(int x,int y){
val = x;
pos = y;
}
};
inline bool comp(const node&a,const node&b){
return a.val <= b.val;
}
int c[N]; // c[i] 实时记录了数值在[1,i]区间的数的总数
int b[N]; // 离散化处理
node arr[N]; // 原数组
inline int lowbit(int x){
return x & -x;
}
inline void add(int n,int pos,int val){ // 单点修改
while (pos <= n) {
c[pos] += val;
pos = pos + lowbit(pos);
}
}
inline int query(int pos){ // 查询c[pos]即查询当前一共出现的数值在[1,pos]的数的总数
int ans = 0;
while(pos >= 1){
ans += c[pos];
pos = pos - lowbit(pos);
}
return ans;
}
int main(){
int n;
cin >> n;
for(int i = 1; i <= n; i++){
cin >> arr[i].val;
arr[i].pos = i;
}
/* 排序,求出每一个元素应该处在的位置 */
sort(arr+1,arr+1+n,comp);
/* 离散化处理,将复杂度由MAXN将至n */
int cnt = 1;
b[arr[1].pos] = 1;
for(int i = 2; i <= n; i++){
if(arr[i].val != arr[i-1].val){
cnt++;
}
b[arr[i].pos] = cnt;
}
int ans = 0;
for(int i = 1; i <= n; i++){ // 第i个加入的数
add(n,b[i],1);
ans += (i - query(b[i])); // 第i个加入的数 - 当前出现的不大于(当前加入的数)的数的总数 = 逆序数
}
cout << ans << endl;
return 0;
}