跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

C++ 树状数组算法详解与实战

综述由AI生成树状数组(Fenwick Tree)是处理区间查询和单点更新的高效数据结构,时间复杂度为 O(log n)。通过对比前缀和与树状数组的差异,详细解析 lowbit 操作、update 及 getprefix 函数的实现原理。结合蓝桥杯经典例题“殷老师排队”与“异或和”,展示了树状数组在动态数组求和及子树异或查询中的具体应用,并提供了 C++ 完整代码实现与复杂度分析。

数字游民发布于 2026/3/28更新于 2026/6/218 浏览
C++ 树状数组算法详解与实战

一、简介

树状数组(Fenwick Tree)是一种高效的数据结构,主要用于解决区间查询和单点更新问题,时间复杂度均为 O(log n),适用于数组长度固定且需要频繁进行这两种操作的场景。它是一种可以'动态求区间和'的树形数据结构,但并没有真正构造出边来,所以称为'树状的'。

以下是其核心知识点:

  1. 基本原理
    • 结构特点:树状数组通过将数组元素按照二进制低位的'1'进行分层存储,形成一个树形结构(非传统二叉树),每个节点负责存储特定区间的前缀和。
  2. 核心操作
    • 单点更新:修改某个元素的值,并更新其所有相关父节点。
    • 前缀和查询:计算从数组第一个元素到指定位置的前缀和,进而推导出区间和。

二、区分与前缀和的区别和联系

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; } // O(1)

解释: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 函数
// 给 a[k] 增加 x
void update(int k, int x) {
    for (int i = k; i <= n; i += lowbit(i)) t[i] += x;
    // 解释:把 a[k] 增加 x,相当于把与 a[k] 相关的几个区间和都要加上 x。所以 t[i] 要加上 x
}
2. getprefix 函数
// 返回区间 [1, k] 的和
int getprefix(int k) {
    int res = 0;
    for (int i = k; i >= 1; i -= lowbit(i)) res += t[i];
    // 以管辖区间为单位依次向前加上 t[i]!!! 就返回的是区间 [1, k] 的和!
    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 个事件共分为两种情况:

  1. 第 x 个学生的魅力值变为 y。
  2. 殷老师想查看第 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
{
    a[k] += x;
    for (int i = k; i <= n; i += lowbit(i)) t[i] += x;
}

ll getprefix(int k) // 求区间 [1, k] 的和
{
    ll res = 0;
    for (int i = k; i >= 1; i -= lowbit(i)) res += t[i];
    return res;
}

ll getsum(int l, int r) // 求区间 [l, 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); // 将 a[x] 加上 -a[x]+y,即将 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];
}

// update(i,k) 将 a[i] 加上 k
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();
}

// 前缀和:求 [1, k] 的和
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
样例输出
4 5 6
评测用例规模与约定

对于 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) // 更新 x 为 y
        {
            int x, y;
            cin >> x >> y;
            int val = query(dfn[x]) ^ query(dfn[x] - 1);
            // 在初始状态(未进行任何更新操作时),query(dfn[x]) ^ query(dfn[x] - 1) 等于 a[x];
            // 但在经过更新操作后,它等于节点 x 当前的最新值
            // val 表示的是 DFS 序中某一点的当前点权值
            // query(dfn[x]) 返回区间 [1, dfn[x]] 的异或和。
            // query(dfn[x]-1) 返回区间 [1, dfn[x]-1] 的异或和。
            // 两者异或后,得到的 val 就是位置 dfn[x] 对应的点权(即节点 x 的当前值)。
            update(dfn[x], val ^ y); // 这一步的意思就是将 dfn[x] 所代表的点权(就是 val)在和 val^y 进行异或运算
            // val^val^y=0^y=y,至此,val 被替换成 y!!!
        }
        else // 求异或和
        {
            int x;
            cin >> x;
            int l = dfn[x], r = dfn[x] + sz[x] - 1;
            int ans = query(r) ^ query(l - 1); // 类似 prefix
            // ans 表示 l 到 r 的异或和
            /* 异或(XOR,符号为 ^)具有以下关键性质:
               交换律:a ^ b = b ^ a
               结合律:(a ^ b) ^ c = (a ^ b) ^ c
               自反性:a ^ a = 0(任何数异或自身为 0)
               单位元:a ^ 0 = a(任何数异或 0 等于自身)*/
            // 定义 prefix[i] 为前 i 个元素的异或和:
            // prefix[i] = a[1] ^ a[2] ^ ... ^ a[i]
            // 则区间 [l, r] 的异或和可表示为:
            // a[l] ^ a[l+1] ^ ... ^ a[r] = prefix[r] ^ prefix[l-1]
            // 前面的 1 到 l-1 部分由交换律 a[i]^a[i]=0,都抵消了
            cout << ans << '\n';
        }
    }
    return 0;
}
方法 2:更加简单直观的方法
////// 方法 2:更加直观
// 需要设置两个数组:in_[N] 和 out[N], in_[i] 表示第 i 个结点的入序,out[i] 表示第 i 个结点的出序
// 对树进行一次 dfs,在进入函数时更新入序,在退出函数时更新出序
// 对于以结点 x 为根结点的子树,其所有子结点的 dfs 序都将位于 [ in_[x], out[x] ] 间
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
vector<int> edge[N]; // edge[i] 存储第 i 个结点的邻接点
int in_[N], out[N]; // in_[i] 存储第 i 个结点的入序,out[i] 存储第 i 个结点的出序
int weight[N]; // weight[i] 存储的第 i 个结点的值 (原始数据)
int tot = 0; // 记录 dfs 序的变量
int seq[N]; // seq[i] 为 dfs 序中第 i 个结点的权值
int n, m;

