跳到主要内容
C++ 算法
C++ 树状数组算法详解与实战 综述由AI生成 树状数组(Fenwick Tree)是处理区间查询和单点更新的高效数据结构,时间复杂度为 O(log n)。通过对比前缀和与树状数组的差异,详细解析 lowbit 操作、update 及 getprefix 函数的实现原理。结合蓝桥杯经典例题“殷老师排队”与“异或和”,展示了树状数组在动态数组求和及子树异或查询中的具体应用,并提供了 C++ 完整代码实现与复杂度分析。
数字游民 发布于 2026/3/28 更新于 2026/6/2 18 浏览一、简介
树状数组(Fenwick Tree)是一种高效的数据结构,主要用于解决区间查询 和单点更新 问题,时间复杂度均为 O(log n),适用于数组长度固定且需要频繁进行这两种操作的场景。它是一种可以'动态求区间和'的树形数据结构,但并没有真正构造出边来,所以称为'树状的'。
以下是其核心知识点:
基本原理
结构特点:树状数组通过将数组元素按照二进制低位的'1'进行分层存储,形成一个树形结构(非传统二叉树),每个节点负责存储特定区间的前缀和。
核心操作
单点更新:修改某个元素的值,并更新其所有相关父节点。
前缀和查询:计算从数组第一个元素到指定位置的前缀和,进而推导出区间和。
二、区分与前缀和的区别和联系
1. 时间复杂度
操作 前缀和 树状数组 初始化 O(n) O(n log n) 或 O(n) 单点修改 O(n) O(log n) 区间查询 O(1) O(log n) 区间修改 O(n) (需配合差分) O(log n)
2. 空间复杂度
前缀和:O(n)(需要额外存储前缀和数组)。
树状数组:O(n)(直接在原数组基础上维护树状结构)。
3. 适用场景
前缀和:适合静态数组(不涉及修改),频繁查询区间和。例如:多次查询数组中任意子数组的和。
树状数组:适合动态数组(需频繁修改和查询)。例如:单点修改后快速查询前缀和、区间修改与查询(需配合差分)。
4. 实现灵活性
前缀和:仅支持区间和查询,难以扩展到其他操作(如区间最大值)。
树状数组:可扩展支持多种操作,如区间最大值/最小值(需调整更新策略)。
三、基本步骤演示
1. lowbit 操作
lowbit 是一种位运算操作,用于计算出数字的二进制表达中的最低位的 1 以及后面所有的 0。
int lowbit (int x) { return x & -x; }
解释:x = 6 → 二进制 0000 0110;~x + 1 = -x → 二进制 1111 1010;x & (-x) → 二进制 → 十进制 。
lowbit(x) 的值等于 2^k,其中 k 是 x 的二进制末尾连续 0 的个数。
0000 0010
2
2. lowbit 和树状数组 t[] 的联系 定义 t[i] 存储以 i 结尾,且区间大小为 lowbit(i) 的区间和。
t[j] = sum(a[j]),其中 j 的范围为 [i-lowbit(i)+1, i],可将 [i-lowbit(i)+1, i] 称为'管辖区间'。
例子 :a[1..n] 分别为 1, 2, 3, 4...n
t[1] --> 1
t[2] --> [1, 2]
t[3] --> 3
t[4] --> [1, 4]
t[5] --> 5
t[6] --> [5, 6]
t[7] --> 7
t[8] --> [1, 8]
如何进行单点修改?
假设要修改 a[3],包含 a[3] 的区间有 t[3], t[4], t[8],所以应该修改这 3 个节点。
只需进行 +lowbit 操作即可(二进制性质):3+lowbit(3)=4,4+lowbit(4)=8......就是原来的编号加上它所管辖的区域等于后面与之相连的编号。
怎么进行区间查询?
第一步:将其拆为两个区间的差。例如:假设要查询 [3, 7] 的和,就要拆分为 sum[1, 7] - sum[1, 2]。
由上图,我们要求 sum[1, 7],结果为 t[7] + t[6] + t[4],通过 lowbit 即可得到:7-lowbit(7)=6,6-lowbit(6)=4。就是原来的编号减去它所管辖的区域等于前面与之相连的编号。
于是可以直接得到树状数组最重要的两个函数 update() 和 getprefix()!
1. update 函数
void update (int k, int x) {
for (int i = k; i <= n; i += lowbit (i)) t[i] += x;
}
2. getprefix 函数
int getprefix (int k) {
int res = 0 ;
for (int i = k; i >= 1 ; i -= lowbit (i)) res += t[i];
return res;
}
四、例题详解
例题 1:蓝桥杯官网——殷老师排队
问题描述 体育课上,殷老师正在给学生们排队,排完队后他发现这些学生有些不同。每个学生都有一个魅力值 ai 和愉悦值 bi,愉悦值的计算公式如下:
bi = (i-1)ai - sum(a[1]...a[i-1]) + sum(a[i+1]...an) - (n-i)ai
作为一名优秀的教师他一眼就可以看出学生们的魅力值,但随着时间的推移魅力值可能会发生变化,所以愉悦值也会发生变化。在排队的过程中会有 n 个事件,这 n 个事件共分为两种情况:
第 x 个学生的魅力值变为 y。
殷老师想查看第 x 个学生的愉悦值。
殷老师太忙了,所以你可以帮他计算某个学生的愉悦值吗?
输入格式 第一行包含两个整数 n, m,表示学生的个数和事件的个数。
第二行包含 n 个数表示每个学生的魅力值。
之后每一个操作的开头都输入一个数字 op 表示事件的类型,如果 op=1 则输入两个整数 x, z 表示将第 x 个学生的魅力值修改为 z;若 op=2 则输入一个整数 x 表示询问从第 x 个学生的愉悦值。
输出格式 对于每个操作 2 输出一个数字 ans 表示学生的愉悦值。
样例输入 10 10
74 56 25 77 55 36 50 89 42 15
1 2 38
2 8
2 1
2 1
3 11
2 1
2 6
2 8
1 5 47
2 3
样例输出 147 -239 -239 -253 -23 161 189
数据规模 对于所有评测数据,1≤n,m≤10^5,1≤op≤2,1≤x≤n,0≤z≤10^9。
代码详解!
方法一:正确方法,树状数组 #include <iostream>
using namespace std;
using ll = long long ;
const int N = 1e5 + 9 ;
ll a[N], t[N], n, m;
int lowbit (int x) {
return x & -x;
}
void update (int k, ll x)
{
a[k] += x;
for (int i = k; i <= n; i += lowbit (i)) t[i] += x;
}
ll getprefix (int k)
{
ll res = 0 ;
for (int i = k; i >= 1 ; i -= lowbit (i)) res += t[i];
return res;
}
ll getsum (int l, int r)
{
return getprefix (r) - getprefix (l - 1 );
}
ll b (int i)
{
return (2 * i - n - 1 ) * a[i] - getsum (1 , i - 1 ) + getsum (i + 1 , n);
}
int main () {
ios::sync_with_stdio (0 ), cin.tie (0 ), cout.tie (0 );
cin >> n >> m;
for (int i = 1 ; i <= n; i++) {
int x;
cin >> x;
update (i, x);
}
while (m--) {
int op;
cin >> op;
if (op == 1 ) {
ll x, y;
cin >> x >> y;
update (x, -a[x] + y);
} else {
int x;
cin >> x;
cout << b (x) << '\n' ;
}
}
return 0 ;
}
大家可能会好奇,树状数组的相关操作和前缀和差分具有相似之处。那么这题可以用普通的前缀和和差分方法做吗?其实是可以的。只是时间复杂度较大,只能通过部分案例。
方法二:普通前缀和差分方法,时间复杂度高 #include <iostream>
#include <cstring>
using namespace std;
using ll = long long ;
const int N = 1e5 + 9 ;
ll a[N], diff[N], prefix[N], n;
void pre_fix () {
memset (prefix, 0 , sizeof (prefix));
for (int i = 1 ; i <= n; i++) prefix[i] = prefix[i - 1 ] + a[i];
}
void update (int i, int k) {
diff[i] += k;
diff[i + 1 ] -= k;
for (int i = 1 ; i <= n; i++) a[i] = a[i - 1 ] + diff[i];
pre_fix ();
}
ll getprefix (int k) {
return prefix[k];
}
ll getsum (int l, int r) {
return getprefix (r) - getprefix (l - 1 );
}
ll b (int i) {
return (2 * i - n - 1 ) * a[i] - getsum (1 , i - 1 ) + getsum (i + 1 , n);
}
int main () {
ios::sync_with_stdio (0 ), cin.tie (0 ), cout.tie (0 );
int m;
cin >> n >> m;
for (int i = 1 ; i <= n; i++) cin >> a[i];
for (int i = 1 ; i <= n; i++) diff[i] = a[i] - a[i - 1 ];
for (int i = 1 ; i <= n; i++) prefix[i] = prefix[i - 1 ] + a[i];
while (m--) {
int op;
cin >> op;
if (op == 1 ) {
int x, y;
cin >> x >> y;
update (x, y - a[x]);
} else if (op == 2 ) {
int x;
cin >> x;
cout << b (x) << '\n' ;
}
}
return 0 ;
}
例题 2:23 年蓝桥杯真题——异或和
问题描述 给一棵含有 n 个结点的有根树,根结点为 1,编号为 i 的点有点权 ai(i∈[1,n])。现在有两种操作,格式如下:
1 x y:该操作表示将点 x 的点权改为 y。
2 x:该操作表示查询以结点 x 为根的子树内的所有点的点权的异或和。
现有长度为 m 的操作序列,请对于每个第二类操作给出正确的结果。
输入格式 输入的第一行包含两个正整数 n, m,用一个空格分隔。
第二行包含 n 个整数 a1, a2,…, an,相邻整数之间使用一个空格分隔。
接下来 n−1 行,每行包含两个正整数 ui, vi,表示结点 ui 和 vi 之间有一条边。
接下来 m 行,每行包含一个操作。
输出格式
样例输入 4 4
1 2 3 4
1 2
1 3
2 4
2 1
1 1 0
2 1
2 2
样例输出
评测用例规模与约定 对于 30% 的评测用例,n, m≤1000;
对于所有评测用例,1≤n, m≤100000,0≤ai, y≤100000,1≤ui, vi, x≤n。
代码详解!
方法一:树状数组 #include <iostream>
#include <vector>
using ll = long long ;
using namespace std;
const int N = 1e5 + 9 ;
ll a[N], t[N], idx[N], dfn[N], sz[N];
vector<int > g[N];
int n, m, tot = 0 ;
int lowbit (int x) {
return x & -x;
}
void update (int k, int x) {
for (int i = k; i <= n; i += lowbit (i)) t[i] ^= x;
}
int query (int k) {
int res = 0 ;
for (int i = k; i > 0 ; i -= lowbit (i)) res ^= t[i];
return res;
}
void dfs (int x, int fa) {
dfn[x] = ++tot;
idx[dfn[x]] = x;
sz[x] = 1 ;
for (const auto & y : g[x]) {
if (y == fa) continue ;
dfs (y, x);
sz[x] += sz[y];
}
}
int main () {
int q;
cin >> n >> q;
for (int i = 1 ; i <= n; i++) cin >> a[i];
for (int i = 1 ; i < n; i++) {
int u, v;
cin >> u >> v;
g[u].push_back (v), g[v].push_back (u);
}
dfs (1 , 0 );
for (int i = 1 ; i <= n; i++) update (dfn[i], a[i]);
while (q--) {
int op;
cin >> op;
if (op == 1 )
{
int x, y;
cin >> x >> y;
int val = query (dfn[x]) ^ query (dfn[x] - 1 );
update (dfn[x], val ^ y);
}
else
{
int x;
cin >> x;
int l = dfn[x], r = dfn[x] + sz[x] - 1 ;
int ans = query (r) ^ query (l - 1 );
cout << ans << '\n' ;
}
}
return 0 ;
}
方法 2:更加简单直观的方法
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10 ;
vector<int > edge[N];
int in_[N], out[N];
int weight[N];
int tot = 0 ;
int seq[N];
int n, m;
void dfs (int u, int father)
{
in_[u] = ++tot;
seq[tot] = weight[u];
for (const auto & v : edge[u])
{
if (v == father) continue ;
dfs (v, u);
}
out[u] = tot;
}
int main () {
scanf ("%d%d" , &n, &m);
for (int i = 1 ; i <= n; i++) scanf ("%d" , &weight[i]);
for (int i = 1 ; i < n; i++)
{
int u, v;
scanf ("%d%d" , &u, &v);
edge[u].push_back (v);
edge[v].push_back (u);
}
dfs (1 , 0 );
while (m--) {
int op, x, y;
scanf ("%d" , &op);
if (op == 1 )
{
scanf ("%d%d" , &x, &y);
seq[in_[x]] = y;
}
else if (op == 2 ) {
scanf ("%d" , &x);
int ans = 0 ;
for (int i = in_[x]; i <= out[x]; i++)
{
ans = ans ^ seq[i];
}
printf ("%d\n" , ans);
}
}
return 0 ;
}
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online