一、树状数组的介绍
1.思维导引
树状数组 ( B i n a r y I n d e x e d T r e e , B I T ) (Binary Indexed Tree,BIT) (BinaryIndexedTree,BIT)是利用数的二进制特征进行检索的一种树状的结构。
如何利用二分的思想高效地求前缀和?
如图 4.7 4.7 4.7所示, 以 A A A = a 1 , =\[a_1, =\[a1, a 2 a_2 a2 , a 3 a_3 a3... a 8 a_8] a8]为例,将二叉树的结构画成树状。
这幅图是树状数组的核心,理解了它,就能明白树状数组的一切操作。

图 4.7 4.7 4.7圆圈中标记有数字的节点,存储的是称为树状数组的 t r e e tree\[\] tree\[\]。
一个节点上的 t r e e tree\[\] tree\[\]的值就是其树下的直连的子节点的和。
例如:
t r e e 1 = a 1 tree1=a_1 tree1=a1
t r e e 2 = t r e e 1 + a 2 tree2=tree1+a_2 tree2=tree1+a2
t r e e 3 = a 3 tree3=a_3 tree3=a3
t r e e 4 = t r e e 2 + t r e e 3 + a 4 tree4=tree2+tree3+a_4 tree4=tree2+tree3+a4
...
t r e e l 8 = t r e e 4 + t r e e 6 + t r e e 7 + a 8 treel8=tree4+tree6+tree7+a_8 treel8=tree4+tree6+tree7+a8
利用 t r e e tree\[\] tree\[\]可以高效地完成以下两个操作。
( 1 ) (1) (1)查询,即求前缀和 s u m sum sum。
例如:
s u m ( 8 ) = t r e e 8 sum(8)=tree8 sum(8)=tree8, s u m ( 7 ) = t r e e 7 + t r e e 6 ] + t r e e 4 sum(7)=tree7+tree6]+tree4 sum(7)=tree7+tree6]+tree4
s u m ( 6 ) = t r e e 6 + t r e e 4 sum(6)=tree6+tree4 sum(6)=tree6+tree4
如图 4.7 4.7 4.7所示,下图中的虚线箭头是计算 s u m ( 7 ) sum(7) sum(7)的过程。
显然,计算复杂度为 O ( l o g 2 n ) O(log2n) O(log2n),这样就达到了快速计算前缀和的目的。
( 2 ) (2) (2)维护。
t r e e tree\[\] tree\[\]本身的维护也是高效的。当元素 a a a发生改变时,能以 O ( l o g 2 n ) O(log_2n) O(log2n)的高效率修改 t r e e tree\[\] tree\[\]的值。
例如,更新了 a 3 a_3 a3, ,那么只需要修改 t r e e 3 , t r e e 4 . t r e e 8 tree3,tree4.tree8 tree3,tree4.tree8,即修改它和它上面的那些节点:父节点(后文指出, x x x 的父节点是 x + l o w b i t ( x ) ) x+lowbit(x)) x+lowbit(x))以及父节点的父节点。
有了方案,剩下的问题是如何快速计算出 t r e e tree\[\] tree\[\]。
观察查询和维护两个操作,发现(读者完全可以自己观察树状数组的原理图,根据二进制的特征得到以下结论):
( 1 ) (1) (1)查询的过程是每次去掉二进制的最后的 1 1 1。例如,求 s u m ( 7 ) = t r e e 7 + t r e e 6 + t r e e 4 sum(7)= tree7+tree6+tree4 sum(7)=tree7+tree6+tree4,
步骤如下:
1 、 7 1、7 1、7的二进制是 111 111 111,去掉最后的 1 1 1,得 110 110 110,即 t r e e 6 tree6 tree6;
2 、 2、 2、去掉 6 6 6的二进制 110 110 110的最后一个1 , , ,得 100 100 100,即 t r e e 4 tree4 tree4;
3 、 3、 3、 4 4 4 的二进制是 100 100 100,去掉 1 1 1之后就没有了。
( 2 ) (2) (2)维护的过程是每次在二进制的最后的 1 1 1上加 1 1 1。
例如,更新了 a 3 a_3 a3 ,需要修改 t r e e 3 、 t r e e 4 , t r e e 8 tree3、tree4,tree8 tree3、tree4,tree8等
步骤如下:
1 、 1、 1、 3 3 3的二进制是 11 11 11,在最后的 1 1 1上加 1 1 1得 100 100 100,即 4 4 4,修改 t r e e 4 tree4 tree4;
2 、 2、 2、 4 4 4的二进制是 100 100 100,在最后的 1 1 1上加 1 1 1,得 1000 1000 1000,即 8 8 8,修改 t r e e 8 tree8 tree8;
3 、 3、 3、继续修改 t r e e 16 tree16 tree16, t r e e 32 tree32 tree32等。
最后,树状数组归结到一个关键问题:如何找到一个数的二进制的最后一个 1 1 1。
2.神奇的 l o w b i t ( x ) lowbit(x) lowbit(x)
l o w b i t ( x ) = x & − x lowbit(x)=x\&-x lowbit(x)=x&−x功能是找到 x x x 的二进制数的最后一个 1 1 1。
其原理是利用了负数的补码表示,补码是原码取反加 1 1 1。
例如, x = 6 = 0000011 0 2 x =6 =00000110_2 x=6=000001102, − x = x 补 = 1111101 0 2 -x =x_补=11111010_2 −x=x补=111110102
那么 l o w b i t ( x ) = x & ( − x ) = 1 0 2 = 2 lowbit(x)=x\&(-x)=10_2=2 lowbit(x)=x&(−x)=102=2。
l o w b i t ( x ) lowbit(x) lowbit(x)的结果如表 4.1 4.1 4.1所示 ( x = 1 − 9 ) (x =1-9) (x=1−9)


