树状数组
基本操作
一、单修区查
洛谷P3374
题目描述
如题,已知一个数列,你需要进行下面两种操作:
将某一个数加上 xx
求出某区间每一个数的和
输入格式
第一行包含两个正整数 n,m,分别表示该数列数字的个数和操作的总个数。
第二行包含 n个用空格分隔的整数,其中第 i个数字表示数列第 i项的初始值。
接下来 m 行每行包含 3个整数,表示一个操作,具体如下:
1 x k 含义:将第 x个数加上 k
2 x y 含义:输出区间 [x,y] 内每个数的和
输出格式
输出包含若干行整数,即为所有操作 2的结果。
对于 100% 的数据,1 <= n, m <= 5 * 105
输入 #1
5 5
1 5 4 2 3
1 1 3
2 2 5
1 3 -1
1 4 2
2 1 4
输出 #1
14
16
代码
//单修区查
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define lowbit(x) x&(-x) //返回非负整数x在二进制表示下最低位1及其后面的0构成的数值
const int N = 5e5+10;
int n, m;
LL a[N];
LL tr[N];
//将序列中第x个数加上k
void add(int x, int k)
{
for(int i = x; i <= n; i += lowbit(i)) tr[i] += k;
}
//查询序列前x个数的和
LL ask(int x)
{
LL ans=0;
for(int i = x; i ; i -= lowbit(i)) ans += tr[i];
return ans;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i = 1; i <= n; i ++)
{
scanf("%lld",&a[i]);
add(i,a[i]);
}
while(m--)
{
int op, x, y, k;
scanf("%d%d", &op, &x);
if(op==2)
{
scanf("%d",&y);
cout << ask(y)-ask(x-1) << endl;
}
else
{
scanf("%d",&k);
add(x,k);
}
}
return 0;
}
二、区修单查
洛谷P3368
题目描述
如题,已知一个数列,你需要进行下面两种操作:
将某区间每一个数数加上 x;
求出某一个数的值。
输入格式
第一行包含两个整数 N、M,分别表示该数列数字的个数和操作的总个数。
第二行包含 N个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。
接下来 M 行每行包含 2 或 4个整数,表示一个操作,具体如下:
操作 1: 格式:1 x y k 含义:将区间 [x,y] 内每个数加上 k;
操作 2: 格式:2 x 含义:输出第 x个数的值。
输出格式
输出包含若干行整数,即为所有操作 2的结果。
对于 100% 的数据,1 <= n, m <= 5 * 105
输入输出样例
输入 #1
5 5
1 5 4 2 3
1 2 4 2
2 3
1 1 5 -1
1 3 5 7
2 4
输出 #1
6
10
代码
//区修单查
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 5e5+10;
typedef long long LL;
int n, m;
int a[N]; //原数组
LL tr[N]; //差分数组
int lowbit(int x)
{
return x & -x;
}
void add(int x, int k)
{
for(int i = x; i <= n; i += lowbit(i)) tr[i] += k;
}
LL ask(int x)
{
LL res = 0;
for(int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
int main()
{
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]); //维护差分数组
while(m--)
{
int op;
int l, r, k;
scanf("%d%d",&op, &l);
if(op == 1)
{
scanf("%d%d",&r, &k);
add(l, k);
add(r+1, -k); //前缀和思想
}
else
{
printf("%lld\n",ask(l));
}
}
return 0;
}
三、区修区查
AcWing 243.一个简单的整数问题2
题目描述
给定一个长度为N的数列A,以及M条指令,每条指令可能是以下两种之一:
1、“C l r d”,表示把 A[l],A[l+1],…,A[r] 都加上 d。
2、“Q l r”,表示询问 数列中第 l~r 个数的和。
对于每个询问,输出一个整数表示答案。
输入格式
第一行两个整数N,M。
第二行N个整数A[i]。
接下来M行表示M条指令,每条指令的格式如题目描述所示。
输出格式
对于每个询问,输出一个整数表示答案。
每个答案占一行。
数据范围
1 ≤ N, M ≤ 105,
|d| ≤ 10000,
|A[i]| ≤ 1000000000
输入样例:
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
解析
1.这是一道最常见的树状数组模板题,用于实现区间的修改与求和,在时间与空间上都优于带lazy标记的线段树。
2.具体分析,请看大佬题解
代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010;
int n, m;
int a[N];
LL tr1[N]; // 维护b[i]的前缀和
LL tr2[N]; // 维护b[i] * i的前缀和
int lowbit(int x)
{
return x & -x;
}
void add(LL tr[], int x, LL k)
{
for(int i = x; i <= n; i += lowbit(i)) tr[i] += k;
}
LL sum(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) //a[]的前缀和
{
return sum(tr1, x) * (x + 1) - sum(tr2, x);
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
for(int i = 1; i <= n; i ++)
{
int b = a[i] - a[i - 1]; //差分数组
add(tr1, i, b);
add(tr2, i, (LL)b * i);
}
while(m --)
{
char op[2];
int l, r, d;
scanf("%s%d%d", op, &l, &r);
if(*op == 'Q')
{
printf("%lld\n", prefix_sum(r) - prefix_sum(l - 1));
}
else
{
scanf("%d", &d);
// a[l] += d
add(tr1, l, d), add(tr2, l, l * d);
// a[r + 1] -= d
add(tr1, r + 1, -d), add(tr2, r + 1, (r + 1) * -d);
}
}
return 0;
}
简单应用
一、谜一样的牛
AcWing 244
题目描述
有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
代码
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
int h[N];
int ans[N];
int tr[N]; //树状数组维护h[1]~h[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()
{
scanf("%d", &n);
for(int i = 2; i <= n; i ++) scanf("%d", &h[i]); //i从2开始,因为h[1]默认为0
for(int i = 1; i <= n; i ++) tr[i] = lowbit(i); //初始化,每一个区间的总和=区间长度(优化版)
// add(i,1)也可,每个数都是1
for(int i = n; i; i --) //从后往前推
{
int k = h[i] + 1; //第i头牛前面有h[i]头牛比它矮,说明ans[i]在剩下的牛中排名h[i]+1
int l = 1, r = n;
while(l < r)
{
int mid = l + r >> 1;
if (sum(mid) >= k) r = mid;
else l = mid + 1;
}
ans[i] = r;
add(r, -1); //删除某个数
}
for(int i = 1; i <= n; i ++) printf("%d\n", ans[i]);
return 0;
}
二、楼兰图腾
AcWing 241
题目描述
在完成了分配任务之后,西部314来到了楼兰古城的西部。
相传很久以前这片土地上(比楼兰古城还早)生活着两个部落,一个部落崇拜尖刀(‘V’),一个部落崇拜铁锹(‘∧’),他们分别用V和∧的形状来代表各自部落的图腾。
西部314在楼兰古城的下面发现了一幅巨大的壁画,壁画上被标记出了N个点,经测量发现这N个点的水平位置和竖直位置是两两不同的。
西部314认为这幅壁画所包含的信息与这N个点的相对位置有关,因此不妨设坐标分别为(1,y1),(2,y2),…,(n,yn),其中y1~yn是1到n的一个排列。
西部314打算研究这幅壁画中包含着多少个图腾。
如果三个点(i,yi),(j,yj),(k,yk)满足1≤i<j<k≤n且yi>yj,yj<yk,则称这三个点构成V图腾;
如果三个点(i,yi),(j,yj),(k,yk)满足1≤i<j<k≤n且yi<yj,yj>yk,则称这三个点构成∧图腾;
西部314想知道,这n个点中两个部落图腾的数目。
因此,你需要编写一个程序来求出V的个数和∧的个数。
输入格式
第一行一个数n。
第二行是n个数,分别代表y1,y2,…,yn。
输出格式
两个数,中间用空格隔开,依次为V的个数和∧的个数。
数据范围
对于所有数据,n≤200000,且输出答案不会超过int64。
y1∼yn 是 1 到 n 的一个排列。
输入样例:
5
1 5 3 2 4
输出样例:
3 4
解析请看大佬题解
代码
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 2000010;
typedef long long LL;
int n;
int a[N];
LL t[N]; //t[i]表示树状数组i结点覆盖的范围和
//Lower[i]表示左边比第i个位置小的数的个数
//Greater[i]表示左边比第i个位置大的数的个数
LL Lower[N], Greater[N];
int lowbit(int x)
{
return x & -x;
}
void add(int x, int k)
{
for(int i = x; i <= n; i += lowbit(i)) t[i] += k;
}
LL ask(int x)
{
LL sum = 0;
for(int i = x; i; i -= lowbit(i)) sum += t[i];
return sum;
}
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
//从左向右,依次统计每个位置左边比第i个数y小的数的个数以及大的数的个数
for(int i = 1; i <= n; i ++)
{
int y = a[i]; //第i个数
//在前面已加入树状数组的所有数中统计在区间[1, y - 1]的数字的出现次数
Lower[i] = ask(y - 1);
//在前面已加入树状数组的所有数中统计在区间[y + 1, n]的数字的出现次数
Greater[i] = ask(n) - ask(y);
//将y加入树状数组,即数字y出现1次
add(y, 1);
}
//清空树状数组,从右往左统计每个位置右边比第i个数y小的数的个数以及大的数的个数
memset(t, 0, sizeof t);
LL resA = 0, resV = 0;
for(int i = n; i >= 1; i --)
{
int y = a[i];
resA += (LL)Lower[i] * ask(y - 1);
resV += (LL)Greater[i] * (ask(n) - ask(y));
//将y加入树状数组,即数字y出现1次
add(y, 1);
}
printf("%lld %lld\n", resV, resA);
return 0;
}