掌握树状数组(BIT)的基础操作后,真正的考验是将其应用到各类复杂场景中。树状数组的核心价值不仅在于单点修改和区间查询的高效实现,更在于与离散化、差分、贪心、DFS 等思想的结合,解决逆序对、图腾计数、 Promotion 统计等经典问题。 本文精选 6 道树状数组高频练习题,均来自洛谷等权威 OJ 平台,帮你拆解解题逻辑,掌握树状数组的灵活用法。刷完这 6 道题,你能彻底跳出模板的局限,真正理解树状数组的应用场景和解题技巧!下面就让我们正式开始吧!
前言
练习题 1:逆序对(洛谷 P1908)
题目信息
题目描述
核心思路
关键分析
核心步骤
完整 C++ 代码
代码详解
练习题 2:火柴排队(洛谷 P1966)
题目信息
题目描述
核心思路
步骤 1:贪心确定最优排列
步骤 2:映射与逆序对统计
完整 C++ 代码
代码详解
练习题 3:楼兰图腾(洛谷 P10589)
题目信息
题目描述
核心思路
关键分析
核心步骤
完整 C++ 代码
代码详解
练习题 4:Promotion Counting(洛谷 P3605)
题目信息
题目描述
核心思路
关键分析
核心步骤
完整 C++ 代码
代码详解
练习题 5:计数问题(洛谷 P4054)
题目信息
题目描述
核心思路
关键分析
核心步骤
完整 C++ 代码
代码详解
练习题 6:LOG(洛谷 P3586)
题目信息
题目描述
核心思路
关键分析
核心步骤
完整 C++ 代码
代码详解
6 道练习题核心总结
题型与核心技巧对应表
通用解题技巧
总结