令 m = l o w b i t ( x ) , t r e e x m =lowbit(x) , treex m=lowbit(x),treex的值是把 a x a_x ax。和它前面共 m m m 个数相加的结果。如表 4.1 4.1 4.1所示。
例如, l o w b i t ( 6 ) = 2 lowbit(6)=2 lowbit(6)=2,有 t r e e 6 = a 5 + a 6 tree6=a_5+a_6 tree6=a5+a6.
t r e e tree\[\] tree\[\]是通过 l o w b i t ( ) lowbit() lowbit()计算出的树状数组,它能够以二分的复杂度存储一个数列的数据。
具体地, t r e e x treex treex中存储的是区间 x --- l o w b i t ( x ) + 1 , r x---lowbit(x)+1,r x---lowbit(x)+1,r中每个数的和。
表 4.1 4.1 4.1可以画成图 4.8 4.8 4.8。
横条中的黑色部分表示 t r e e x treex treex,它等于横条上元素相加的和。

二、树状数组的基本应用
1.单点修改+区间查询
这是最常规的树状数组模板,代码如下:
cpp
#include<bits/stdc++.h>
using namespace std;
const int maxn =5e5 + 10;
const int maxm = 5e5 + 10;
#define int long long
int n,m;
int tree[maxn]; //树状数组
/* 树状数组 */
inline int lowbit(int x) { //神奇的lowbit函数!
return x & (-x);
}
//单点修改
void add(int i,int x) {
while (i <= n) {
tree[i] += x;
i += lowbit(i);
}
}
//区间查询 返回1-i的和
int query(int i) {
int res = 0;
while (i > 0) {
res += tree[i];
i -= lowbit(i);
}
return res;
}
signed main() {
ios::sync_with_stdio(0);
std::cout.tie(0);
std::cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
add(i, x);
}
while (m--) {
int op, x, y;
cin >> op;
if (op == 1) {
cin >> x >> y;
add(x, y);
}
else {
cin >> x >> y;
cout << query(y) - query(x - 1) << "\n";
}
}
}
2.区间修改+单点查询
我们使用差分的思想来建树,我们知道,差分的前缀和是单个点的值。
因此,我们对 1 − i 1-i 1−i的差分数组求一次前缀和就能得到 a i ai ai的值了,而修改操作就类似于差分的修改,在左右两端修改即可,即在 i i i处 + x +x +x、 i + 1 i+1 i+1处 − x -x −x。
代码如下:
cpp
#include<bits/stdc++.h>
using namespace std;
const int maxn = 5e5 + 10;
const int maxm = 1e5 + 10;
int c[maxn];//树状数组
int a[maxn]; //原数组
int n,m;
inline int lowbit(int x) {
return x & (-x);
}
void add(int i,int x) {
while (i <= n) {
c[i] += x;
i += lowbit(i);
}
}
//查找1-i的区间和
int query(int i) {
int res = 0;
while (i > 0) {
res += c[i];
i -= lowbit(i);
}
return res;
}
void solve() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
add(i,a[i]-a[i-1]); //差分建树
}
while (m--) {
int op, l, r,x;
cin >> op;
if (op == 1) {
cin >> l >> r>>x;
add(l, x);
add(r + 1, -x);
}
else {
cin >>l;
cout << query(l)<< '\n';
}
}
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
//FILE* stream;
//freopen_s(&stream, "stdcin.txt", "r", stdin);
int t = 1;
//cin >> t;
while (t--) {
solve();
}
//fclose(stdin);
}
3.区间修改+区间查询
操作 ( 1 ) (1) (1)是区间修改,操作 ( 2 ) (2) (2)是区间查询。
首先,求区间和 s u m ( L , R ) = a L + a L + 1 + ... + a R = s u m ( 1 , R ) − s u m ( 1 , L − 1 ) sum(L ,R)=aL+aL+1+...+aR=sum(1,R )-sum(1,L-1) sum(L,R)=aL+aL+1+...+aR=sum(1,R)−sum(1,L−1),问题转换为求 s u m ( 1 , k ) sum(1,k) sum(1,k)。
定义一个差分数组 D D D,它和原数组α的关系仍然是
D k = a k − a k − 1 Dk=ak-ak-1 Dk=ak−ak−1 ,
a k = D 1 + D 2 + ... + D k ak=D1+D2+...+Dk ak=D1+D2+...+Dk。
下面推导区间和,看它与求前缀和有没有关系,如果有关系,就能用树状数组。
a 1 + a 2 + ... + a k a_1+a_2+...+a_k a1+a2+...+ak
= D 1 + ( D 1 + D 2 ) + ( D 1 + D 2 + D 3 ) + ... + ( D 1 + D 2 + ... + D k ) =D_1+(D_1+D_2)+(D_1+D_2+D_3)+...+(D_1+D_2+...+D_k) =D1+(D1+D2)+(D1+D2+D3)+...+(D1+D2+...+Dk)
= k D 1 + ( k − 1 ) D 2 + ( k − 2 ) D 3 , + ... + ( k − ( k − 1 ) ) D k , =kD_1+(k-1)D_2+(k-2)D_3,+...+(k-(k-1))D_k, =kD1+(k−1)D2+(k−2)D3,+...+(k−(k−1))Dk,
= k ( D 1 + D 2 + ... + D k ) − ( D 2 + 2 D 3 + ... + ( k − 1 ) D k ) =k(D_1+D_2+...+D_k)-(D_2+2D_3+...+(k-1)D_k) =k(D1+D2+...+Dk)−(D2+2D3+...+(k−1)Dk)
= k ∑ i = 1 k D i − ∑ i = 1 k ( i − 1 ) D i =k\sum_{i=1}^{k} D_i-\sum_{i=1}^{k}(i -1)D_i =k∑i=1kDi−∑i=1k(i−1)Di
公式最后一行求两个前缀和,用两个树状数组分别处理,一个实现 D i D_i Di,另一个实现 ( i − 1 ) D i (i-1)D_i (i−1)Di。
代码如下:
cpp
#include<bits/stdc++.h>
using namespace std;
const int maxn = 5e5 + 10;
const int maxm = 5e5 + 10;
#define int long long
int n, m;
int tree1[maxn]; //差分树状数组
int tree2[maxn]; //(i-1)*tree1[i]树状数组
int a[maxn]; //原数组
/* 树状数组 */
inline int lowbit(int x) { //神奇的lowbit函数!
return x & (-x);
}
/* 维护两个树状数组 */
//区间修改1
void add1(int i, int x) {
while (i <= n) {
tree1[i] += x;
i += lowbit(i);
}
}
//区间修改2
void add2(int i, int x) {
while (i <= n) {
tree2[i] += x;
i += lowbit(i);
}
}
//区间查询1
int query1(int i) {
int res = 0;
while (i > 0) {
res += tree1[i];
i -= lowbit(i);
}
return res;
}
//区间查询2
int query2(int i) {
int res = 0;
while (i > 0) {
res += tree2[i];
i -= lowbit(i);
}
return res;
}
signed main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
add1(i, a[i] - a[i - 1]); //差分建树
add2(i, (i - 1) * (a[i] - a[i - 1]));//(i-1)*tree1[i]建树
}
while (m--) {
int op, x, y, k;
cin >> op;
if (op == 1) {
cin >> x >> y >> k; //修改区间
add1(x, k);
add1(y + 1, -k);
add2(x, (x - 1) * k);
add2(y + 1, y * (-k));
}
else {
cin >> x >> y;
cout << y * query1(y) - (x - 1) * query1(x - 1) - (query2(y) - query2(x - 1)) << "\n"; //区间查询
}
}
}
三、树状数组的扩展应用
1.树状数组求二维偏序(逆序对)
一种方法是通过归并排序来实现,也可以使用树状数组来实现,下面的代码实现了这两种方法:
cpp
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int maxn = 5e5 + 10;
const int maxm = 5e5 + 10;
#define pii pair<int,int>
#define int long long
int n;
int tree[maxn];
inline int lowbit(int x) {
return x & (-x);
}
void add(int i,int x) {
while (i <= n) {
tree[i] += x;
i += lowbit(i);
}
}
int query(int i) {
int res = 0;
while (i > 0) {
res += tree[i];
i -= lowbit(i);
}
return res;
}
void solve1() { //逆序对第一种解法-树状数组
cin >> n;
vector<pii>a(n + 1);
for (int i = 1; i <= n; ++i) {
cin >> a[i].first;
a[i].second = i;
}
//离散化
sort(a.begin() + 1, a.end());
vector<int>b(n + 1);
for (int i = 1; i <= n; ++i) {
b[a[i].second] = i;
}
int res = 0;
for (int i = 1; i <= n; ++i) {
add(b[i], 1);
res +=query(n) - query(b[i]);
}
cout << res << endl;
}
int b[maxn]; //辅助数组
int a[maxn]; //原序列
int res = 0; //逆序对的个数
void merge(int l,int mid,int r) { //排序
int i = l, j = mid + 1 ;
int k = 0;
while (i <= mid && j <= r) {
if (a[i] > a[j]) {
b[++k] = a[j++];
res += mid - i + 1; //记录逆序对个数
}
else {
b[++k] = a[i++];
}
}
while (i <= mid) b[++k] = a[i++];
while (j <= r) b[++k] = a[j++];
for (int i = 1; i <= k; ++i) { //把辅助数组拷贝到原数组
a[l + i - 1] = b[i];
}
}
void mergesort(int l,int r) { //划分
if (l >= r) return; //边界
int mid = l + r >> 1;
mergesort(l, mid);
mergesort(mid + 1, r);
merge(l, mid, r);
}
void solve2() { //第二种解法-归并排序
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
mergesort(1, n);
cout << res << endl;
}
signed main() {
//solve1();
solve2();
return 0;
}
2.树状数组求区间最值
用暴力法求解,复杂度为 O ( M N ) O(MN) O(MN)。
下面用树状数组求解。
在标准前缀和树状数组中, t r e e x treex treex中存储的是 x − l o w b i t ( x ) + 1 , x x-lowbit(x )+1,x x−lowbit(x)+1,x中每个数的和。
在求最值的树状数组中, t r e e x treex treex记录 x 一 l o w b i t ( x ) + 1 , x x一lowbit(x)+1,x x一lowbit(x)+1,x内所有数的最大值。
阅读下面内容时,请对照树状数组原理图,如图 4.11 4.11 4.11所示。

