树状数组仅支持单点修改+区间查询或者区间修改+单点查询。
前缀和
给定一个长度为n(n≤105)的序列 𝑎。
有很多次询问,每次询问一个区间和:
Q l r 询问 𝑎[𝑙,𝑟] 的区间和
动态前缀和
给定一个长度为 n(n≤105) 的序列𝑎(初值全为0)。
有很多次操作,每个操作可能是:
Q l r 询问𝑎[𝑙,𝑟] 的区间和
M x y 将 𝑎x 的值改为 𝑎x+y
如使用朴素的前缀和算法,每一次修改操作后需要重新扫一遍数组。
能否找出一个能够尽可能地重复利用信息的方法呢?
树状数组
每一个整数都可以表示成 2 的幂次的和的形式,
那么一个序列是否也可以表示成一系列子序列的和呢?
令 ci=ai−2k+1+ai−2k+2+⋯+ai
其中 k 表示 i 的二进制形式中末尾 0
的个数
于是 si=a1+a2+⋯+ai=ci+a1+a2+⋯+ai−2k=ci+si−2k
si−2k又可以递归地表示下去。
由于i表示成二进制只有 logi 位,所以 si 能表示
c序列中不超过 logi 项的和。
ai 的改变会影响到 ci 中的哪些项呢?
例:包含 a11010(2) 的有 c11010(2),c11100(2),c100000(2),c1000000(2)
被波及到的不超过 logn 项.
设函数 lowbit(x) 表示 x 的二进制最低位
例:28=00011100(2),lowbit (28)=00000100(2)=4。
lowbit (x)=x&(∼x+1)=x&−x
查询:
si=a1+a2+⋯+ai
=ci+ci−lowbit(i)+ci−lowbit(i)−lowbit(i−lowbit(i))+⋯
int query(int x) {
int s = 0;
for (int i = x; i; i -= i & -i)
s += c[i];
return s;
}
修改:
void modify(int x, int y) {
for (int i = x; i <= n; i += i & -i)
c[i] += y;
}
单次修改和查询操作时间复杂度均为 O(logn)
注意:要求元素下标必须从 1 开始,否则会出现死循环。
区间加法单点求值
给定一个长度为 n(n≤105) 的序列 𝑎 (初值全为0)。
有很多次操作,每个操作可能是:
- M l r k 将 𝑎[𝑙,𝑟] 每个值加上 k
- Q x 询问 ax的值
Sol:
设差分序列 b,其中 b[i]=a[i]−a[i−1]
于是序列 a 在区间 [l,r] 上的区间加法,就可以转化为序列 b 在 l−1 和
r 两个位置上的单点修改。序列 a 的单点求值就可以转化为序列 b 上的
前缀和。
用树状数组维护序列b的单点修改和求前缀和即可。
树状数组的作用
- 单点修改复杂度 O(logn)
- 计算区间和复杂度 O(logn
- 树状数组适合单个元素经常修改,而且还反复要求部分的区间的和的情况
例题:
题目: 模板】树状数组 1
Code:
#include <iostream>
#include <vector>
#include <cassert>
using namespace std;
using uint = unsigned int;
using ll = long long;
using ull = unsigned long long;
constexpr ll TEN(int n) { return (n == 0) ? 1 : 10 * TEN(n - 1); }
template <class T> using V = vector<T>;
template <class T> using W = V<V<T>>;
template <class T> struct Fenwick {
int n;
V<T> seg;
Fenwick(int _n = 0) : n(_n), seg(n + 1) {}
// 在第i个元素上+x
void add(int i, T x) {
while (i <= n) {
seg[i] += x;
i += i & -i;
}
}
// [1,i]的sum
T sum(int i) {
T s = 0;
while (i > 0) {
s += seg[i];
i -= i & -i;
}
return s;
}
// [a, b]的sum
T sum(int a, int b) { return sum(b) - sum(a); }
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
cin >> n >> q;
Fenwick<ll> fw(n);
for (int i = 1; i <= n; ++i) {
ll a;
cin >> a;
fw.add(i, a);
}
for (int i = 0; i < q; ++i) {
int t;
cin >> t;
if (t == 1) {
int p;
ll x;
cin >> p >> x;
fw.add(p, x);
}
else {
ll l, r;
cin >> l >> r;
cout << fw.sum(l - 1, r) << '\n';
}
}
return 0;
}
题目: 【模板】树状数组 2
利用差分把区间查询变成单点修改,把单点查询变成区间查询
Code:
#include <iostream>
using namespace std;
using ll = long long;
const int N = 5e5 + 10;
ll n, m;
ll c[N];
void add(ll x, ll y) {
while (x <= n) {
c[x] += y;
x += x & -x;
}
}
ll sum(ll x) {
ll s = 0;
while (x > 0) {
s += c[x];
x -= x & -x;
}
return s;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
ll pre = 0;
for (int i = 1; i <= n; ++i) {
int x;
cin >> x;
add(i, x - pre);
pre = x;
}
for (int i = 1; i <= m; ++i) {
int t;
cin >> t;
if (t == 1) {
ll x, y, k;
cin >> x >> y >> k;
add(x, k);
add(y + 1, -k);
}
else {
ll x;
cin >> x;
cout << sum(x) << '\n';
}
}
return 0;
}
二维前缀和
给定一个矩阵 𝑎 (初值全为0)。有很多次询问,每个询问形如:
- Q u d l r 询问 a[𝑢,𝑑]×[𝑙,𝑟] 的区间和
- M x y z 将 𝑎x,𝑦 的值改为 𝑎x,𝑦+z
其实,树状数组可以简单地扩展到二维,例如修改函数只需要改成
如下形式。可惜的是,二维树状数组的空间消耗较大,在算法竞赛
中不太常用。
void modify(int x, int y, int z) {
for (int i = x; i <= n; i += i & -i)
for (int j = y; j <= m; j += j & -j)
c[i][j] += z;
}
如果矩阵过大或者稀疏矩阵的话,用树套树或者动态开点的线段树来维护更好。
逆序对
给定一个序列 𝑎,称满足 1≤𝑗<𝑘≤n, ai>𝑎j 的数对 (𝑗,𝑘) 为序列 𝑎 的
逆序对。
定理:每次交换相邻的两个元素,序列中逆序对个数增加或减少1.
逆序对个数为冒泡排序过程中交换两个元素的次数。
求逆序对个数:归并排序或树状数组。
用树状数组求逆序对,只需要从后向前枚举数列中的数a[i],查询
树状数组中小于a[i]的元素个数,再在树状数组让a[i]的计数增加1。
long long ans = 0;
for (int i = n; i >= 1; --i)
ans += query(a[i] - 1), modify(a[i]);
离散化
如果想要使用树状数组求逆序对,那么空间消耗与元素的最大取值是
同级别的。当元素取值范围太大时,空间消耗便会无法承受。其实,
我们只需要知道元素之间的大小关系即可,所以可以将元素的具体值
转化为值的排名,从而将空间消耗优化为与元素个数相同。
将取值转化为排名的方法叫做离散化。
离散化有多种实现方式,时间复杂度与排序的时间复杂度相同。
通过 vector 对序列 a 进行离散化:
vector<int> v;
for (int i = 1; i <= n; ++i)
v.push_back(a[i]);
sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()), v.end());
for (int i = 1; i <= n; ++i)
a[i] = lower_bound(v.begin(), v.end(), a[i]) - v.begin() + 1;