逆序对的定义:对于给定的正整数序列,逆序对是序列中满足 ai>aj 且 i<j 的有序对。给定一个长度为 n 的序列,求序列中逆序对的数目(序列中可能有重复数字)。
输入要求:第一行输入 n(序列长度),第二行输入 n 个整数(序列元素,每个数字不超过 109)。
输出要求:输出逆序对的数目。
逆序对计数是树状数组的经典应用,直接暴力枚举(时间复杂度 O(n2))会超时,树状数组可将时间复杂度优化至 O(nlogn)。
对于序列中第 i 个元素 a[i],我们需要统计前 i−1 个元素中比 a[i] 大的元素个数,所有位置的统计结果之和即为逆序对总数。
query(max_rank) - query(rank[a[i]]),其中 max_rank 是离散化后的最大排名);#include <iostream>
#include <algorithm>
#include <unordered_map>
using namespace std;
#define lowbit(x) (x & -x)
typedef long long LL;
const int N = 5e5 + 10; // 适配n的最大范围
int n;
int a[N], t[N]; // a存储原序列,t存储用于离散化的序列
int m; // 离散化后的最大排名
unordered_map<int, int> mp; // 映射:元素值 -> 离散化后的排名
LL tree[N]; // 树状数组,维护元素出现次数
// 单点修改:将排名x的元素计数+1
void modify(int x) {
for (int i = x; i <= m; i += lowbit(i)) {
tree[i] += 1;
}
}
// 区间查询:查询[1, x]的元素个数(即排名≤x的元素个数)
LL query(int x) {
LL sum = 0;
for (int i = x; i > 0; i -= lowbit(i)) {
sum += tree[i];
}
return sum;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
t[i] = a[i]; // 复制序列用于离散化
}
// 步骤1:离散化(去重+排序+映射)
sort(t + 1, t + 1 + n); // 排序
for (int i = 1; i <= n; i++) {
if (!mp.count(t[i])) { // 去重,避免重复映射
mp[t[i]] = ++m;
}
}
// 步骤2:遍历序列,统计逆序对
LL ans = 0;
for (int i = 1; i <= n; i++) {
int rank = mp[a[i]]; // 当前元素的离散化排名
// 前i-1个元素中比a[i]大的个数 = 总元素个数 - 排名≤rank的个数
ans += query(m) - query(rank);
modify(rank); // 插入当前元素的排名
}
cout << ans << endl;
return 0;
}t 数组,对 t 排序后去重,用 unordered_map 建立元素值到排名的映射,确保相同元素映射到同一排名,不同元素映射到不同排名;modify(x):将排名 x 的元素计数 + 1,用于记录元素的出现;query(x):查询排名≤x 的元素总数,通过 query(m) - query(rank) 得到排名 > rank 的元素个数(即比当前元素大的个数)。long long 存储。
两盒火柴各有 n 根,每根火柴有高度。两列火柴的距离定义为 ∑i=1n(ai−bi)2(ai 是第一列第 i 根火柴的高度,bi 是第二列第 i 根火柴的高度)。通过交换同一列中相邻火柴的位置,使两列火柴的距离最小,求最少交换次数(结果对 108−3 取模)。
输入要求:第一行输入 n,第二行输入第一列火柴的高度,第三行输入第二列火柴的高度(两列火柴高度均互不相同)。
输出要求:输出最少交换次数对 108−3 取模的结果。
本题需分两步解决:先确定 “距离最小” 的火柴排列方式,再计算该排列所需的最少交换次数。
根据数学推导,当两列火柴的高度按相同大小顺序对应时,距离最小。证明如下:
因此,最优方案是:将两列火柴分别排序,使第一列第 k 小的火柴对应第二列第 k 小的火柴。
最少交换次数等价于 “将一个数组调整为目标顺序所需的相邻交换次数”,该次数等于目标数组的逆序对个数(因为每次相邻交换可消除一个逆序对)。
具体实现:
#include <iostream>
#include <algorithm>
using namespace std;
#define lowbit(x) (x & -x)
typedef long long LL;
const int N = 1e5 + 10;
const int mod = 1e8 - 3; // 取模常数
int n;
// 结构体:存储火柴高度和原下标
struct Node {
int x; // 高度
int id; // 原下标
} a[N], b[N];
int c[N]; // 映射数组:c[a的原下标] = b的原下标
LL tree[N]; // 树状数组,维护逆序对统计
// 排序规则:按高度从小到大排序
bool cmp(Node& x, Node& y) {
return x.x < y.x;
}
// 单点修改:将位置x的计数+1
void modify(int x, int k) {
for (int i = x; i <= n; i += lowbit(i)) {
tree[i] += k;
}
}
// 区间查询:查询[1, x]的元素个数
LL query(int x) {
LL sum = 0;
for (int i = x; i > 0; i -= lowbit(i)) {
sum += tree[i];
}
return sum;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
// 读入第一列火柴的高度和原下标
for (int i = 1; i <= n; i++) {
cin >> a[i].x;
a[i].id = i;
}
// 读入第二列火柴的高度和原下标
for (int i = 1; i <= n; i++) {
cin >> b[i].x;
b[i].id = i;
}
// 步骤1:按高度排序,建立映射关系
sort(a + 1, a + 1 + n, cmp);
sort(b + 1, b + 1 + n, cmp);
for (int i = 1; i <= n; i++) {
// a排序后的第i个元素的原下标,对应b排序后的第i个元素的原下标
c[a[i].id] = b[i].id;
}
// 步骤2:统计数组c的逆序对个数
LL ans = 0;
for (int i = 1; i <= n; i++) {
// 逆序对个数 = 已插入元素个数 - 比c[i]小的元素个数
ans += query(n) - query(c[i]);
ans %= mod; // 防止溢出,及时取模
modify(c[i], 1); // 插入当前元素
}
cout << ans % mod << endl;
return 0;
}Node 结构体存储火柴的高度和原下标,排序后可保留原位置信息;c[a[i].id] = b[i].id 建立原下标之间的对应关系,数组 c 的有序化过程即为原火柴列的最优交换过程;
给定 n 个点,坐标为 (1,y1),(2,y2),...,(n,yn)(y1∼yn 是 1∼n 的一个排列,即水平和竖直位置均互不相同)。定义两种图腾:
求这两种图腾的个数。
输入要求:第一行输入 n,第二行输入 n 个整数 y1∼yn。
输出要求:输出两个整数,分别为 V 图腾和∧图腾的个数。
对于每个点 j(中间点),统计其左侧和右侧满足条件的点的个数,两者相乘即为该点对对应图腾的贡献,所有点的贡献之和即为图腾总数。
up[j](左侧比 yj 大的个数)和 down[j](左侧比 yj 小的个数);#include <iostream>
#include <cstring>
using namespace std;
#define lowbit(x) (x & -x)
typedef long long LL;
const int N = 2e5 + 10;
int n;
int a[N]; // 存储y数组(a[j] = y_j)
LL up[N], down[N]; // up[j]:左侧比a[j]大的个数;down[j]:左侧比a[j]小的个数
LL tree[N]; // 权值树状数组
// 单点修改:将值x的点标记为已出现(计数+1)
void modify(int x, int k) {
for (int i = x; i <= n; i += lowbit(i)) {
tree[i] += k;
}
}
// 区间查询:查询[1, x]的点个数(即值≤x的点个数)
LL query(int x) {
LL sum = 0;
for (int i = x; i > 0; i -= lowbit(i)) {
sum += tree[i];
}
return sum;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
// 第一次遍历:左到右,统计左侧比a[i]大、小的个数
memset(tree, 0, sizeof tree);
for (int i = 1; i <= n; i++) {
int val = a[i];
down[i] = query(val - 1); // 左侧比val小的个数:[1, val-1]的和
up[i] = query(n) - query(val); // 左侧比val大的个数:总个数 - [1, val]的和
modify(val, 1); // 标记当前点已出现
}
// 第二次遍历:右到左,统计右侧比a[i]大、小的个数,计算贡献
memset(tree, 0, sizeof tree);
LL v_tot = 0, lambda_tot = 0;
for (int i = n; i >= 1; i--) {
int val = a[i];
LL right_down = query(val - 1); // 右侧比val小的个数
LL right_up = query(n) - query(val); // 右侧比val大的个数
v_tot += up[i] * right_up; // V图腾贡献:左大 × 右大
lambda_tot += down[i] * right_down; // ∧图腾贡献:左小 × 右小
modify(val, 1); // 标记当前点已出现
}
cout << v_tot << " " << lambda_tot << endl;
return 0;
}
公司组织成一棵树,1 号奶牛为总裁(根节点),每个奶牛有唯一的能力值 pi。对于每头奶牛 i,求其下属(所有后代节点)中能力值大于 pi 的奶牛个数。
输入要求:第一行输入 n,接下来 n 行输入 p1∼pn(能力值,互不相同),再接下来 n−1 行输入奶牛 2∼n 的上司编号。
输出要求:输出 n 行,第 i 行输出奶牛 i 的下属中能力值更高的个数。
这是一道树状数组与 DFS 结合的经典题,核心是用 “树上差分” 的思想,通过 DFS 遍历树的过程中维护树状数组,统计子树内的符合条件的节点个数。
ret[u](初始为负,用于后续计算差值);#include <iostream>
#include <algorithm>
#include <unordered_map>
#include <vector>
using namespace std;
#define lowbit(x) (x & -x)
const int N = 1e5 + 10;
int n;
int p[N], t[N]; // p存储能力值,t存储用于离散化的能力值
unordered_map<int, int> mp; // 映射:能力值 -> 离散化排名(值越大,排名越高)
vector<int> edges[N]; // 邻接表:edges[fa]存储fa的下属
int ret[N]; // 存储每个节点的答案
int tree[N]; // 树状数组,维护排名的出现次数
// 单点修改:将排名x的节点计数+1
void modify(int x, int k) {
for (int i = x; i <= n; i += lowbit(i)) {
tree[i] += k;
}
}
// 区间查询:查询[1, x]的节点个数
int query(int x) {
int sum = 0;
for (int i = x; i > 0; i -= lowbit(i)) {
sum += tree[i];
}
return sum;
}
// DFS遍历树
void dfs(int u) {
int rank = p[u]; // 当前节点的能力值排名
// 进入节点u:查询当前树状数组中排名>rank的节点个数(即已插入的非子树节点)
ret[u] -= query(n) - query(rank);
// 递归遍历子节点
for (auto v : edges[u]) {
dfs(v);
}
// 退出节点u:查询当前树状数组中排名>rank的节点个数(子树节点已全部插入)
ret[u] += query(n) - query(rank);
// 将当前节点的排名插入树状数组
modify(rank, 1);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
// 读入能力值
for (int i = 1; i <= n; i++) {
cin >> p[i];
t[i] = p[i];
}
// 读入树的结构(下属→上司)
for (int i = 2; i <= n; i++) {
int fa;
cin >> fa;
edges[fa].push_back(i);
}
// 离散化能力值(值越大,排名越高)
sort(t + 1, t + 1 + n);
for (int i = 1; i <= n; i++) {
mp[t[i]] = i;
}
for (int i = 1; i <= n; i++) {
p[i] = mp[p[i]];
}
// DFS遍历统计答案
dfs(1);
// 输出结果
for (int i = 1; i <= n; i++) {
cout << ret[i] << endl;
}
return 0;
}query(n) - query(rank));
给定 n×m 的方格,每个格子有一个整数权值。支持两种操作:
x y c,将格子 (x,y) 的权值改为 c;x1 x2 y1 y2 c,查询子矩阵 [x1,x2]×[y1,y2] 中权值为 c 的格子个数。输入要求:第一行输入 n,m,接下来 n 行输入初始权值,再输入 Q(操作次数),接下来 Q 行输入操作。
输出要求:对于每个操作 2,输出对应的格子个数。
本题需要统计子矩阵中特定权值的出现次数,核心是用 “多个二维树状数组” 分别维护每个权值的位置分布。
modify(x,y,-1),再在新权值 c 的树状数组中执行 modify(x,y,1),更新格子的权值;#include <iostream>
using namespace std;
#define lowbit(x) (x & -x)
const int N = 310; // 方格最大尺寸
const int M = 110; // 权值最大范围(1~100)
int n, m;
int a[N][N]; // 存储每个格子的当前权值
// 二维树状数组结构体
struct BIT2D {
int tree[N][N];
// 单点修改:(x,y)位置的计数±1
void modify(int x, int y, int k) {
for (int i = x; i <= n; i += lowbit(i)) {
for (int j = y; j <= m; j += lowbit(j)) {
tree[i][j] += k;
}
}
}
// 区间查询:查询(1,1)到(x,y)的和
int query(int x, int y) {
int sum = 0;
for (int i = x; i > 0; i -= lowbit(i)) {
for (int j = y; j > 0; j -= lowbit(j)) {
sum += tree[i][j];
}
}
return sum;
}
} bit[M]; // bit[c]:维护权值为c的格子分布
// 子矩阵查询:(x1,y1)到(x2,y2)的和(容斥原理)
int matrix_query(int c, int x1, int x2, int y1, int y2) {
return bit[c].query(x2, y2) - bit[c].query(x1 - 1, y2) -
bit[c].query(x2, y1 - 1) + bit[c].query(x1 - 1, y1 - 1);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
// 初始化:为每个格子的初始权值标记
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> a[i][j];
int c = a[i][j];
bit[c].modify(i, j, 1);
}
}
int Q;
cin >> Q;
while (Q--) {
int op;
cin >> op;
if (op == 1) {
// 操作1:修改格子(x,y)的权值为c
int x, y, c;
cin >> x >> y >> c;
int old_c = a[x][y];
// 从原权值的树状数组中删除
bit[old_c].modify(x, y, -1);
// 加入新权值的树状数组
bit[c].modify(x, y, 1);
// 更新当前格子的权值
a[x][y] = c;
} else {
// 操作2:查询子矩阵[x1,x2]×[y1,y2]中权值为c的个数
int x1, x2, y1, y2, c;
cin >> x1 >> x2 >> y1 >> y2 >> c;
cout << matrix_query(c, x1, x2, y1, y2) << endl;
}
}
return 0;
}query(x2,y2) - query(x1-1,y2) - query(x2,y1-1) + query(x1-1,y1-1) 计算,避免重复或遗漏。
维护一个长度为 n 的序列(初始全为 0),支持两种操作:
U k a,将序列中第 k 个数修改为 a;Z c s,每次选出 c 个正数,将它们各减 1,询问能否进行 s 次这样的操作(每次查询独立,不修改序列)。输入要求:第一行输入 n,m(序列长度和操作次数),接下来 m 行输入操作。
输出要求:对于每个操作 Z,输出 “TAK”(可行)或 “NIE”(不可行)。
本题的关键是将 “能否进行 s 次操作” 转化为可通过树状数组统计的条件,核心是问题转化和数学推导。
将序列中的每个数看作 “石子堆的高度”,操作 Z 的本质是:能否通过 s 次操作,每次从 c 个堆中各取 1 个石子,最终取完所有需要取的石子。
转化为数学条件:
#include <iostream>
#include <algorithm>
#include <unordered_map>
using namespace std;
#define lowbit(x) (x & -x)
typedef long long LL;
const int N = 1e6 + 10;
int n, m;
// 存储所有操作
struct Op {
char op;
int x, y; // U: x=k, y=a; Z: x=c, y=s
} op[N];
int t[N]; // 存储所有需要离散化的值(a和s)
int a[N]; // 存储序列当前值(初始为0)
unordered_map<int, int> mp; // 映射:原始值 -> 离散化排名
// 树状数组结构体
struct BIT {
LL tree[N];
void modify(int x, LL k) {
for (int i = x; i <= m; i += lowbit(i)) {
tree[i] += k;
}
}
LL query(int x) {
LL sum = 0;
for (int i = x; i > 0; i -= lowbit(i)) {
sum += tree[i];
}
return sum;
}
} A, B; // A维护总和,B维护个数
int main() {
// 用scanf加速输入(数据量较大)
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
scanf(" %c", &op[i].op);
scanf("%d%d", &op[i].x, &op[i].y);
t[i] = op[i].y; // 收集所有a和s的值,用于离散化
}
// 步骤1:离散化所有需要用到的值
sort(t + 1, t + 1 + m);
int max_rank = 0;
for (int i = 1; i <= m; i++) {
if (!mp.count(t[i])) {
mp[t[i]] = ++max_rank;
}
}
// 步骤2:处理所有操作
for (int i = 1; i <= m; i++) {
if (op[i].op == 'U') {
// 操作U:修改第k个数为a
int k = op[i].x;
int new_val = op[i].y;
int old_val = a[k];
// 1. 删除原数值的贡献
if (old_val != 0) {
int r = mp[old_val];
A.modify(r, -old_val);
B.modify(r, -1);
}
// 2. 添加新数值的贡献
if (new_val != 0) {
int r = mp[new_val];
A.modify(r, new_val);
B.modify(r, 1);
}
// 3. 更新序列当前值
a[k] = new_val;
} else {
// 操作Z:查询能否进行s次操作
int c = op[i].x;
int s = op[i].y;
// 计算所需统计量
LL total = A.query(max_rank); // 总元素总和
if (total < (LL)c * s) { // 总石子数不足,直接不可行
puts("NIE");
continue;
}
// 离散化s,找到其排名
int rank_s = mp.count(s) ? mp[s] : 0;
// cnt:≥s的元素个数 = 总个数 - <s的个数
LL cnt = B.query(max_rank) - B.query(rank_s - 1);
// sum_less:<s的元素总和
LL sum_less = A.query(rank_s - 1);
// 检查条件:sum_less ≥ max(0, c - cnt) * s
LL required = (LL)max(0, c - (int)cnt) * s;
if (sum_less >= required) {
puts("TAK");
} else {
puts("NIE");
}
}
}
return 0;
}题号 | 题型 | 核心技巧 | 树状数组作用 |
|---|---|---|---|
1 | 逆序对计数 | 离散化 + 权值统计 | 统计前缀中特定范围的元素个数 |
2 | 火柴排队 | 贪心映射 + 逆序对 | 统计数组有序化所需的交换次数 |
3 | 楼兰图腾 | 双向遍历 + 权值统计 | 统计左右两侧特定大小关系的元素个数 |
4 | Promotion Counting | DFS + 树上差分 | 统计子树内特定条件的节点个数 |
5 | 计数问题 | 多二维树状数组 | 维护不同权值的位置分布,统计子矩阵个数 |
6 | LOG | 问题转化 + 双树状数组 | 统计总和、个数,验证可行性条件 |
long long存储统计结果和中间变量。通过以上这些练习题的训练,相信大家已经能熟练运用树状数组的基本原理了。希望大家能多多练习加以巩固~