1 树状数组
1.1 引例导入
有
n
n
n个数据
a
1
,
a
2
⋯
,
a
n
(
1
≤
n
≤
1
0
6
)
a_1, a_2 \cdots, a_n(1 \le n \le 10^6)
a1,a2⋯,an(1≤n≤106), 有
m
m
m个操作
(
1
≤
m
≤
1
0
6
)
(1 \le m \le 10^6)
(1≤m≤106), 每个操作为以下两种之一
1 x
表示查询
∑
x
i
=
1
a
[
i
]
\sum^{i=1}_x a[i]
∑xi=1a[i]
2 x y
表示把a[x]修改为y
用普通数组做修改为
O
(
1
)
O(1)
O(1), 查询为
O
(
n
)
O(n)
O(n)
用前缀和做修改为
O
(
n
)
O(n)
O(n), 查询为
O
(
1
)
O(1)
O(1)
用暴力或前缀和做都会超时, 我们就要用到树状数组
1.2 简介
树状数组可应用于单点修改, 区间求和
单次修改和查询时间复杂度都为
O
(
log
n
)
O(\log n)
O(logn), 空间复杂度为
O
(
n
)
O(n)
O(n)
2 使用方法
2.1构建
假如我们把
a
a
a数组看成是完全二叉树的叶子, 构建出树, 在把所有节点向右靠, 就得到以下图形
有人可能会奇怪, 为什么这棵树红色的节点不用来存东西
可以通过每一位的二进制看出
c
i
c_i
ci的层数取决于
i
i
i二进制末尾有多少
0
0
0, 而
0
0
0的数量可以用最后的一位
1
1
1算出, 我们就需要找出最后的
1
1
1
int lowbit(int x){
return x & (-x);
}
正确性证明: 设
x
x
x的末
k
−
1
k-1
k−1位是
0
0
0, 第
k
k
k位是
1
1
1
转成负数的反码后, 第
k
k
k位是
0
0
0, 末
k
−
1
k-1
k−1都是
1
1
1
+
1
+1
+1变成补码后, 第
k
k
k位是
1
1
1, 其他位正好与
x
x
x取反, 证明完毕
c
x
c_x
cx要表示的数是
∑
i
=
x
−
l
o
w
b
i
t
(
x
)
+
1
x
a
i
\sum_{i=x-lowbit(x)+1}^x{a_i}
∑i=x−lowbit(x)+1xai, 也就是从
c
x
c_x
cx向前数
l
o
w
b
i
t
(
x
)
lowbit(x)
lowbit(x)个数
2.2 修改
从图中可以找出以下规律: i + l o w b i t ( i ) i + lowbit(i) i+lowbit(i)正好是 i i i的父节点, 所以我们修改 a i a_i ai, 所有 i i i的祖先都要进行修改, 就可以顺着 i i i一直往上找到根节点进行修改
void update(int x, int val){ //x是节点编号, val是要修改的值
for(;x <= n;x += lowbit(x))
c[x] += val;
}
2.3 查询
我们要进行查询 ∑ i = 1 7 a i \sum_{i=1}^7{a_i} ∑i=17ai的值, 发现 s u m 7 = c 7 + c 6 + c 4 sum_7 = c_7 + c_6 + c_4 sum7=c7+c6+c4, 而 7 6 4 7\;6\;4 764这三个数的二进制为 111 110 100 111\;\;110\;\;100 111110100, 7 − l o w b i t ( 7 ) = 6 7 - lowbit(7) = 6 7−lowbit(7)=6, 6 − l o w b i t ( 6 ) = 4 6 - lowbit(6) = 4 6−lowbit(6)=4, 所以只需要每次减去 l o w b i t ( i ) lowbit(i) lowbit(i)求和, 就可以得出结果
int query(int x){ //查询a[1]到a[x]的和
int res = 0;
for(;x;x -= lowbit(x))
res += c[x];
return res;
}
3 例题
3.1 逆序对
3.1.1 题目思路
求逆序对, 也就是求前面的数比后面的数大, 可以每输入一个数 x x x就查询比 x x x小的数累加到答案, 1 ≤ a i ≤ 1 0 9 1 \le a_i \le 10^9 1≤ai≤109, 要用到离散化
3.1.2 AC代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <map>
using namespace std;
const int N = le6;
long long a[N],b[N],c[N],ans;
int n, x;
map<int, int> mp;
int lowbit(int);
void update(int, int);
long long query(int);
int lowbit(int x){
return x & (-x);
}
void update(int x){
//把a[x]增加1
while(x <=n){
c[x]++;
x += 1owbit(x);
}
long long query(int x){
// 求a[1] + a[2] + ... + a[x]
long long res = 0;
while(x >0){
res += c[x];
x -= 1owbit(x);
}
return res;
}
int main(){
scanf("%d", &n);
for(int i = 1;i <= n;i++){
scanf("%11d", &a[i]);
b[i] = a[i];
}
sort(b+1, b+n+1);
for(int i = 1;i <= n;i++){
mp[b[i]] = i;
}
for(int i = 1;i <= n;i++){
x = mp[a[i]];
update(x);
ans += i - query(x);
}
printf("%11d\n", ans);
return 0;
}
3.2 BIT-2
3.2.1 题目大意
给定有
n
n
n个元素的数组
a
a
a, 进行
q
q
q次操作, 有两种操作:
1 l r k
: 将
a
l
a_l
al到
a
r
a_r
ar每个数
+
k
+k
+k
2 k
: 输出
a
k
a_k
ak
3.2.2 题目思路
用差分思想, c c c表示差分数组, u p d a t e update update时把 c l c_l cl到 c r c_r cr加 k k k, q u e r y query query时计算 c k c_k ck的前缀和
3.2.3 AC代码
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
const int N = 1e6 + 5;
int n, q;
ll c[N];
int lowbit(int x){
return x & (-x);
}
void update(int l, int r, int k){
for(;l <= n;l += lowbit(l))
c[l] += k;
for(r++;r <= n;r += lowbit(r))
c[r] -= k;
}
ll query(int k){
ll res = 0;
for(;k != 0;k -= lowbit(k))
res += c[k];
return res;
}
int main(){
scanf("%d %d", &n, &q);
int t;
for(int i = 1;i <= n;i++){
scanf("%d", &t);
update(i, i, t);
}
int op, l, r, k;
while(q--){
scanf("%d", &op);
if(op == 1){
scanf("%d %d %d", &l, &r, &k);
update(l, r, k);
}
else if(op == 2){
scanf("%d", &k);
if(k == 0) printf("0\n");
else printf("%lld\n", query(k));
}
}
return 0;
}
3.3 BIT-3
3.3.1 题目大意
给定有
n
n
n个元素的数组
a
a
a, 进行
q
q
q次操作, 有两种操作:
1 l r k
: 将
a
l
a_l
al到
a
r
a_r
ar每个数
+
k
+k
+k
2 l r
: 输出
a
l
a_l
al到
a
r
a_r
ar的和
3.3.2 题目思路
∑
i
=
l
r
a
i
=
∑
i
=
l
r
∑
j
=
1
i
c
i
\sum_{i=l}^r{a_i} = \sum_{i=l}^r \sum_{j=1}^i{c_i}
∑i=lrai=∑i=lr∑j=1ici
∵
a
1
=
c
1
,
\because a_1 = c_1,
∵a1=c1,
a
2
=
c
1
+
c
2
,
a_2 = c_1 + c_2,
a2=c1+c2,
a
3
=
c
1
+
c
2
+
c
3
,
a_3 = c_1 + c_2 + c_3,
a3=c1+c2+c3,
a
4
=
c
1
+
c
2
+
c
3
+
c
4
,
a_4 = c_1 + c_2 + c_3 + c_4,
a4=c1+c2+c3+c4,
⋯
⋯
\cdots~\cdots
⋯ ⋯
∴
∑
i
=
l
r
∑
j
=
1
i
c
i
=
∑
i
=
l
r
c
i
∗
(
l
−
i
+
1
)
=
(
l
+
1
)
∗
∑
i
=
l
r
c
i
−
∑
i
=
l
r
c
i
∗
i
\therefore \sum_{i=l}^r \sum_{j=1}^i{c_i} = \sum_{i=l}^r{c_i * (l-i+1)} = (l + 1) * \sum_{i=l}^r{c_i} - \sum_{i=l}^r{c_i * i}
∴∑i=lr∑j=1ici=∑i=lrci∗(l−i+1)=(l+1)∗∑i=lrci−∑i=lrci∗i
所以我们只需要维护两个数组
c
i
c_i
ci和
s
u
m
i
=
c
i
∗
i
sum_i = c_i * i
sumi=ci∗i
3.3.3 AC代码
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll; //等效于 define ll long long
const int N = 1e6 + 5;
int n, q;
ll c[N], sum[N];
int lowbit(int x){
return x & (-x);
}
void update(int l, int r, int x){
for(int i = l;i <= n;i += lowbit(i)){
c[i] += x;
sum[i] += x * l;
}
for(int i = r + 1;i <= n;i += lowbit(i)){
c[i] -= x;
sum[i] -= x * (r+1);
}
}
ll query(int l, int r){
ll res1 = 0, res2 = 0;
for(int i = l;i != 0;i -= lowbit(i)){
res1 += c[i] * l;
res2 += sum[i];
}
for(int i = r + 1;i != 0;i -= lowbit(i)){
res1 += c[i] * (r + 1);
res2 += sum[i];
}
return res1 - res2;
}
int main(){
scanf("%d %d", &n, &q);
int t;
for(int i = 1;i <= n;i++){
scanf("%d", &t);
update(i, i, t);
}
int op, l, r, k;
while(q--){
scanf("%d", &op);
if(op == 1){
scanf("%d %d %d", &l, &r, &k);
update(l, r, k);
}
else if(op == 2){
scanf("%d %d", &l, &r);
printf("%lld\n", query(l, r));
}
}
return 0;
}
3.4 其他例题
火柴排队
和逆序对差别不大
虔诚的墓主人
比较难了
感谢观看
此篇文章为可达鸭Y1第5和第6节课介绍