树状数组小记
\large\color{00aacd}\textbf{树状数组(Binary Index Tree)}
发现自己之前零零碎碎地写过一些树状数组的应用,所以写这一篇文章来整合一下。
树状数组是一种支持单点修改,区间查询的精巧的数据结构,通常用于维护满足结合律和可差分的运算和信息。又称二叉索引树(Binary Index Tree)、Fenwick Tree。
\color{00cd00}\text{单点修改,区间查询}
下面这张图展示了树状数组的原理(来源:OI-Wiki)。
其中
例如,$10$ 在二进制表示下为 $10\underset{\blacktriangle}{\bf1}0$,加粗的就是最低位的 $1$,它的权值是 $2$,因此 $\rm lowbit(10)=2$。 再例如,$24$ 在二进制表示下为 $1\underset{\blacktriangle}{\bf1}000$,最低位的 $1$ 的权值为 $8$,因此 $\rm lowbit(24)=8$。 根据位运算知识,可以得到 `lowbit(x) = x & -x`,其中 `&` 为**按位与**运算。
如果一个数减去自己的
例如
那么我们要计算
由此我们可以得到查询
int query(int x)
{
int ans = 0;
while(x > 0)
{
ans += c[x];
x -= lowbit(x);
}
return ans;
}
可以发现,树状数组通过将一段数划分成
如果要求任意一段区间 query(r) - query(l-1)
。这也说明树状数组可以当成一个支持修改的前缀和来用。
如果要将
void update(int x, int k)
{
while(x <= n)
{
c[x] += k;
x += lowbit(x);
}
}
显然,修改操作的时间复杂度也为
例题:P3374 【模板】树状数组 1
以下是一份经过封装的极简树状数组代码,可以通过本题。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5;
struct BIT{ //树状数组
int c[N], lowbit(int x){return x & -x;}
void update(int x, int k){while(x < N) c[x] += k, x += lowbit(x);}
int query(int x){int s = 0; while(x) s += c[x], x -= lowbit(x); return s;}
} t;
long long n, m;
signed main(){
cin.tie(nullptr) -> sync_with_stdio(false);
cin >> n >> m;
for(int i=1, x; i<=n; i++) cin >> x, t.update(i, x);
while(m --> 0){
int op, x, y; cin >> op >> x >> y;
if(op == 1) t.update(x, y);
if(op == 2) cout << t.query(y) - t.query(x - 1) << "\n";
}
return 0;
}
\color{00cd00}\text{区间修改,单点查询}
例题:P3368 【模板】树状数组 2
借助差分的思想。定义差分数组
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5;
struct BIT{ //树状数组维护差分数组
int c[N], lowbit(int x){return x & -x;}
void update(int x, int k){while(x < N) c[x] += k, x += lowbit(x);}
int query(int x){int s = 0; while(x) s += c[x], x -= lowbit(x); return s;}
} t;
int n, m, a[N];
signed main(){
cin.tie(nullptr) -> sync_with_stdio(false);
cin >> n >> m;
for(int i=1; i<=n; i++) cin >> a[i], t.update(i, a[i] - a[i-1]);
while(m --> 0){
int op, x, y, k; cin >> op;
if(op == 1) cin >> x >> y >> k, t.update(x, k), t.update(y + 1, -k);
if(op == 2) cin >> x, cout << t.query(x) << "\n";
}
return 0;
}
\color{00cd00}\text{区间修改,区间查询}
例题:P3372 【模板】线段树 1
我们已经会了使用差分数组实现区间修改。接下来只要考虑如何区间查询。因为
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5;
struct BIT{
int c[2][N], lowbit(int x){return x & -x;}
void update(int x, int k){
for(int i=x; i<N; i+=lowbit(i)){
c[0][i] += k, c[1][i] += k * x;
}
}
int query(int x){
int ans = 0;
for(int i=x; i>0; i-=lowbit(i)){
ans += c[0][i] * (x + 1) - c[1][i];
}
return ans;
}
} t;
int n, m, a[N];
signed main(){
cin.tie(nullptr) -> sync_with_stdio(false);
cin >> n >> m;
for(int i=1; i<=n; i++) cin >> a[i], t.update(i, a[i] - a[i-1]);
while(m --> 0){
int op, x, y, k; cin >> op >> x >> y;
if(op == 1) cin >> k, t.update(x, k), t.update(y + 1, -k);
if(op == 2) cout << t.query(y) - t.query(x - 1) << "\n";
}
return 0;
}
\color{00cd00}\text{逆序对}
例题:P1908 逆序对
逆序对,就是在一个序列
可以用
你说
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5;
struct BIT{
int c[N], lowbit(int x){return x & -x;}
void update(int x, int k){while(x < N) c[x] += k, x += lowbit(x);}
int query(int x){int s = 0; while(x) s += c[x], x -= lowbit(x); return s;}
} t;
long long n, m, ans;
int a[N], rk[N];
signed main(){
cin.tie(nullptr) -> sync_with_stdio(false);
cin >> n;
for(int i=1; i<=n; i++) cin >> a[i], rk[i] = a[i];
sort(rk + 1, rk + 1 + n); int len = unique(rk + 1, rk + 1 + n) - (rk + 1);
for(int i=1; i<=n; i++) a[i] = lower_bound(rk + 1, rk + 1 + len, a[i]) - rk;
for(int i=n; i>=1; i--) ans += t.query(a[i] - 1), t.update(a[i], 1);
cout << ans;
return 0;
}
\color{00cd00}\text{二维树状数组}
和一维的树状数组类似,我们设
单点修改时,也和一维一样,
区间查询时,
如果要查询任意一个矩阵
例题:P4054 [JSOI2009] 计数问题
这题的值域只有
#include <bits/stdc++.h>
using namespace std;
const int N = 3e2 + 5;
struct BIT{ //二维树状数组
int c[N][N], lowbit(int x){return x & -x;}
void update(int x, int y, int k){
for(int i=x; i<N; i+=lowbit(i)){
for(int j=y; j<N; j+=lowbit(j)){
c[i][j] += k;
}
}
}
int query(int x, int y){
int ans = 0;
for(int i=x; i; i-=lowbit(i)){
for(int j=y; j; j-=lowbit(j)){
ans += c[i][j];
}
}
return ans;
}
int query(int x1, int y1, int x2, int y2){
return query(x2, y2) - query(x2, y1-1) - query(x1-1, y2) + query(x1-1, y1-1);
}
} t[101];
long long n, m, Q;
int a[N][N];
signed main(){
cin.tie(nullptr) -> sync_with_stdio(false);
cin >> n >> m;
for(int i=1; i<=n; i++){
for(int j=1; j<=m; j++){
cin >> a[i][j];
t[a[i][j]].update(i, j, 1);
}
}
for(cin >> Q; Q --> 0;){
int op, c; cin >> op;
if(op == 1){
int x, y; cin >> x >> y >> c;
t[a[x][y]].update(x, y, -1);
t[c].update(x, y, 1);
a[x][y] = c;
}
if(op == 2){
int x1, y1, x2, y2; cin >> x1 >> x2 >> y1 >> y2 >> c;
cout << t[c].query(x1, y1, x2, y2) << "\n";
}
}
return 0;
}
要实现矩阵修改,也和一维时的方法一样,在二维数组上差分,维护差分数组即可。二维的差分数组定义为
如果要同时实现矩阵修改和矩阵查询,也可以先差分,然后推一下式子:
于是我们用四个二维树状数组,分别维护
例题:P4514 上帝造题的七分钟
#include <bits/stdc++.h>
using namespace std;
const int N = 3e3 + 5;
struct BIT{
int c[4][N][N], lowbit(int x){return x & -x;}
void update(int x, int y, int k){
for(int i=x; i<N; i+=lowbit(i)){
for(int j=y; j<N; j+=lowbit(j)){
c[0][i][j] += k;
c[1][i][j] += k * x;
c[2][i][j] += k * y;
c[3][i][j] += k * x * y;
}
}
}
int query(int x, int y){
int ans = 0;
for(int i=x; i; i-=lowbit(i)){
for(int j=y; j; j-=lowbit(j)){
ans += c[0][i][j] * (x + 1) * (y + 1)
- c[1][i][j] * (y + 1)
- c[2][i][j] * (x + 1)
+ c[3][i][j];
}
}
return ans;
}
void update(int x1, int y1, int x2, int y2, int k){
update(x1, y1, k), update(x2+1, y1, -k), update(x1, y2+1, -k), update(x2+1, y2+1, k);
}
int query(int x1, int y1, int x2, int y2){
return query(x2, y2) - query(x2, y1-1) - query(x1-1, y2) + query(x1-1, y1-1);
}
} t;
long long n, m;
char op;
signed main(){
cin.tie(nullptr) -> sync_with_stdio(false);
cin >> op >> n >> m;
while(cin >> op){
int x1, y1, x2, y2, k;
cin >> x1 >> y1 >> x2 >> y2;
if(op == 'L') cin >> k, t.update(x1, y1, x2, y2, k);
if(op == 'k') cout << t.query(x1, y1, x2, y2) << "\n";
}
return 0;
}
\color{00cd00}\text{权值树状数组}
所谓权值数组,就是将权值作为下标,统计每种权值出现的次数。权值树状数组就是使用树状数组维护权值数组。我们在前面的“逆序对”一节中已经用到了权值树状数组,这里我们利用权值树状数组解决“查询全局第
我们需要实现以下操作:
- 在序列中加入一个数
x 。 - 在序列中删除一个数
x 。 - 查询序列中第
k 小的数是多少。
对于操作
二分法的时间复杂度为
把二分换成倍增。设
- 令
s=\operatorname{Sum}(x+1, x+2^i) 。 - 如果
sum+s<k ,就将sum\gets sum+s ,x\gets x+2^i 。
最终得到的
根据树状数组的美好性质,查询
int get_kth(int k){
int sum = 0, x = 0;
for(int i=20; i>=0; i--){
x += 1 << i;
if(x > N || sum + c[x] >= k) x -= 1 << i;
else sum += c[x];
}
return x + 1;
}
例题:P3369 【模板】普通平衡树
想不到吧,树状数组还能当平衡树用。
这题比上面还多了查询
因为需要离散化,所以只能离线下来做。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
struct BIT{
int c[N], lowbit(int x){return x & -x;}
void update(int x, int k){while(x < N) c[x] += k, x += lowbit(x);}
int query(int x){int s = 0; while(x) s += c[x], x -= lowbit(x); return s;}
int get_kth(int k){
int sum = 0, x = 0;
for(int i=20; i>=0; i--){
x += 1 << i;
if(x > N || sum + c[x] >= k) x -= 1 << i;
else sum += c[x];
}
return x + 1;
}
} t;
int n, m, rl[N];
pair<int, int> q[N];
signed main(){
cin.tie(nullptr) -> sync_with_stdio(false);
cin >> n;
for(int i=1; i<=n; i++){
auto &[opt, x] = q[i];
cin >> opt >> x;
if(opt != 4) rl[++m] = x;
}
sort(rl+1, rl+1+m), m = unique(rl+1, rl+1+m) - (rl+1);
for(int i=1; i<=n; i++){
auto [opt, x] = q[i];
if(opt != 4) x = lower_bound(rl+1, rl+1+m, x) - rl;
if(opt == 1) t.update(x, 1);
if(opt == 2) t.update(x, -1);
if(opt == 3) cout << t.query(x - 1) + 1 << "\n";
if(opt == 4) cout << rl[t.get_kth(x)] << "\n";
if(opt == 5) cout << rl[t.get_kth(t.query(x - 1))] << "\n";
if(opt == 6) cout << rl[t.get_kth(t.query(x) + 1)] << "\n";
}
return 0;
}
可以发现,用权值树状数组实现普通平衡树的代码只有约
\color{00cd00}\text{树状数组与 min/max}
需要注意的是,因为
代码就是把普通树状数组里的
struct BIT{
int c[N], lowbit(int x){return x & -x;}; BIT(){memset(c, 0x3f, sizeof(c));}
void update(int x, int k){while(x < N) c[x] = min(c[x], k), x += lowbit(x);}
int query(int x){int s = N; while(x) s = min(s, c[x]), x -= lowbit(x); return s;}
};
事实上,有一种
以上所有代码的树状数组都使用了结构体封装,大家可以直接拿来用 QwQ。