基本概念
树状数组,即 Binary Indexed Tree (B.I.T), 通常用于解决区间查询,单点修改。
其复杂度为 log(n)
(哇好快
命题描述
给定数列 a[1], a[2]...a[n] ,你需要依次进行 q 个操作,操作有两类:
1 i x:给定 i,将 a[i] 加上 x;
2 l r:给定 l r,求 a[l] + a[l + 1] + a[l + 2] + ... + a[r - 1] + a[r] 的值
-
输入格式
第一行 包含 2 个正整数 n,q,n 表示数列长度,q 表示询问个数。
第二行 n 个整数 ,表示初始数列.
接下来 q 行,每行一个操作,为以下两种之一:1 i x:给定 i,x,将 a[i] 加上 x;
2 l r:给定 l, r,求 a[l] + a[l + 1] + a[l + 2] + ... + a[r - 1] + a[r] 的值。 -
输出格式
对于每个 2 l r 操作输出一行,每行有一个整数,表示所求的结果。
样例输入
3 2
1 2 3
1 2 0
2 1 3
样例输出
6
分析
-
想法NO.1:暴力
利用数组,每次修改只需 a[i] =x 即可,时间复杂度为 O(1)
每次查询从 l 到 r 循环一遍累加即可,时间复杂度为 O(n)
(但好像很慢的样子。。。 -
想法NO.2:前缀和
利用前缀和数组,每次修改 a[i] = x ,再把 i 到 n 的前缀和维护一遍,时间复杂度为 O(n)
每次查询输出 sum[r] - sum[l - 1] 即可,时间复杂度为 O(1)
(时间上好像和NO.1区别不大? -
想法NO.3,NO.4:
线段树,分块
(啊这。。。 -
想法NO.5:树状数组!!
step 1 建树
以 BIT[i] 一定有 Low_Bit(i) 个子叶为原则建造树状数组
Low_Bit(i) 表示将 i 转化成二进制数之后,只保留最低位的1及其后面的0,再转成十进制数的值。
不难想出两种求 Low_Bit 的方法:
1.(比较少用
int Low_Bit(int x) {
return x - (x & (x - 1));
// x 的二进制可以看做 A1B(A 是最后一个 1 之前的部分, B 是最后一个 1 之后的 0)
// x - 1 的二进制可以看做 A0C (C 是和 B 一样长的 1)
// 所以 x & (x - 1) 的二进制就是 A1B & A0C = A0B
}
2.(常用
int Low_Bit(int x) {
return x & -x;
// 先将原数转化成二进制之后,在与原数相反数的二进制按位与
// 因为计算机是用二进制补码计算的,且负数的补码等于它的原码取反加1
// 相与之后刚好就是Low_Bit的值
}
综上,树长这样。。。
其中
c[1] = a[1]
c[2] = c[1] + a[2]
c[3] = a[3]
c[4] = c[2] + c[3]
......
注:代码的 BIT 数组就是这里的 c 数组
step 2 单点修改
其实很简单嘛,定义函数 Update(int k, int x) 表示将原数组 a[k] += x
因为 c[i] 后面的值一定与的 a[j] (j < i) 有关,所以当修改了 a[i] 之后应该把后面的 c 都维更新一遍
void Update(int k, int x) {
for(int i = k; i <= n; i += Low_Bit(i)) // 相当于遍历 k 所有的父叶
BIT[i] += x;
return ;
}
是不是很奇怪上文的建树部分为什么没放建树的代码?
hhhhhh 因为我们可以直接在单点修改这里建树!!
函数 Update(int k, int x) 表示将原数组 a[k] += x,且 c(树状数组)初值为0
所以 Update(int k, int x) 在 c[k] = 0 时就是完成的建树操作!
step 3 区间查询
也很简单的哇,首先我们都知道给定 l,r
它的区间和应该是前缀和数组 sum[r] - sum[l - 1]
然后,我们再来看看刚刚那个图
这里面,如果要求 a[1] ~ a[7] 加起来,答案很显然应该是 c[4] + c[6] + c[7]
看似毫无规律?其实是有的!不难发现 c[i] 其实是 a[i] + a[i - 1] + ... + a[i - Low_Bit(i) + 1]
那我们要求的 sum[i] 就等于 c[i] + sum[i - Low_Bit(i)]!
所以就有:
long long Sum(int k) {
long long ans = 0;
for(int i = k; i >= 1; i -= Low_Bit(i))
ans += BIT[i];
return ans;
}
printf("%lld
", Sum(r) - Sum(l - 1));
step 4 完整代码
#include <cstdio>
const int MAXN = 1000005;
int a[MAXN];
long long BIT[MAXN];
int n, q;
int Low_Bit(int x) {
return x & -x;
}
void Update(int k, int x) {
for(int i = k; i <= n; i += Low_Bit(i))
BIT[i] += x;
return ;
}
long long Sum(int k) {
long long ans = 0;
for(int i = k; i >= 1; i -= Low_Bit(i))
ans += BIT[i];
return ans;
}
int main() {
scanf ("%d %d", &n, &q);
for(int i = 1; i <= n; i++) {
scanf ("%d", &a[i]);
Update(i, a[i]);
}
for(int i = 1; i <= q; i++) {
int flag;
scanf ("%d", &flag);
if(flag == 1) {
int index, x_;
scanf ("%d %d", &index, &x_);
Update(index, x_);
}
else {
int l, r;
scanf ("%d %d", &l, &r);
printf("%lld
", Sum(r) - Sum(l - 1));
}
}
return 0;
}
推广 1 区间修改,单点查询
命题描述
给定数列 a[1],a[2],...,a[n],你需要依次进行 q 个操作,操作有两类:
1 l r x:给定 l, r,将 a[l],a[l + 1],a[l + 2],...,a[r - 1], a[r] 分别加上 x;
2 i:给定 i ,求 a[i] 的值。
-
输入格式
第一行包含 2 个正整数 n,q 表示数列长度和询问个数。保证 。
第二行 n 个整数 ,表示初始数列。
接下来 q 行,每行一个操作,为以下两种之一:1 l r x:给定 l, r,将 a[l],a[l + 1],a[l + 2],...,a[r - 1], a[r] 分别加上 x; 2 i:给定 i ,求 a[i] 的值。
-
输出格式
对于每个 2 i 操作,输出一行,每行有一个整数,表示所求的结果。
样例输入
3 2
1 2 3
1 1 3 0
2 2
样例输出
2
分析
其实总体思路差不多……
作者能不能就直接上码(带注释……【小声】
在看代码之前你必须要知道,给原数组的差分数组求前缀和得到的结果就是原数组
证明:
c[1] = a[1] - a[0],c[2] = a[2] - a[1],...,c[i] = a[i] - a[i - 1]
所以其前缀和数组
sum[1] = c[1] = a[1]
sum[2] = sum[1] + c[2] = a[1] + a[2] - a[1] = a[2]
sum[3] = sum[2] + c[3] = a[2] + a[3] - a[2] = a[3]
...
sum[i] = sum[i] + c[i] = a[i - 1] + a[i] - a[i - 1] = a[i]
得证(撒花
AC代码
#include <cstdio>
const int MAXN = 1000005;
int a[MAXN];
long long BIT[MAXN], c[MAXN]; // 注意不要弄混了,这里的c指的是原数组的差分数组
int n, q;
int Low_Bit(int x) { // 求Low_Bit的函数
return x & -x;
}
void Update(int k, int x) {
for(int i = k; i <= n; i += Low_Bit(i))
BIT[i] += x;
return ;
}
long long Sum(int k) {
long long ans = 0;
for(int i = k; i >= 1; i -= Low_Bit(i))
ans += BIT[i];
return ans;
}
// 树状数组模板
int main() {
scanf ("%d %d", &n, &q);
for(int i = 1; i <= n; i++) {
scanf ("%d", &a[i]);
c[i] = a[i] - a[i - 1]; // 求差分数组
Update(i, c[i]);
}
for(int i = 1; i <= q; i++) {
int flag;
scanf ("%d", &flag);
if(flag == 1) {
int l, r, x_;
scanf ("%d %d %d", &l, &r, &x_);
Update(l, x_);
Update(r + 1, -x_);
// 这里比较重要,因为我们是关于差分数组建的树状数组,所以r后面的那部分是不应该加的
// 但在 Update(l, x_); 中加了,所以这里要减去
}
else {
int x;
scanf ("%d", &x);
printf("%lld
", Sum(x)); // 因为是单点修改,所以直接计算出x前的差分数组的前缀和即可
}
}
return 0;
}
推广 2 区间修改,区间查询
命题描述
给定数列 a[1],a[2],...,a[n] ,你需要依次进行 个操作,操作有两类:
1 l r x:给定 l, r,换言之,将 a[l],a[l + 1], a[l + 2],..., a[r - 1],a[r] 分别加上 x
2 l r:给定 l, r,求 a[l] + a[l + 1] + a[l + 2] + ... + a[r - 1] + a[r] 的值
-
输入格式
第一行包含 2 个正整数 ,n, q 表示数列长度和询问个数。
第二行 n 个整数 ,表示初始数列。
接下来 q 行,每行一个操作,为以下两种之一:1 l r x:将 a[l],a[l + 1], a[l + 2],..., a[r - 1],a[r] 分别加上 x 2 l r:输出 a[l] + a[l + 1] + a[l + 2] + ... + a[r - 1] + a[r] 的值。
-
输出格式
对于每个 2 l r 操作,输出一行,每行有一个整数,表示所求的结果。
样例输入
5 10
2 6 6 1 1
2 1 4
1 2 5 10
2 1 3
2 2 3
1 2 2 8
1 2 3 7
1 4 4 10
2 1 2
1 4 5 6
2 3 4
样例输出
15
34
32
33
50
蜜汁玄学推论:(A[1]+A[2]+……+ A[n])
(=P[1]+(P[1]+P[2])+(P[1]+P[2]+P[3])+……+(P[1]+P[2]+……+P[n]))
(=n*P[1]+(n-1)*P[2]+(n-2)*P[3]+……+P[n])
(=n*(P[1]+P[2]+P[3]+……+P[n])-(0*P[1]+1*P[2]+2*P[3]+……+(n-1)*P[n]))
AC代码
#include <cstdio>
const int MAXN = 1000005;
long long a[MAXN];
long long BIT1[MAXN], BIT2[MAXN];
int n, q;
int Low_Bit(int x) { return x & (-x); }
void Update_1(int k, long long x) {
for (int i = k; i <= n; i += Low_Bit(i)) BIT1[i] += x;
return;
}
long long Sum_1(long long k) {
long long ans = 0;
for (int i = k; i >= 1; i -= Low_Bit(i)) ans += BIT1[i];
return ans;
}
void Update_2(int k, long long x) {
for (int i = k; i <= n; i += Low_Bit(i)) BIT2[i] += x;
return;
}
long long Sum_2(long long k) {
long long ans = 0;
for (int i = k; i >= 1; i -= Low_Bit(i)) ans += BIT2[i];
return ans;
}
long long sum_(long long k) {
long long ans = k * Sum_1(k) - Sum_2(k);
return ans;
}
int main() {
scanf("%d %d", &n, &q);
for (int i = 1; i <= n; i++) {
scanf("%lld", &a[i]);
long long x_ = a[i] - a[i - 1];
Update_1(i, x_);
Update_2(i, x_ * (i - 1));
}
for (int i = 1; i <= q; i++) {
int flag;
scanf("%d", &flag);
if (flag == 1) {
int l, r;
long long x_;
scanf("%d %d %lld", &l, &r, &x_);
Update_1(l, x_);
Update_1(r + 1, -x_);
Update_2(l, x_ * (l - 1));
Update_2(r + 1, -x_ * (r));
} else {
int l, r;
scanf("%d %d", &l, &r);
printf("%lld
", sum_(r) - sum_(l - 1));
}
}
return 0;
}
T1 冒泡排序 & 逆序对
题目描述
clj 想起当年自己刚学冒泡排序时的经历,不禁思绪万千
当年,clj 的冒泡排序(伪)代码是这样的:
flag=false
while (not flag):
flag=true
for i = 0 to N-2:
if A[i+1] < A[i]:
swap A[i], A[i+1]
flag=false
现在的 clj 想知道冒泡排序究竟有多慢,所以在(伪)代码的第三行下面加入了这么一句:
printf("LJS NB
");
但是随着需要排序的 个数越来越多,这个程序的速度已经不能满足 clj 的耐心了
他想请你帮忙算出这个程序到底能输出多少行LJS NB
-
输入格式
第一行一个数 (n) ,表示 (A) 数组有 (n) 个数;
接下来一行共 (n) 个数,用空格隔开,表示 (A) 数组的每一个元素。 -
输出格式
一行一个整数,表示这个程序会输出多少行 LJS NB。
输入样例
5
1 5 3 8 2
输出样例1
4
分析
其实我很好奇 LJS 是哪个 LJS
呐,这道题应该有手就能做知道冒泡排序的原理就能做叭。。。
冒泡排序,是遇到不符合顺序的就交换
不符合顺序的?那不就是求逆序对吗??!
求法GM讲过的
AC代码
#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXN = 2000005;
int a[MAXN], dis[MAXN];
int n;
long long BIT[MAXN];
// 树状数组
int Low_Bit(int x) { return x & (-x); }
void Update(int k, int x) {
for (int i = k; i <= n; i += Low_Bit(i)) BIT[i] += x;
return;
}
long long Sum(int k) {
int ans = 0;
for (int i = k; i >= 1; i -= Low_Bit(i)) ans += BIT[i];
return ans;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
dis[i] = a[i];
}
sort(dis + 1, dis + n + 1);
int len = unique(dis + 1, dis + n + 1) - dis - 1;
// 去重函数
for (int i = 1; i <= n; i++) a[i] = lower_bound(dis + 1, dis + len + 1, a[i]) - dis;
// 离散化操作
long long ans = 0;
for (int i = 1; i <= n; i++) {
Update(a[i], 1);
ans = max(ans, (long long)(i - Sum(a[i])));
// Sum(a[i])就是在它前面比它小的,即顺序对个数
// i - Sum(a[i]) 就是逆序对个数
// 求出逆序对,并记录每个位置求的逆序对个数的最大值
}
printf("%lld", ans + 1);
// 加上最后一次交换
return 0;
}
T2 校门外的树
题目描述
校门外有很多树,学校决定在某个时刻在某一段种上一种树,保证任一时刻不会出现两段相同种类的树,现有两种操作:
-
(k = 1), 读入 (l, r) 表示在 (l) 到 (r) 之间种上一种树,每次操作种的树的种类都不同;
-
(k = 2), 读入 (l, r) 表示在 (l) 到 (r) 之间有多少种树。
注意:每个位置都可以重复种树。
-
输入格式
第一行 表示道路总长为 ,共有 个操作;
接下来 行为 个操作。 -
输出格式
对于每个 输出一个答案。
样例输入
5 4
1 1 3
2 2 5
1 2 4
2 3 5
样例输出
1
2
分析
我觉得这个很好,如下图
具体实现
#include <cstdio>
const int MAXN = 1000005;
int a[MAXN];
long long BIT1[MAXN], BIT2[MAXN];
int n, m;
int Low_Bit(int x) {
return x & -x;
}
// 两个树状数组
void Update1(int k, int x) {
for(int i = k; i <= n; i += Low_Bit(i))
BIT1[i] += x;
return ;
}
long long Sum1(int k) {
long long ans = 0;
for(int i = k; i >= 1; i -= Low_Bit(i))
ans += BIT1[i];
return ans;
}
void Update2(int k, int x) {
for(int i = k; i <= n; i += Low_Bit(i))
BIT2[i] += x;
return ;
}
long long Sum2(int k) {
long long ans = 0;
for(int i = k; i >= 1; i -= Low_Bit(i))
ans += BIT2[i];
return ans;
}
int main() {
scanf ("%d %d", &n, &m);
for(int i = 1; i <= m; i++) {
int flag;
scanf ("%d", &flag);
if(flag == 1) {
int x, y;
scanf ("%d %d", &x, &y);
Update1(x, 1); // 左端点
Update2(y, 1); // 右端点
}
else {
int l, r;
scanf ("%d %d", &l, &r);
printf("%lld
", Sum1(r) - Sum2(l - 1));
// 左端点数 - 右端点数
// 其实就是求区间啦
}
}
return 0;
}
T3 数星星 Stars
天空中有一些星星,这些星星都在不同的位置,每个星星有个坐标。如果一个星星的左下方(包含正左和正下)有 (k) 颗星星,就说这颗星星是 (k) 级的。
例如,上图中星星 (5) 是 (3) 级的( (1, 2, 4) 在它左下),星星 (2, 4) 是 (1) 级的。例图中有 (1) 个 (0) 级,(2) 个 (1) 级,(1) 个 (2) 级,(1) 个 (3) 级的星星。
给定星星的位置,输出各级星星的数目。
一句话题意 给定 (n) 个点,定义每个点的等级是在该点左下方(含正左、正下)的点的数目,试统计每个等级有多少个点。
-
输入格式
第一行一个整数 (n),表示星星的数目;
接下来 (n) 行给出每颗星星的坐标,坐标用两个整数 (x, y) 表示;
不会有星星重叠。星星按 (y) 坐标增序给出,(y) 坐标相同的按 (x) 坐标增序给出。 -
输出格式
(n) 行,每行一个整数,分别是 (0) 级,(1) 级, (2) 级,……,(n - 1) 级的星星的数目。
样例输入
5
1 1
5 1
7 1
3 3
5 5
样例输出
1
2
1
1
0
分析
哈这道题。。。其实与 (y) 的值无关
这就不用再分析了叭
然后就很简单了
求出每个点前面(看成线性的)有多少个星星
并把新的这颗星星记录下来
最后利用桶排思想储存答案并输出即可
AC代码
#include <cstdio>
const int MAXN = 35555;
int flag[MAXN], n;
int BIT[MAXN];
int Low_Bit(int x) {
return x & -x;
}
// 树状数组
void Update(int k, int x) {
for(int i = k; i <= MAXN; i += Low_Bit(i))
BIT[i] += x;
return ;
}
int Sum(int k) {
int ans = 0;
for(int i = k; i >= 1; i -= Low_Bit(i))
ans += BIT[i];
return ans;
}
int main() {
scanf ("%d", &n);
for(int i = 1; i <= n; i++) {
int x, y;
scanf ("%d %d", &x, &y);
int v = Sum(x + 1); // 前面的星星数
Update(x + 1, 1); // 将这个新的星星加入树状数组
flag[v]++;
}
for(int i = 0; i < n; i++)
printf("%d
", flag[i]); // 输出对应级数的星星数量
return 0;
}
T4 加法
题目描述
可怜有一个长度为 (n) 的正整数序列 (A), 但是她觉得 (A) 中的数字太小了,这让她很不开心。
于是她选择了 (m) 个区间 ([l_i, r_i]) 和两个正整数 (a), (k) 。她打算从这 (m) 个区间里选出 恰好 (k) 个区间,并对每个区间执行一次区间加 (a) 的操作。 (每个区间最多只能选择一次。)
对区间 ([l, r]) 进行一次加 (a) 操作可以定义为将 (A[l], A[l + 1], A[l + 2]...A[r - 1], A[r]) 变成 (A[l] + a, A[l + 1] + a, A[l + 2] + a...A[r -1] + a, A[r] + a)。
现在可怜想要知道怎么选择区间才能让操作后的序列的最小值尽可能的大,即最大化 。
-
输入格式
第一行输入一个整数表示数据组数。
对于每组数据第一行输入四个整数 (n, m, k, a) 。
第二行输入 (n) 个整数描述序列 (A) 。
接下来 (m) 行每行两个整数 (l_i, r_i) 描述每一个区间。数据保证所有区间两两不同。 -
输出格式
对于每组数据输出一个整数表示操作后序列最小值的最大值。
样例输入
1
3 3 2 1
1 3 2
1 1
1 3
3 3
样例输出
3
样例解释
选择给区间 ([1, 1]) 和 ([1, 3]) 加 (a)。
分析
step 1:最小值的最大值?肯定是二分a,大家都看的出来吧。。。
所以我们只需要二分查找 (mid)((mid) 表示操作后的最小值
然后判断 (mid) 是否可行,然后再放大或缩小 (mid) 的范围
step 2:如何判断呢?利用贪心的思想
遍历一遍 (A) 数组,如果 (a[i]) 严格小于 (mid),那就一定要把 (a[i]) 往上调
也就是说,要对包含 (a[i]) 的区间进行操作。
在满足这个条件的同时,你会发现当包含 (a[i]) 的这个区间左端点确定时,它的右端点越大,对后面的效益就越大,所以我们每次操作都用右端点更大的
利用优先队列或堆即可实现
step 3:如何实现每个区间的操作呢?
利用树状数组的区间修改,单点查询即可啦(但这个做法比CC说的直接使用差分数列修改的方法慢了10倍,不过笔者还是写的这种方法,毕竟标题是树状数组嘛
AC代码
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 100005;
long long BIT[MAXN];
struct data {
int l, r;
} s[MAXN];
int n, a_, m, a[MAXN];
struct node { // 优先队列,重载运算符,关于 r 建立小根堆
int l, r;
bool operator < (const node &x) const {
if(r != x.r)
return r < x.r;
return l < x.l;
}
};
bool cmp(data x, data y) {
if (x.l == y.l)
return x.r > y.r;
// 一开始就先将右端点更大的,而左端点相同的放在前面
else
return x.l < y.l;
}
// 树状数组
int Low_Bit(int x) { return x & -x; }
void Update(int k, int x) {
for (int i = k; i <= n; i += Low_Bit(i)) BIT[i] += (long long)x;
return;
}
long long Sum(int k) {
long long ans = 0;
for (int i = k; i >= 1; i -= Low_Bit(i)) ans += BIT[i];
return ans;
}
bool check(long long mid, int k) {
memset(BIT, 0, sizeof BIT);
for (int i = 1; i <= n; i++) Update(i, a[i] - a[i - 1]);
priority_queue<node> q; // 定义关于可修改区间优先队列
int j = 1; // 当前枚举到哪个可修改区间了
int tot = 0; // 修改区间次数
for(int i = 1; i <= n; i++) {
while(s[j].l <= i && j <= m) { // 如果当前区间包含 i
node x;
x.l = s[j].l;
x.r = s[j].r;
j++;
q.push(x); // 把这个区间操作放入优先队列
}
while(Sum(i) < mid) { // 如果 a[i] 还未加到大于 mid 的程度
if(q.empty()) return false; // 没有可用的区间了?返回不行
if(tot == k) return false;
// 这说明现在的修改区间次数已经到 k 了,但却进入了循环
// 即 Sum(i) < mid && tot == k 明显不能,返回不行
node x = q.top(); // 取出第一个进行操作
q.pop();
Update(x.l, a_);
Update(x.r + 1, -a_);
tot++; // 修改次数加一
}
}
return true;
}
int main() {
int t;
scanf("%d", &t);
while (t--) {
int k;
scanf("%d %d %d %d", &n, &m, &k, &a_);
long long l = 1, r = 0x3f3f3f3f;
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
r = min(r, (long long)a[i]);
}
r += (k * a_);
// r 最大取原数组的最小值加上最多的修改次数乘每次叠加的数
for (int i = 1; i <= m; i++) scanf("%d %d", &s[i].l, &s[i].r);
sort(s + 1, s + m + 1, cmp);
while (l <= r) { // 二分
long long mid = (l + r) >> 1;
if (check(mid, k)) // 判断 mid 是否可行
l = mid + 1;
else
r = mid - 1;
}
printf("%lld
", l - 1); // 最后答案在 l - 1 里面
}
return 0;
}