树状数组
树状数组(Binary Indexed Tree,BIT)是一种用于维护
以前缀和为例,对于数列
显然,对于长度为
类似地,对于长度为
例题:P3374 【模板】树状数组 1
已知一个数列,需要支持两种操作:
1. 将某一个数加上;
2. 求出某区间中每一个数的和并输出。
数列长度和操作个数均不超过。
分析:如果使用朴素的做法,将这个数列保存在一个数组
另一种想法是,通过将数列存储为前缀和数组
那么有没有方法可以结合两种做法的优势,使得两个操作均使用较低的时间复杂度来完成呢?这里就可以用到树状数组。
对于任何一种数据结构,可以将其抽象为一个黑匣子:黑匣子里面存储的是数据,可以向其提供支持的操作,包括修改操作和查询操作。当向其支持查询操作时,其需要通过保存的数据计算出需要的结果然后返回;当向其提供修改操作时,黑匣子需要更新其内部的数据,来保证对于之后的查询操作,黑匣子仍能够返回正确的结果。能否解决问题取决于这个黑匣子是否能以及能以何种复杂度实现这些操作;而如何实现这样一个黑匣子,则是我们的任务。
在这个问题中,黑匣子需要维护一个数列,需要支持的有单点修改操作和区间查询操作。
和前缀和类似,树状数组每个位置保存的也是原数组中某一段区间的和。为了准确说明每个位置分别保存的是哪一段区间,首先引入一个函数 lowbit(x)
,它的值是
在常见的计算机中,有符号数采用补码表示,而在补码表示下,lowbit(x)=x&(-x)
,其中 &
为按位与。由于
x 二进制表示:0101...1000...0 -x 的反码表示:1010...0111...1 -x 的补码表示:1010...1000...0
那么,假设树状数组使用数组
这样做有什么好处呢?考虑假设已经有了这样一个数组
int query(int x) { int res = 0; while (x > 0) { res += c[x]; x -= lowbit(x); // 从大到小将需要的值求和 } return res; }
这个过程的每一步中,把一个数
接下来再考虑单点修改操作。假设修改的数是
例如,要查询
有哪些位置需要包含
。这一点很显然,因为一个位置只会包含它前面的数。 ,当且仅当 时取等号。 的值相等的位置不会包含同一个数。
综合以上的结论,可以按
首先,
下一个
再下一个
void add(int x, int y) { while (x <= n) { c[x] += y; x += lowbit(x); // 从小到大修改需要修改的位置 } }
由于
至此,我们知道树状数组可以维护一个数列,并以
#include <cstdio> typedef long long LL; const int MAXN = 5e5 + 5; LL a[MAXN]; int n, m; int lowbit(int x) { return x & -x; } LL query(int x) { LL ret = 0; while (x > 0) { ret += a[x]; x -= lowbit(x); } return ret; } void update(int x, LL d) { while (x <= n) { a[x] += d; x += lowbit(x); } } int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { int x; scanf("%d", &x); update(i, x); } while (m--) { int op, x, y; scanf("%d%d%d", &op, &x, &y); if (op == 1) update(x, y); else printf("%lld\n", query(y) - query(x - 1)); } return 0; }
例题:P3368 【模板】树状数组 2
已知一个数列,需要进行两种操作:将区间
每一个数加上 ;或者求出某一个数的值。
数列长度和操作个数均不超过。
分析:和上个问题相反,这里需要对于数列实现区间加法的修改操作和单点的查询操作。乍一看好像没法使用树状数组,但实际上只需要进行一些小处理,就能把这个问题变得和上个问题相同。
对数组进行差分操作:假设原来的数列为
#include <cstdio> const int MAXN = 5e5 + 5; int a[MAXN], n; int lowbit(int x) { return x & -x; } int query(int x) { int ret = 0; while (x > 0) { ret += a[x]; x -= lowbit(x); } return ret; } void update(int x, int d) { while (x <= n) { a[x] += d; x += lowbit(x); } } int main() { int m, pre = 0; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { int x; scanf("%d", &x); update(i, x - pre); pre = x; } while (m--) { int op; scanf("%d", &op); if (op == 1) { int x, y, k; scanf("%d%d%d", &x, &y, &k); update(x, k); update(y + 1, -k); } else { int x; scanf("%d", &x); printf("%d\n", query(x)); } } return 0; }
例题:P1908 逆序对
对于给定的一段正整数序列,逆序对就是序列中
且 的有序对。给定长度为 的正整数序列,求逆序对数。其中 。
分析:考虑朴素的做法,枚举
但是还有一个问题:数列 std::lower_bound()
,可以快速求出原数列中一个数是数列中的第几小。那么
#include <cstdio> #include <vector> #include <algorithm> using std::lower_bound; using std::sort; using std::unique; using std::vector; typedef long long LL; const int N = 5e5 + 5; int n, a[N], bit[N], bound; vector<int> data; int discretization(int x) { // 求出x是第几小 return lower_bound(data.begin(), data.end(), x) - data.begin() + 1; } int lowbit(int x) { return x & -x; } void add(int x) { while (x <= bound) { bit[x]++; x += lowbit(x); } } int query(int x) { int res = 0; while (x > 0) { res += bit[x]; x -= lowbit(x); } return res; } int main() { scanf("%d", &n); for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); data.push_back(a[i]); } // 离散化的准备工作 sort(data.begin(), data.end()); data.erase(unique(data.begin(), data.end()), data.end()); bound = data.size(); LL ans = 0; for (int i = n; i >= 1; i--) { ans += query(discretization(a[i]) - 1); add(discretization(a[i])); } printf("%lld\n", ans); }
习题:P5459 [BJOI2016] 回转寿司
给定一个长度为
的序列 ,从中选出一段连续子序列 ,使得 ,求方案数。
数据范围:。
解题思路
枚举
先预处理出前缀和数组
所以只需要找到在
参考代码
#include <cstdio> #include <algorithm> #include <vector> typedef long long LL; using std::sort; using std::lower_bound; using std::unique; using std::vector; const int N = 1e5 + 5; int a[N], bit[N * 3], bound; LL sum[N]; vector<LL> data; int discretization(LL x) { return lower_bound(data.begin(), data.end(), x) - data.begin() + 1; } int lowbit(int x) { return x & -x; } void add(int x) { while (x <= bound) { bit[x]++; x += lowbit(x); } } int query(int x) { int res = 0; while (x > 0) { res += bit[x]; x -= lowbit(x); } return res; } int main() { int n, l, r; scanf("%d%d%d", &n, &l, &r); data.push_back(0); for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); sum[i] = sum[i - 1] + a[i]; // 预处理前缀和 data.push_back(sum[i]); data.push_back(sum[i] - l); data.push_back(sum[i] - r); } sort(data.begin(), data.end()); data.erase(unique(data.begin(), data.end()), data.end()); bound = data.size(); LL ans = 0; add(discretization(0)); // sum[0]计数加1 for (int i = 1; i <= n; i++) { // 枚举右端点 int q1 = query(discretization(sum[i] - l)); int q2 = query(discretization(sum[i] - r) - 1); ans += q1 - q2; // 累加在值域范围内的方案数 add(discretization(sum[i])); // sum[i]计数加1 } printf("%lld\n", ans); return 0; }
习题:P6186 [NOI Online #1 提高组] 冒泡排序
给定一个长度为
的排列 , 个操作,需要支持两种操作:交换 和 ;查询数组经过 轮冒泡排序后的逆序对个数。
数据范围:。
解题思路
设
每经过一轮冒泡排序,若原本
由上可知,经过
针对交换操作,如果左小右大,则左边那个数对应的
参考代码
#include <cstdio> #include <algorithm> using std::swap; using std::min; typedef long long LL; const int N = 2e5 + 5; int n, p[N], f[N]; // 树状数组inv用于求一开始的f[i] // 树状数组cnt用于维护f[i]的个数的前缀和 // 树状数组sum用于维护f[i]的前缀和 LL sum[N], cnt[N], inv[N]; int lowbit(int x) { return x & -x; } void update(LL bit[], int x, int delta) { while (x <= n) { bit[x] += delta; x += lowbit(x); } } LL query(LL bit[], int x) { LL res = 0; while (x > 0) { res += bit[x]; x -= lowbit(x); } return res; } int main() { int m; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { scanf("%d", &p[i]); f[p[i]] = query(inv, n) - query(inv, p[i]); if (f[p[i]] > 0) { update(sum, f[p[i]], f[p[i]]); update(cnt, f[p[i]], 1); } update(inv, p[i], 1); } while (m--) { int t, c; scanf("%d%d", &t, &c); if (t == 1) { int i = p[c] < p[c + 1] ? p[c] : p[c + 1]; // 注意不要忘了判f[i]>0 if (f[i] > 0) { update(sum, f[i], -f[i]); update(cnt, f[i], -1); } f[i] += p[c] < p[c + 1] ? 1 : -1; if (f[i] > 0) { update(sum, f[i], f[i]); update(cnt, f[i], 1); } swap(p[c], p[c + 1]); } else { c = min(c, n - 1); // 冒泡排序n-1轮过后足够完成排序 LL ans = query(sum, n) - query(sum, c) - (query(cnt, n) - query(cnt, c)) * c; printf("%lld\n", ans); } } return 0; }
树状数组优化 DP
如果将树状数组代码中的求和改为取 max 或取 min,则树状数组可以用来维护前缀最大或最小值,从而帮助优化一些 DP 问题。
例题:P3431 [POI 2005] AUT-The Bus
在一个二维平面上给定
解题思路
若某个点为点
为了保证计算某个点
时间复杂度
参考代码
#include <cstdio> #include <algorithm> #include <vector> using ll = long long; const int K = 100005; struct Point { int x, y, p; }; Point a[K]; int k; ll c[K], dp[K]; std::vector<int> num; int discretize(int x) { return std::lower_bound(num.begin(), num.end(), x) - num.begin() + 1; } int lowbit(int x) { return x & -x; } void update(int x, ll val) { while (x <= k) { c[x] = std::max(c[x], val); x += lowbit(x); } } ll query(int x) { ll res = 0; while (x > 0) { res = std::max(res, c[x]); x -= lowbit(x); } return res; } int main() { int n, m; scanf("%d%d%d", &n, &m, &k); for (int i = 1; i <= k; i++) { scanf("%d%d%d", &a[i].x, &a[i].y, &a[i].p); num.push_back(a[i].y); } std::sort(num.begin(), num.end()); num.erase(std::unique(num.begin(), num.end()), num.end()); std::sort(a + 1, a + k + 1, [](const Point& lhs, const Point& rhs) { return lhs.x != rhs.x ? lhs.x < rhs.x : lhs.y < rhs.y; }); ll ans = 0; for (int i = 1; i <= k; i++) { a[i].y = discretize(a[i].y); dp[i] = query(a[i].y) + a[i].p; ans = std::max(ans, dp[i]); update(a[i].y, dp[i]); } printf("%lld\n", ans); return 0; }
习题:P6007 [USACO20JAN] Springboards G
在一个二维平面上给定
解题思路
设到点
而如果点
为了在点排序后能维持之前的跳板关系,可以使用索引排序。
时间复杂度
参考代码
#include <cstdio> #include <vector> #include <algorithm> using ll = long long; const int P = 200005; int n, p, x[P], y[P], idx[P], from[P]; ll c[P], dp[P]; std::vector<int> num; int discretize(int x) { return std::lower_bound(num.begin(), num.end(), x) - num.begin() + 1; } int lowbit(int x) { return x & -x; } void update(int x, ll val) { while (x <= 2 * p) { c[x] = std::min(c[x], val); x += lowbit(x); } } ll query(int x) { ll res = 2 * n; while (x > 0) { res = std::min(res, c[x]); x -= lowbit(x); } return res; } int main() { scanf("%d%d", &n, &p); for (int i = 1; i <= p; i++) { int x1, y1, x2, y2; scanf("%d%d%d%d", &x1, &y1, &x2, &y2); x[i] = x1; y[i] = y1; x[i + p] = x2; y[i + p] = y2; from[i + p] = i; num.push_back(y1); num.push_back(y2); idx[i] = i; idx[i + p] = i + p; } std::sort(num.begin(), num.end()); num.erase(std::unique(num.begin(), num.end()), num.end()); std::sort(idx + 1, idx + 2 * p + 1, [](int i, int j) { return x[i] != x[j] ? x[i] < x[j] : y[i] < y[j]; }); for (int i = 1; i <= 2 * p; i++) c[i] = 2 * n; ll ans = 2 * n; for (int i = 1; i <= 2 * p; i++) { int cur = idx[i]; if (x[cur] > n || y[cur] > n) continue; dp[cur] = x[cur] + y[cur]; // 从(0,0)直接走过来 int d = discretize(y[cur]); dp[cur] = std::min(dp[cur], query(d) + x[cur] + y[cur]); if (from[cur] != 0) { // 如果是某个跳板的右上端点 dp[cur] = std::min(dp[cur], dp[from[cur]]); } ans = std::min(ans, n - x[cur] + n - y[cur] + dp[cur]); update(d, dp[cur] - x[cur] - y[cur]); } printf("%lld\n", ans); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?