void dfs(int u, int father) // 对结点 u 进行 dfs,其父结点为 father
{
    in_[u] = ++tot; // 进入函数,计算入序,tot 先自加
    seq[tot] = weight[u]; // 第 tot 个结点的权值是 weight[u]
    for (const auto& v : edge[u]) // 遍历 u 的邻接点 v
    {
        if (v == father) continue;
        dfs(v, u);
    }
    out[u] = tot; // 退出函数,更新 u 的出序,tot 无需自加
}
/* in_[u]:表示节点 u 在 DFS 遍历中首次被访问的时间戳(即进入节点 u 的时间)。
   这个时间戳是唯一的,并且在整个 DFS 过程中严格递增。
   out[u]:表示节点 u 的所有子树都被遍历完成后,退出节点 u 的时间戳。
   由于 DFS 会递归遍历当前节点的所有子节点,
   因此 out[u] 实际上等于当前节点及其所有子树的节点总数(从 in_[u] 开始的连续区间)。*/
/* 入序(in_):在递归进入节点 u 时立即记录,确保每个节点的入序唯一且递增。
   出序(out):在遍历完所有子节点后记录,此时的 tot 值恰好等于当前子树的最后一个节点的入序,
   因此 out[u] 表示子树区间的右端点。*/

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) scanf("%d", &weight[i]); // 输入 n 个结点的原始值
    for (int i = 1; i < n; i++) // 输入 n-1 条无向边
    {
        int u, v;
        scanf("%d%d", &u, &v);
        edge[u].push_back(v);
        edge[v].push_back(u);
    }
    dfs(1, 0); // 从 1 号结点开始进行一次 dfs,求出所有结点的 dfs 序,填写 in_数组和 out 数组
    // 此后我们只按照 dfs 序处理结点,原始的 weight 不再使用
    while (m--) {
        int op, x, y;
        scanf("%d", &op);
        if (op == 1) // 把第 x 个结点的权值改为 y
        {
            scanf("%d%d", &x, &y);
            seq[in_[x]] = y; // 转化为将第 in_[x] 个结点 (dfs 序下) 的权值改为 y
        }
        else if (op == 2) {
            scanf("%d", &x); // 查询以 x 为根的子树的异或和
            int ans = 0;
            for (int i = in_[x]; i <= out[x]; i++) // 转化为求区间 [ in_[x], out[x] ] 的异或和
            {
                ans = ans ^ seq[i];
            }
            printf("%d\n", ans);
            // 注意这里使用的是暴力求前缀和的方式而不是前缀和数组
            // 这是因为本题是一边修改一边查询,前缀和数组无法实现动态修改和查询
            // 如果要提高效率可以使用线段树,可在 O(log n) 时间复杂度内完成查询
            // 但对于本题,暴力求异或和即可通过全部测试点
        }
    }
    return 0;
}

目录

  1. 一、简介
  2. 二、区分与前缀和的区别和联系
  3. 1. 时间复杂度
  4. 2. 空间复杂度
  5. 3. 适用场景
  6. 4. 实现灵活性
  7. 三、基本步骤演示
  8. 1. lowbit 操作
  9. 2. lowbit 和树状数组 t[] 的联系
  10. 1. update 函数
  11. 2. getprefix 函数
  12. 四、例题详解
  13. 例题 1:蓝桥杯官网——殷老师排队
  14. 问题描述
  15. 输入格式
  16. 输出格式
  17. 样例输入
  18. 样例输出
  19. 数据规模
  20. 代码详解!
  21. 方法一:正确方法,树状数组
  22. 方法二:普通前缀和差分方法,时间复杂度高
  23. 例题 2:23 年蓝桥杯真题——异或和
  24. 问题描述
  25. 输入格式
  26. 输出格式
  27. 样例输入
  28. 样例输出
  29. 评测用例规模与约定
  30. 代码详解!
  31. 方法一:树状数组
  32. 方法 2:更加简单直观的方法
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Rust 异步微服务架构最佳实践与反模式
  • SpringBoot 源码解析:AnnotationConfigServletWebServerApplicationContext 构造方法
  • 大模型 RAG 中关键字检索的实现与实战
  • CSS 绘制圆形与三角形技巧:border 与 border-radius 实战
  • YOLO-DRONE 无人机低空巡检模型实测与电力部署解析
  • Python tkinter 核心组件 IntVar() 用法详解
  • OpenClaw 龙虾机器人 Windows 系统部署指南
  • CSS 样式基础与界面布局实战指南
  • Open WebUI MCPo 项目解析:将 MCP 工具转换为 OpenAPI 接口
  • ESP32 智能家居开发环境搭建与配置要点
  • 基于 GraphRAG 构建知识图谱增强 LLM 检索:以《红楼梦》为例
  • Whisper-WebUI 本地部署与核心功能详解
  • HTML 基础语法与常用标签详解
  • Java 集合体系与 Collection 遍历方法
  • GO 谷歌安装器.apk 一键安装包
  • Stable Diffusion 模型原理与本地部署实践
  • OpenClaw 多机器人团队协作构建指南
  • GitHub Copilot 学生认证流程与材料准备指南
  • 基于Spring Boot的微信小程序二手物品租赁系统设计与实现
  • DeepSeek-R1 大模型基于 MS-Swift 框架的部署、推理与微调实践

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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