(1)单点修改:
u p d a t e ( x , v a l u e ) update( x, value) update(x,value)。用 v a l u e value value更新 t r e e z treez treez的最大值,并更新树状数组上被它影响的节点。
例如,修改 x = 4 x=4 x=4,步骤如下:
修改 x x x子树上直连的 t r e e 2 , t r e e 3 tree2,tree3 tree2,tree3。
修改 x x x 的父节点 t r e e 8 tree8 tree8,以及 t r e e 8 tree8 tree8的直连子节点 t r e e 6 tree6 tree6, t r e e 7 tree7 tree7等。
每步复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n),共 O ( l o g 2 n ) O(log_2n) O(log2n)步,总复杂度为 O ( ( l o g 2 n ) 2 ) O((log_2n)^ 2) O((log2n)2)。
注意一个特殊情况,初始化时需要修改所有 n n n个数,复杂度为 O ( n ( l o g 2 n ) 2 ) O(n(log_2n)^2) O(n(log2n)2) ,符合要求。
( 2 ) (2) (2)区间最值查询: q u e r y ( ) query() query()。
关于区间 L , R L,R L,R的最值,分两种情况讨论。
- R − L ≥ l o w b i t ( R ) 1.R-L \geq lowbit(R) 1.R−L≥lowbit(R)
对照图 4.11 4.11 4.11,即 L , R L,R L,R区间包含了 t r e e R treeR treeR直连子节点的个数,此时直接使用 t r e e R treeR treeR值: q u e r y ( L , R ) = m a x ( t r e e R , q u e r y ( L , R − l o w b i t ( R ) ) ) query(L,R)=max(treeR,query(L,R-lowbit(R))) query(L,R)=max(treeR,query(L,R−lowbit(R)))。
- R − L ≤ l o w b i t ( R ) 2.R-L\leq lowbit(R) 2.R−L≤lowbit(R)
上述包含关系不成立,先使用 a R aR aR的值,然后向前递推: q u e r y ( L , R ) = m a x ( a R , q u e r y ( L , R − 1 ) ) query(L,R)=max(aR,query(L,R-1)) query(L,R)=max(aR,query(L,R−1))。
q u e r y ( ) query() query()的复杂度仍然为 O ( ( l o g 2 n ) 2 ) O((log_2n)^2) O((log2n)2)。
思路
有很多种求区间最值的方法,这里选择使用树状数组来求区间最值,时间复杂度较差,单次修改和查询都为 O ( ( l o g 2 n ) 2 ) O((log_2n)^2) O((log2n)2),能通过此题。
代码
cpp
const int N = 1e5 + 10;
int a[N];
int tree[N];
int n, m;
void update(int x, int val) {
for (int i = x; i <= n; i += lowbit(i)) {
tree[i] = val;
for (int j = 1; j < lowbit(x); j <<= 1) {
tree[i] = std::max(tree[i], tree[i - j]);
}
}
}
int query(int L, int R) {
int res = -INF;
while (L <= R) {
res = std::max(res, a[R]);
R--;
while (R - L >= lowbit(R)) {
res = std::max(res, tree[R]);
R -= lowbit(R);
}
}
return res;
}
void solve() {
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
update(i, a[i]);
}
while (m--) {
int l, r;
cin >> l >> r;
std::cout << query(l, r) << "\n";
}
}
//main 函数
signed main() {
std::ios::sync_with_stdio(0);
std::cout.tie(0);
std::cin.tie(0);
int t = 1;
//cin >> t;
while (t--) {
solve();
}
return 0;
}
3.二维区间修改+查询
二维区间修改+查询和一维区间修改+查询类似,都是建立在差分的思想上,二维需要有四个树状数组来维护,具体的推导过程如下:
( 1 ) (1) (1)二维区间修改
如何实现区间修改?需要结合二维差分数组。定义一个二维差分数组 D i j Dij Dij,它与矩阵元素 a c d acd acd的关系为
D i j = a i j − a i − 1 j − a i j − 1 + a i − 1 j − 1 Dij=aij-ai -1j-aij -1+ai-1j-1 Dij=aij−ai−1j−aij−1+ai−1j−1
对照图 4.9 4.9 4.9, D i j Dij Dij就是阴影面积。
a c d = ∑ i = 1 c ∑ j = 1 d acd=\sum_{i=1}^{c} \sum_{j=1}^{d} acd=∑i=1c∑j=1d,可看作对以 ( 1 , 1 ) (1,1) (1,1)和 ( c , d ) (c ,d) (c,d)为顶点的矩阵内的 D i j Dij Dij求和。
它们同样满足"差分是前缀和的逆运算"。
进行区间修改时,在 u p d a t e ( ) update() update()函数中,每次第 i i i行减少 l o w b i t ( i ) lowbit(i) lowbit(i),第 j j j列减少 l o w b i t ( j ) lowbit(j) lowbit(j) ,复杂度为 O ( l o g 2 n l o g 2 m ) O(log_2nlog_2m) O(log2nlog2m)。
( 2 ) (2) (2)二维区间查询
查询以 ( a , b ) (a ,b) (a,b)和 ( c , d ) (c ,d) (c,d)为顶点的矩阵区间和,
对照图 4.10 4.10 4.10的阴影部分,有
∑ i = a c ∑ j = b d a i j = ∑ i = 1 c ∑ j = 1 d a i j − ∑ i = 1 c ∑ j = 1 b − 1 a i j − ∑ i = 1 a − 1 ∑ j = 1 d a i j + ∑ i = 1 a − 1 ∑ j = 1 b − 1 a i j \sum_{i=a}^{c} \sum_{j=b}^{d} aij=\sum_{i=1}^{c} \sum_{j=1}^{d} aij-\sum_{i=1}^{c} \sum_{j=1}^{b-1} aij-\sum_{i=1}^{a-1} \sum_{j=1}^{d} aij+\sum_{i=1}^{a-1} \sum_{j=1}^{b-1} aij i=a∑cj=b∑daij=i=1∑cj=1∑daij−i=1∑cj=1∑b−1aij−i=1∑a−1j=1∑daij+i=1∑a−1j=1∑b−1aij
问题转换为计算 ∑ i = 1 n ∑ j = 1 m \sum_{i=1}^{n}\sum_{j=1}^{m} ∑i=1n∑j=1m,根据它与差分数组 D D D的关系进行变换,有:
∑ i = 1 n ∑ j = 1 m a i j = ∑ i = 1 n ∑ j = 1 m ∑ k = 1 i ∑ l = 1 j D k l = ∑ i = 1 n ∑ j = 1 m D i j × ( n − i + 1 ) × ( m − j + 1 ) = ( n + 1 ) ( m + 1 ) ∑ i = 1 n ∑ j = 1 m D i j − ( m + 1 ) ∑ i = 1 n ∑ j = 1 m D i j × i − ( n + 1 ) ∑ i = 1 n ∑ j = 1 m D i j × j + ∑ i = 1 n ∑ j = 1 m D i j × i × j \begin{aligned} & \sum_{i=1}^{n} \sum_{j=1}^{m} aij \\ = & \sum_{i=1}^{n} \sum_{j=1}^{m} \sum_{k=1}^{i} \sum_{l=1}^{j} Dkl \\ = & \sum_{i=1}^{n} \sum_{j=1}^{m} Dij \times(n-i+1) \times(m-j+1) \\ = & (n+1)(m+1) \sum_{i=1}^{n} \sum_{j=1}^{m} Dij-(m+1) \sum_{i=1}^{n} \sum_{j=1}^{m} Dij \times i- \\ & (n+1) \sum_{i=1}^{n} \sum_{j=1}^{m} Dij \times j+\sum_{i=1}^{n} \sum_{j=1}^{m} Dij \times i \times j \end{aligned} ===i=1∑nj=1∑maiji=1∑nj=1∑mk=1∑il=1∑jDkli=1∑nj=1∑mDij×(n−i+1)×(m−j+1)(n+1)(m+1)i=1∑nj=1∑mDij−(m+1)i=1∑nj=1∑mDij×i−(n+1)i=1∑nj=1∑mDij×j+i=1∑nj=1∑mDij×i×j
这是 4 4 4个二维树状数组。
代码如下:
思路
使用二维线段树也可以实现,使用二维树状数组比较代码量比较小一点,并且空间复杂度也小一点。
代码
cpp
const int N = 2100 + 10;
int tree1[N][N], tree2[N][N], tree3[N][N], tree4[N][N];
char ch;
int n, m;
void update(int x, int y, int val) {
for (int i = x; i <= n; i += lowbit(i)) {
for (int j = y; j <= m; j += lowbit(j)) {
tree1[i][j] += val;
tree2[i][j] += x * val;
tree3[i][j] += y * val;
tree4[i][j] += x * y * val;
}
}
}
int query(int x, int y) {
int res = 0;
for (int i = x; i > 0; i -= lowbit(i)) {
for (int j = y; j > 0; j -= lowbit(j)) {
res += (x + 1) * (y + 1) * tree1[i][j] - (y + 1) * tree2[i][j] - (x + 1) * tree3[i][j] + tree4[i][j];
}
}
return res;
}
void solve() {
cin >> ch >> n >> m;
int a, b, c, d;
while (cin >> ch >> a >> b >> c >> d) {
if (ch == 'L') {
int val;
cin >> val;
update(a, b, val);
update(a, d+1, -val);
update(c+1, b, -val);
update(c+1, d+1, val);
}
else {
std::cout << query(c, d) + query(a - 1, b - 1) - query(a - 1, d) - query(c, b - 1) << "\n";
}
}
}
//main 函数
signed main() {
std::ios::sync_with_stdio(0);
std::cout.tie(0);
std::cin.tie(0);
int t = 1;
//cin >> t;
while (t--) {
solve();
}
return 0;
}
4.离线算法
这题一种方法是在线通过主席树来做,另一种方法是离线排序后使用树状数组,下面两种方法都实现了:
解法一(主席树)
思路
首先我们需要查询 L , R L,R L,R中不同种类数字的个数。
我们可以在主席树的每个位置存入每个数下一次出现的位置(如果不出现,则存入 n + 1 n+1 n+1)。
这样做有什么用呢?
考虑一个区间 1 , 2 , 3 , 3 , 4 , 2 , 5 1,2,3,3,4,2,5 1,2,3,3,4,2,5,假设我们要查询区间 2 , 3 , 3 2,3,3 2,3,3,而我们在主席树中存放的位置为 8 , 4 , 8 8,4,8 8,4,8,可以发现,我们只需要在主席树上查找区间内大于 R R R的数有多少个,而对于这样的查询,主席树显然是很容易做到的。
代码
cpp
const int N = 1e6 + 10;
int root[N];
int tot = 0;
struct node {
int l, r, cnt;
}tree[N * 40];
#define ls(x) tree[x].l
#define rs(x) tree[x].r
int n;
void build(int l, int r, int& u) {
u = ++tot;
if (l == r) return;
int mid = l + r >> 1;
build(l, mid, ls(u));
build(mid + 1, r, rs(u));
}
void insert(int u, int& v, int l, int r, int val) {
v = ++tot;
tree[v] = tree[u];
tree[v].cnt++;
if (l == r) return;
int mid = l + r >> 1;
if (val <= mid) {
insert(ls(u), ls(v), l, mid, val);
}
else {
insert(rs(u), rs(v), mid + 1, r, val);
}
}
int query(int u, int v, int l, int r, int k) {
if (l == r) return tree[v].cnt - tree[u].cnt;
int res = 0;
int s = tree[rs(v)].cnt - tree[rs(u)].cnt;
int mid = l + r >> 1;
if (k <= mid) {
res += s;
res += query(ls(u), ls(v), l, mid, k);
}
else res += query(rs(u), rs(v), mid+1, r, k);
return res;
}
int a[N], head[N], nxt[N];
void solve() {
cin >> n;
build(1, n+1, root[0]);
memset(head, -1, sizeof(head));
for (int i = 1; i <= n; ++i) {
cin >> a[i];
if (head[a[i]] == -1) head[a[i]] = i;
else {
int t = head[a[i]];
head[a[i]] = i;
nxt[t] = i;
}
}
for (int i = 1; i <= n; ++i) {
if (!nxt[i]) nxt[i] = n + 1;
insert(root[i - 1], root[i], 1, n + 1, nxt[i]);
}
int q;
cin >> q;
while (q--) {
int l, r;
cin >> l >> r;
int res = query(root[l - 1], root[r], 1, n + 1, r + 1);
cout << res << "\n";
}
}
signed main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(0);
int t = 1;
//cin >> t;
while (t--) {
solve();
}
}
解法二(离线+树状数组)
思路
首先我们把序列存起来,按照右端点从小到大排序。
为什么呢?
考虑一段区间 1 , 2 , 3 , 3 , 4 , 2 , 5 1,2,3,3,4,2,5 1,2,3,3,4,2,5,我们依次从左到右存入相应位置上(下标 i i i)的 1 1 1,在序列 1 , 2 , 3 1,2,3 1,2,3中,树状数组中的序列为: 1 , 1 , 1 1,1,1 1,1,1。
如果我们发现出现了重复的数字,我们对 l a s t a \[ i ] lasta\[i] lasta\[i]的位置贡献 − 1 -1 −1,然后在当前位置加上贡献,此时序列为: 1 , 1 , 0 , 1 1,1,0,1 1,1,0,1。
可以发现这样的操作类似于差分序列,但是这样的修改会影响到右端点为 3 3 3的查询。
但是我们开头说了:
首先我们把序列存起来,按照右端点从小到大排序
因此我们修改后,就不再存在有右端点小于当前位置 i i i的查询了,因此这样的修改是有效的。
所以我们要维护的就是每个数上一次出现的位置了。
代码
cpp
const int N = 1e6 + 10;
int n;
int tree[N];
void add(int i, int val) {
while (i <= n) {
tree[i] += val;
i += lowbit(i);
}
}
int query(int i) {
int res = 0;
while (i > 0) {
res += tree[i];
i -= lowbit(i);
}
return res;
}
int a[N];
struct node {
int l, r, id;
bool operator<(node& x) {
if (r != x.r) return r < x.r;
return l < x.l;
}
}q[N];
int res[N], pre[N];
void solve() {
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
int Q;
cin >> Q;
for (int i = 1; i <= Q; ++i) {
int l, r;
cin >> l >> r;
q[i] = { l,r,i };
}
sort(q + 1, q + 1 + Q);
int now = 1;
for (int i = 1; i <= Q; ++i) {
int l = q[i].l, r = q[i].r;
for (int j = now; j <= r; ++j) {
if (!pre[a[j]]) {
pre[a[j]] = j;
add(j, 1);
}
else {
add(pre[a[j]], -1), add(j, 1);
pre[a[j]] = j;
}
}
now = r + 1;
int s = query(r) - query(l - 1);
res[q[i].id] = s;
}
for (int i = 1; i <= Q; ++i) cout << res[i] << "\n";
}
signed main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(0);
int t = 1;
//cin >> t;
while (t--) {
solve();
}
}