树状数组 | 三道模板题小结
今天写了一天北京信息科技大学校赛题,难度不大,跟西电校赛风格类似,大部分为数学题与规律题。(改日更)
其中的I题为树状数组/线段树模板题,发现之前没有在博客总结,一时也找不到好的模板,于是重新深入学习了一番。
首先引入洛谷上的模板题:
P3374 【模板】树状数组 1
题目描述
如题,已知一个数列,你需要进行下面两种操作:
1.将某一个数加上x
2.求出某区间每一个数的和
输入输出格式
输入格式:
第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。
第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。
接下来M行每行包含3个整数,表示一个操作,具体如下:
操作1: 格式:1 x k 含义:将第x个数加上k
操作2: 格式:2 x y 含义:输出区间[x,y]内每个数的和
输出格式:
输出包含若干行整数,即为所有操作2的结果。
说明
时空限制:1000ms,128M
数据规模:
对于30%的数据:N<=8,M<=10
对于70%的数据:N<=10000,M<=10000
对于100%的数据:N<=500000,M<=500000
题解
由于是【单点修改,区间查询】裸题,就直接上AC代码了:
#include<iostream> #include<cstdio> using namespace std; #define lowbit(x) ((x)&(-x)) int n, m; int C[500010]; // 树状数组只需要开一倍内存空间 int sum(int x) { int res = 0; while(x) { res += C[x]; x -= lowbit(x); } return res; } void add(int x, int d) { while(x<=n) { C[x] += d; x += lowbit(x); } } int main() { cin>>n>>m; for(int i=1;i<=n;i++) { int ai; scanf("%d", &ai); add(i, ai); } while(m--) { int q, x, y; scanf("%d %d %d", &q, &x, &y); if(q==1) { add(x, y); } else if(q==2) { printf("%d\n", sum(y)-sum(x-1)); } } return 0; }
P3368 【模板】树状数组 2
题目描述
如题,已知一个数列,你需要进行下面两种操作:
1.将某区间每一个数数加上x
2.求出某一个数的值
输入输出格式
输入格式:
第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。
第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。
接下来M行每行包含2或4个整数,表示一个操作,具体如下:
操作1: 格式:1 x y k 含义:将区间[x,y]内每个数加上k
操作2: 格式:2 x 含义:输出第x个数的值
输出格式:
输出包含若干行整数,即为所有操作2的结果。
说明
时空限制:1000ms,128M
数据规模:
对于30%的数据:N<=8,M<=10
对于70%的数据:N<=10000,M<=10000
对于100%的数据:N<=500000,M<=500000
题解
【区间修改,单点查询】跟标准的状数组模板不太一样,不能直接套模板。(改用的线段树还没理解,改日更)
我们可以对问题进行转化。
区间和利用了前缀和的思想,那么求单点/部分区间和只需要前缀和作差。
如果先把原数组进行差分处理,那么对应的区间和就恢复为原数组单点的值,这样就能实现单点查询。
来介绍一下差分
设数组a[]={1,6,8,5,10},那么差分数组b[]={1,5,2,-3,5}
也就是说b[i]=a[i]-a[i-1];(a[0]=0;),那么a[i]=b[1]+....+b[i];(这个很好证的)。
假如区间[2,4]都加上2的话
a数组变为a[]={1,8,10,7,10},b数组变为b={1,7,2,-3,3};
发现了没有,b数组只有b[2]和b[5]变了,因为区间[2,4]是同时加上2的,所以在区间内b[i]-b[i-1]是不变的.
所以对区间[x,y]进行修改,只用修改b[x]与b[y+1]:
b[x]=b[x]+k;b[y+1]=b[y+1]-k;
在看了题解差分的介绍后,我恍然大悟,随即独立写出以下AC代码:
#include<iostream> #include<cstdio> using namespace std; #define lowbit(x) ((x)&(-x)) int n, m; int s[500010]; int C[500010]; // 维护差分信息 int sum(int x) { int res = 0; while(x) { res += C[x]; x -= lowbit(x); } return res; } void add(int x, int d) { while(x<=n) { C[x] += d; x += lowbit(x); } } int main() { cin>>n>>m; for(int i=1;i<=n;i++) { scanf("%d", &s[i]); } while(m--) { int q, x, y, k; scanf("%d", &q); if(q==1) { scanf("%d %d %d", &x, &y, &k); add(x, k); add(y+1, -k); } else if(q==2) { scanf("%d", &x); printf("%d\n", s[x]+sum(x)); // a[i]=b[1]+....+b[i]
// 而sum(x)结果为区间和,对应b[1]+...+b[x] } } return 0; }
洛谷给出的题解代码:
#include <iostream> #include <cstdio> #define lowbit(x) x & -x using namespace std; long long tree[500005]; int n, m; void add(int x, long long num) { while (x <= n) { tree[x] += num; x += lowbit(x); } } long long query(int x) { long long ans = 0; while (x) { ans += tree[x]; x -= lowbit(x); } return ans; } int main() { freopen("in.txt", "r", stdin); scanf("%d%d", &n, &m); long long last = 0, now; for (int i = 1; i <= n; i++) { scanf("%lld", &now); add(i, now - last); last = now; } int flg; while (m--) { scanf("%d", &flg); if (flg == 1) { int x, y; long long k; scanf("%d%d%lld", &x, &y, &k); add(x, k); add(y + 1, -k); } else if (flg == 2) { int x; scanf("%d", &x); printf("%lld\n", query(x)); } } return 0; }
I-andy种树
链接:https://ac.nowcoder.com/acm/contest/940/I
来源:牛客网
题目描述
andy在他的庄园里种了n棵树,排列成一排,标号为1到n。最开始的时候n棵树的高度都是0,也就是种子刚刚被埋下,树还没有长出来。
andy会一种魔法,他每使用一次魔法,就可以让树标号落在连续区间[l, r]里的树的高度增加1。他可以使用q次这种魔法,然后他很好奇,在使用了q次魔法之后,他的所有树的高度分别是多少呢?
输入描述
第一行输入两个整数n,q。(1<= n, q <= 1e5)
接下来q行,每行输入两个整数l, r(l <= r),表示andy让标号落在区间[l, r]里的数高度都加1
输出描述
输出有一行n个整数,每个整数后面有空格。
输出末尾没有换行,第i个数表示第i棵树的高度
示例
输入
10 3
1 3
2 4
3 3
输出
1 2 3 1 0 0 0 0 0 0
说明
andy种了10棵树
第一次使用魔法使得1、2、3棵树的高度增加1,所有树的高度为
1 1 1 0 0 0 0 0 0 0
第二次使用魔法使得2、3、4棵树的高度增加1,所有树的高度为
1 2 2 1 0 0 0 0 0 0
第三次使用魔法使得第3棵树的高度增加1,所有树的高度为
1 2 3 1 0 0 0 0 0 0
题解
这是典型的区间更新,单点查询的问题,使用线段树的解法(参考):
#include <cstdio> #include <iostream> #include <algorithm> #define lson l, mid, rt << 1 #define rson mid + 1, r, rt << 1 | 1 using namespace std; int const MAX = 1e5 + 100; long long mx[MAX<<2]; void Update(int L, int R, int l, int r, int rt) { if(L<=l && R>=r) { mx[rt] += 1; return; } int mid = (l + r) >> 1; if(L<=mid) Update(L, R, lson); if(mid<R) Update(L, R, rson); } void Query(int l, int r, int rt, long long k) { if(l==r) { if(l==1)printf("%lld", mx[rt]+k); else printf(" %lld", mx[rt]+k); return; } int mid = (l + r) >> 1; Query(lson, mx[rt]+k); Query(rson, mx[rt]+k); } int main() { int n, q; cin>>n>>q; while(q--) { int l, r; scanf("%d %d", &l, &r); Update(l, r, 1, n, 1); } Query(1, n, 1, 0); printf("\n"); return 0; }
利用差分思想,我们考虑用数组b[i]记录树a[i]-a[i-1]的高度差,对区间[l, r]里的数进行+1操作只会影响数组b[l]与b[r+1]处的值,即
b[l] = b[l] + 1
b[r+1] = b[r+1] - 1
而树状数组能在O(logn)内实现单点修改和区间求和的操作,总复杂度O(nlogn)。
维护数组b[],sum(i)=a[i]=b[0]+...+b[i]即为第i颗树的高度。
#include<iostream> #include<cstdio> using namespace std; #define lowbit(x) ((x)&(-x)) int n, m; int C[500010]; // 维护差分信息 int sum(int x) { int res = 0; while(x) { res += C[x]; x -= lowbit(x); } return res; } void add(int x, int d) { while(x<=n) { C[x] += d; x += lowbit(x); } } int main() { cin>>n>>m; while(m--) { int x, y; scanf("%d %d", &x, &y); add(x, 1); add(y+1, -1); } printf("%d", sum(1)); for(int i=2;i<=n;i++) { printf(" %d", sum(i)); } return 0; }
(完)