🚀个人主页:为梦而生~ 关注我一起学习吧!
💡专栏:算法题、 基础算法、数据结构~赶紧来学算法吧
💡往期推荐:
【算法基础 & 数学】快速幂求逆元(逆元、扩展欧几里得定理、小费马定理)
【算法基础】深搜
数据结构各内部排序算法总结对比及动图演示(插入排序、冒泡和快速排序、选择排序、堆排序、归并排序和基数排序等)
【算法 & 高级数据结构】树状数组:一种高效的数据结构(一)
上一篇文章我们介绍了树状数组这个数据结构,并且进行了其原理的数学推导,这篇文章基于上一篇文章,来讲一下这个数据结构在算法题中的应用
还不知道树状数组是什么的来看这篇文章:【算法 & 高级数据结构】树状数组:一种高效的数据结构(一)
题目来源:AcWing
文章目录
- 0 通用模板
- 1 单点修改,区间求和
- 2 区间增减,单点查询
- 3 区间增减,区间求和
0 通用模板
首先,通用模板先摆上
//lowbit
int lowbit(int x){
return x & -x;
}
//修改操作
void add(int x, int c){
for(int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
//查询操作
int sum(int x){
int res = 0;
for(int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
1 单点修改,区间求和
- 例题:
输入格式
第一行一个数
n
n
n。
第二行是
n
n
n
个数,分别代表
y
1
,
y
2
,
…
,
y
n
y_1,y_2,…,y_n
y1,y2,…,yn。
输出格式
两个数,中间用空格隔开,依次为 V
的个数和 ∧
的个数。
数据范围
对于所有数据,
n
≤
200000
n≤200000
n≤200000,且输出答案不会超过 int64。
y
1
∼
y
n
y_1∼y_n
y1∼yn 是
1
1
1到
n
n
n 的一个排列。
输入样例:
5
1 5 3 2 4
输出样例:
3 4
- 主要思路:
因为两个符号的特点对应的每个点的纵坐标有这样的特点: V
意味着两边点的纵坐标高于当前点的纵坐标, ∧
意味着两边的纵坐标小于当前的纵坐标。所以我们只需要遍历两次数组,记录每个点左右高于或低于当前点的纵坐标的点的数量,然后利用排列组合,计算出总数。
以下题解来源:https://www.acwing.com/solution/content/13818/
- 从左向右依次遍历每个数a[i],使用树状数组统计在i位置之前所有比a[i]大的数的个数、以及比a[i]小的数的个数。
统计完成后,将a[i]加入到树状数组。- 从右向左依次遍历每个数a[i],使用树状数组统计在i位置之后所有比a[i]大的数的个数、以及比a[i]小的数的个数。
统计完成后,将a[i]加入到树状数组。
- 代码:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long LL;
const int N = 200010;
int n;
int a[N], tr[N];
int Greater[N], Lower[N];
int lowbit(int x){
return x & -x;
}
//修改操作
void add(int x, int c){
for(int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
//查询操作
int sum(int x){
int res = 0;
for(int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
int main(){
cin >> n;
for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
//将区间分成n份,看每一份左边有多少个大于它和小于它的
for(int i = 1; i <= n; i++){
int y = a[i];
Greater[i] = sum(n) - sum(y);
Lower[i] = sum(y - 1);
add(y, 1);
}
//memset一下tr数组,倒着求每一份右边有多少个小于和大于它的
memset(tr, 0, sizeof(tr));
LL res1 = 0, res2 = 0;
for(int i = n; i; i--){
int y = a[i];
res1 += Greater[i] * (LL)(sum(n) - sum(y));
res2 += Lower[i] * (LL)sum(y - 1);
add(y, 1);
}
cout << res1 << " " << res2 << endl;
return 0;
}
2 区间增减,单点查询
主要思想:建立差分数组
- 例题:
数据范围
1
≤
N
,
M
≤
1
0
5
,
1≤N,M≤10^5,
1≤N,M≤105,
∣
d
∣
≤
10000
,
|d|≤10000,
∣d∣≤10000,
∣
A
[
i
]
∣
≤
1
0
9
|A[i]|≤10^9
∣A[i]∣≤109
输入样例:
10 5
1 2 3 4 5 6 7 8 9 10
Q 4
Q 1
Q 2
C 1 6 3
Q 2
输出样例:
4
1
2
5
- 主要思路:
树状数组每次修改对应的是某个范围的前缀和的值的修改,所以如果需要在大数据量的情况下进行区间修改操作,大概率会TLE的。
但是我们想到,区间操作我们可以利用最基础的差分,使得区间操作优化成 O ( 1 ) O(1) O(1)的复杂度。
可以看到,将题目在原始组上的操作,转换到差分数组上,和树状数组解决的问题一致。
- 代码:
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N = 100010;
int tr[N], a[N];
int n, m;
int lowbit(int x){
return x & -x;
}
void add(int x, int c){
for(int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
LL quary(int x){
LL res = 0;
for(int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> a[i];
add(i, a[i] - a[i - 1]);
}
while(m--){
char op;
cin >> op;
if(op == 'C'){
int l, r, d;
cin >> l >> r >> d;
add(l, d), add(r + 1, -d);
}else{
int x;
cin >> x;
cout << quary(x)<< endl;
}
}
return 0;
}
3 区间增减,区间求和
主要思想:建立差分数组+公式
- 例题:
数据范围
1
≤
N
,
M
≤
1
0
5
,
1≤N,M≤10^5,
1≤N,M≤105,
∣
d
∣
≤
10000
,
|d|≤10000,
∣d∣≤10000,
∣
A
[
i
]
∣
≤
1
0
9
|A[i]|≤10^9
∣A[i]∣≤109
输入样例:
10 5
1 2 3 4 5 6 7 8 9 10
Q 4 4
Q 1 10
Q 2 4
C 3 6 3
Q 2 4
输出样例:
4
55
9
15
- 主要思想:
上一个应用虽然利用差分数组解决了区间操作的问题,但是区间操作完之后,利用差分数组不利于进行区间查询,所以需要进行一些推导,看看有什么性质可以利用。
数学推导见如下题解:https://www.acwing.com/solution/content/44886/
因此只需维护两个树状数组即可
一个是差分数组d[i]的树状数组tr[i],还有一个是i*d[i]的树状数组tri[i]
- 代码:
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N = 100010;
int a[N];
LL tr1[N], tr2[N]; //tr1[i] : b[i]的前缀和 tr2[i] : i * b[i]的前缀和
int n, m;
int lowbit(int x){
return x & -x;
}
void add(LL tr[], int x, LL c){
for(int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
LL quary(LL tr[], int x){
LL res = 0;
for(int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
LL prefix_sum(int x){
return quary(tr1, x) * (LL)(x + 1) - quary(tr2, x);
}
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> a[i];
int b = a[i] - a[i - 1];
add(tr1, i, (LL)b);
add(tr2, i, (LL)i * b);
}
while(m--){
char op;
cin >> op;
if(op == 'C'){
int l, r, d;
cin >> l >> r >> d;
add(tr1, l, (LL)d), add(tr1, r + 1, (LL)-d);
add(tr2, l, (LL)l * d), add(tr2, r + 1, (LL)(r + 1) * -d);
}else{
int l, r;
cin >> l >> r;
cout << prefix_sum(r) - prefix_sum(l - 1) << endl;
}
}
return 0;
}