【高级数据结构】树状数组

一、树状数组的介绍

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的最值,分两种情况讨论。

  1. 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)))。

  1. 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();
	}
}
相关推荐
湖南天硕国产SSD3 分钟前
工业存储可靠性进阶:天硕工业固态硬盘动态温控与寿命优化技术实践
网络·数据库·算法·工业存储·天硕存储·工业固态硬盘
legend050709ComeON3 分钟前
常见面试题-leetcode
数据结构·算法·leetcode
初中就开始混世的大魔王11 分钟前
7 Fast DDS-持久化服务
c++·人工智能·中间件·自动驾驶·信息与通信
Smilecoc13 分钟前
决策树(一):决策树基本原理
算法·决策树·机器学习
weixin_3077791315 分钟前
从工具到协作者:AI在后端研发中的流程重构与组织赋能
人工智能·后端·python·算法·自动化
爱吃生蚝的于勒19 分钟前
QT开发第三章——常用控件
linux·服务器·开发语言·前端·javascript·c++·qt
沉下去,苦磨练!25 分钟前
深度学习神经网络的搭建
人工智能·算法
Lsk_Smion30 分钟前
力扣实训 _ [207].课程表/图论
数据结构·leetcode·图论
孬甭_38 分钟前
深入剖析快速排序:原理、实现与性能优化
数据结构·算法·排序算法
San813_LDD40 分钟前
[数据结构]共享栈与双端队列:算法思想分析及C语言实现
java·开发语言·数据结构