本文先修知识:树状数组详解
区间更新、单点查询
AcWing 242. 一个简单的整数问题
题目描述
给定长度为 N 的数列 A,然后输入 M 行操作指令。
第一类指令形如 C l r d,表示把数列中第 l∼r 个数都加 d。
第二类指令形如 Q x,表示询问数列中第 x 个数的值。
对于每个询问,输出一个整数表示答案。
输入格式
第一行包含两个整数 N 和 M。
第二行包含 N 个整数 A[i]。
接下来 M 行表示 M 条指令,每条指令的格式如题目描述所示。
输出格式
对于每个询问,输出一个整数表示答案。
每个答案占一行。
数据范围
1≤N,M≤105,
|d|≤10000,
|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
分析
我们知道,树状数组一般用于解决单点更新、区间查询问题,但是本题需要解决区间更新问题,相当于原问题的对称问题。如果只是暴力的调用单点更新的算法去进行区间更新,那么单次区间更新的时间复杂度是O(nlogn),是我们所不能接受的。
根据差分知识,很容易想到,给数组a的[l,r]区间内所有元素加上一个数c,等价于对原数组的差分数组b里的b[l]加上c,给b[r+1]减去c,所以不妨用树状数组来维护原数组的差分数组,这样原数组的区间更新操作就等价于在差分数组上执行两次单点更新操作,而原数组的单点查询操作等价于在差分数组里求前缀和,也就是区间查询操作。这样一来,原问题就转化为了单点更新,区间查询问题。
另外要注意的是,读取本题的操作指令时,如果使用scanf读取单个字符,需要在前面用getchar()接收掉上一行的回车符。
代码
#include <cstdio>
using namespace std;
typedef long long ll;
const int N = 100005;
int n,a[N];
ll tr[N];
int lowbit(int x){
return -x & x;
}
void add(int x,int c){
for(;x <= n;x += lowbit(x)) tr[x] += c;
}
ll query(int x){
ll res = 0;
for(;x;x -= lowbit(x)) res += tr[x];
return res;
}
int main(){
int m;
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i++) scanf("%d",&a[i]);
for(int i = 1;i <= n;i++) add(i,a[i] - a[i-1]);
char op;
int l,r,d,x;
while(m--){
getchar();
scanf("%c",&op);
if(op == 'C'){
scanf("%d%d%d",&l,&r,&d);
add(l,d),add(r + 1,-d);
}
else{
scanf("%d",&x);
printf("%lld\n",query(x));
}
}
return 0;
}
区间更新、区间查询
AcWing 243. 一个简单的整数问题2
题目描述
给定一个长度为 N 的数列 A,以及 M 条指令,每条指令可能是以下两种之一:
C l r d,表示把 A[l],A[l+1],…,A[r] 都加上 d。
Q l r,表示询问数列中第 l∼r 个数的和。
对于每个询问,输出一个整数表示答案。
输入格式
第一行两个整数 N,M。
第二行 N 个整数 A[i]。
接下来 M 行表示 M 条指令,每条指令的格式如题目描述所示。
输出格式
对于每个询问,输出一个整数表示答案。
每个答案占一行。
数据范围
1≤N,M≤105,
|d|≤10000,
|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
分析
本题需要我们实现区间更新、区间查询操作,在不使用线段树的基础上,使用树状数组巧妙的解决这个问题。
在上一题中,我们使用差分数组实现了区间更新、单点查询的操作,如果要实现区间查询的操作,暴力的做法就是对差分数组的前缀和数组求前缀和。
原数组a[n],我们维护的差分数组b[n] = a[n] - a[n-1],查询a[n]的值需要对b[n]求前缀和,也就是a[n] = a[n] - a[n-1] + a[n-1] - a[n-2] + … + a[2] - a[1] + a[1] = b[n] + b[n-1] + … + b[1],那么a[n]的前缀和s[n] = a[n] + a[n-1] + … + a[1] = (b[n] + b[n-1] + … + b[1]) + (b[n-1] + … + b[1]) + … + b1 = b[n] + 2b[n-1] + 3b[n-3] + … + nb[1] = (n + 1)(bn +…+b1) - (nb[n] + (n-1)b[n-1] + … + 2b[2] + b[1])。
也就是说,上一题我们只需要维护a的差分数组b,对b求和就可以求出a的值了,但是本题还需要额外维护一个数组c[n] = nb[n],同时对c数组求和,根据b、c数组的值才能够求出a数组的前缀和,进而实现区间查询的功能。为什么我们不直接定义一个(n - i + 1)b[i]的数组呢?因为这个数组的取值会随着n的变化而变化,求前两项和n就等于2,求前三项和n就等于3,数组的值不断变化,无法进行累加求和。
最后要注意下本题输入的后面可能有空格,无法使用上一题那种getchar的方式除掉上一行的回车了,因为每行结束有的有空格有的没有,所以还是直接读取一个字符数组,根据字符数组首字符来判断是什么操作。
代码
#include <cstdio>
using namespace std;
typedef long long ll;
const int N = 100005;
int n,a[N];
ll tr1[N],tr2[N];
int lowbit(int x){
return -x & x;
}
void add(ll x,int c){
ll t = x;
for(;x <= n;x += lowbit(x)) {
tr1[x] += c;
tr2[x] += t * c;
}
}
ll query(ll x){
ll res1 = 0,res2 = 0;
ll t = x + 1;
for(;x;x -= lowbit(x)) {
res1 += tr1[x];
res2 += tr2[x];
}
ll res = t * res1 - res2;
return res;
}
int main(){
int m;
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i++) scanf("%d",&a[i]);
for(int i = 1;i <= n;i++) add(i,a[i] - a[i-1]);
char op[3];
int l,r,d,x;
while(m--){
getchar();
scanf("%s",&op);
if(*op == 'C'){
scanf("%d%d%d",&l,&r,&d);
add(l,d),add(r + 1,-d);
}
else{
scanf("%d%d",&l,&r);
printf("%lld\n",query(r) - query(l - 1));
}
}
return 0;
}
动态数组查询第k大的数
AcWing244. 谜一样的牛
题目描述
有 n 头奶牛,已知它们的身高为 1∼n 且各不相同,但不知道每头奶牛的具体身高。
现在这 n 头奶牛站成一列,已知第 i 头牛前面有 Ai 头牛比它低,求每头奶牛的身高。
输入格式
第 1 行:输入整数 n。
第 2…n 行:每行输入一个整数 Ai,第 i 行表示第 i 头牛前面有 Ai 头牛比它低。
(注意:因为第 1 头牛前面没有牛,所以并没有将它列出)
输出格式
输出包含 n 行,每行输出一个整数表示牛的身高。
第 i 行输出第 i 头牛的身高。
数据范围
1≤n≤105
输入样例:
5
1
2
1
0
输出样例:
2
4
5
3
1
分析
题目告诉了我们每头牛前面有几头牛比它低,并且这些牛身高在1~n之间各不相同。
以用例为例,第一头牛前面没有比它矮的,所以比它矮的数量是0,根据0 1 2 1 0我们知道,最后一头牛前面没有比它矮的,所以最后一头牛高度是1,然后倒数第二头牛前面有一头比它矮的,由于高度是1的牛在它后面,不能算在内,剩下的高度中倒数第二头牛就排第二矮,也就是高度是3,倒数第三头牛也是要在去掉后面两头牛以外,前面有2个比它矮的,所以它在剩下的牛中排名第3矮,也就是高度是5,依次类推。
通过对用例的模拟可知,我们需要实现两个功能:
- 删掉一头牛的高度
- 在剩下的牛中找出第k矮的牛的身高
暴力做法可以使用一个长度为n的bool数组,每删掉一头牛的高度就将该位置置为true,然后在剩下的牛中找到第k矮的,由于这个bool数组里存在已经删掉的数,并且被删掉的数在时时更新,所以如果二分第k矮牛的位置,需要对前面的牛数进行统计,暴力统计是线性复杂度的,这个问题也是单点修改,二分时求牛的数量是进行区间求和,所以也可以使用树状数组优化。
我们可以定义一个初始值全是1的原始数组,表示每头牛开始都在,然后用个树状数组来维护这个数组的一些状态。去掉一头牛,等价于将一个位置上的数减去1,对应于单点修改操作,二分时求前面的牛数也就是树状数组的区间求和操作。
本题属于树状数组在一个动态变化的数组里求第k小的数问题中的应用。
代码
#include <cstdio>
using namespace std;
typedef long long ll;
const int N = 100005;
int n,a[N];
ll tr[N];
int lowbit(int x){
return -x & x;
}
void add(int x,int c){
for(;x <= n;x += lowbit(x)) tr[x] += c;
}
ll query(int x){
ll res = 0;
for(;x;x -= lowbit(x)) res += tr[x];
return res;
}
int main(){
scanf("%d",&n);
for(int i = 2;i <= n;i++) scanf("%d",&a[i]);
for(int i = 1;i <= n;i++) tr[i] = lowbit(i);
for(int i = n;i >= 1;i--){
int target = a[i] + 1;
int l = 1,r = n;
while(l < r){
int mid = l + r >> 1;
if(query(mid) < target) l = mid + 1;
else r = mid;
}
add(l,-1);
a[i] = l;
}
for(int i = 1;i <= n;i++) printf("%d\n",a[i]);
return 0;
}