【复健】树状数组
树状数组复健
Before
昨天机房讲树状数组的时候我在复健 MST,所以今天补一下树状数组。
发现自己以前写的学习笔记毫无逻辑,狗屁不通,根本读不懂。
本文自言自语含量大于学术性内容
树状数组(BIT)
一种支持 \(O(\log n)\) 复杂度单点修改与区间求和的数据结构。
树状数组并不是一种树形结构。
优点
修改与查询性能都比较优秀,比线段树省时省空间。码量小。
缺点
比线段树应用范围小,理解麻烦。
原理
用一些分散的区间和来储存数据。每个小区间储存数据的数量与其二进制最低位的 \(1\) 及其之后的 \(0\) 组成的数相等。
可以考虑使用位运算获得最后一位 \(1\), 即 \(lowerbit\) 运算。
#define lowerbit(x) x & -x;
提供一种原理:计算机内有符号数以补码形式存储,当 \(x\) 是正数时, \(-x\) 会以其反码 \(+1\) 的形式储存。若 \(x\) 是奇数,即其补码尾数是 \(1\), 那么 \(-x\) 的反码尾数必定是 \(0\), 那么反码 \(+1\), 尾数一定也是 \(1\), 此时其它位都相反,x & -x
得到的就是尾数 \(1\).
同理可得偶数证明方法。
实现
区间 \(x\) 一定包含 \(x\), 包含 \(x\) 的区间之间相隔 \(lowebit(x)\) 个单位。
设 \(pos\) 为当前位置,则有:
单点修改:从当前位置开始,步长 \(lowerbit(pos)\), 将所有区间都加上/减去修改的变化量。
求前 \(n\) 项和:从 \(n\) 向下,步长 \(lowerbit(pos)\), 直到 \(pos < 1\) 为止。
利用这两项操作可以完成和树状数组有关的许多问题。
例题
【模板】树状数组1
题面:P3374
一道普通板子。
展开代码
#include <bits/stdc++.h>
#define ll long long
#define lowerbit(x) x & -x
#define MyWife Cristallo
using namespace std;
const int N = 1e5 + 5;
int num[N], bit[N], a, b, c, n, m;
char ch[6];
void update(int i, int x) { //i是位置,x是修改后的变化量
for(int pos = i; pos <= n; pos += lowerbit(pos)) bit[pos] += x;
}
int iplus(int i) {
int sum = 0;
for(int pos = i; pos; pos -= lowerbit(pos)) sum += bit[pos];
//第i个区间一定包含i,从i向下,每个包含i的区间相隔lowerbit(i)个单位长度
return sum;
}
int lplusr(int l, int r) {
return iplus(r) - iplus(l - 1);
}
int main() {
scanf("%d", &n);
for(int i = 1; i <= n; ++i) {scanf("%d", num + i); update(i, num[i]); }
scanf("%d", &m);
while(m--) {
scanf("%s", ch + 1);
if(ch[1] == 'A') {
scanf("%d%d%d", &a, &b, &c);
for(int i = a; i <= b; ++i) update(i, c);
}
if(ch[1] == 'Q') {
scanf("%d", &a);
printf("%d\n", lplusr(a, a));
}
}
return 0;
}
数列操作b
展开题面
假设有一列数 \({A_1, A_2, ..., A_i, ..., A_n}(1 \le i \le N, N \le 100000)\) ,支持如下两种操作:
-
将 \(A_i\) 至 \(A_j\) 的值均增加 \(D\). (\(i, j, D\) 是输入的数)
-
输出 \(A_i\). (\(i\) 是输入的数, \(i \le n\))
根据操作要求进行正确操作并输出结果。
P.S. 由于原题面 \(\LaTeX\) 崩坏过于严重,这里进行了一些修润。
一道板子,区间修改就是 for 循环 + 单点修改,单点查询就是长度为 \(1\) 的区间查询。
展开代码
#include <bits/stdc++.h>
#define ll long long
#define lowerbit(x) x & -x
#define MyWife Cristallo
using namespace std;
const int N = 1e5 + 5;
int num[N], bit[N], a, b, c, n, m;
char ch[6];
void update(int i, int x) { //i是位置,x是修改后的变化量
for(int pos = i; pos <= n; pos += lowerbit(pos)) bit[pos] += x;
}
int iplus(int i) {
int sum = 0;
for(int pos = i; pos; pos -= lowerbit(pos)) sum += bit[pos];
//第i个区间一定包含i,从i向下,每个包含i的区间相隔lowerbit(i)个单位长度
return sum;
}
int lplusr(int l, int r) {
return iplus(r) - iplus(l - 1);
}
int main() {
scanf("%d", &n);
for(int i = 1; i <= n; ++i) {scanf("%d", num + i); update(i, num[i]); }
scanf("%d", &m);
while(m--) {
scanf("%s", ch + 1);
if(ch[1] == 'A') {
scanf("%d%d%d", &a, &b, &c);
for(int i = a; i <= b; ++i) update(i, c);
}
if(ch[1] == 'Q') {
scanf("%d", &a);
printf("%d\n", lplusr(a, a));
}
}
return 0;
}
每天都在被自己的一些离谱错误气疯:
校门外的树
题面:Vijos P1448
开始想的是用树状数组求区间最值,后来发现没有原始数据求不了。抠了一天去看了个题解,说是可以用两个树状数组存每次修改的边界。
这谁能想出来啊,反正我不能。
展开代码
#include <bits/stdc++.h>
#define ll long long
#define lowerbit(x) x & -x
#define MyWife Cristallo
using namespace std;
const int N = 5 * 1e5 + 5;
int num[N], bit1[N], bit2[N], a, b, c, k, n, m;
void update1(int i, int x) {
for(int pos = i; pos <= n; pos += lowerbit(pos)) bit1[pos] += x;
}
void update2(int i, int x) {
for(int pos = i; pos <= n; pos += lowerbit(pos)) bit2[pos] += x;
}
int iplus1(int i) {
int sum = 0;
for(int pos = i; pos; pos -= lowerbit(pos)) sum += bit1[pos];
return sum;
}
int iplus2(int i) {
int sum = 0;
for(int pos = i; pos; pos -= lowerbit(pos)) sum += bit2[pos];
return sum;
}
int lplusr(int l, int r) {
return iplus1(r) - iplus2(l - 1);
}
int main() {
scanf("%d%d", &n, &m);
while(m--) {
scanf("%d%d%d", &k, &a, &b);
if(k == 1) {update1(a, 1); update2(b, 1); }
else printf("%d\n", lplusr(a, b));
}
return 0;
}
伟大的 2k3h 同志做出了这样的决定:
一键查询 Kiichi_ 精神状态: