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

树状数组进阶:在线与离线操作实战,解锁区间统计新姿势

讲解树状数组进阶中的在线与离线操作思想。通过 HH 的项链和采花两道经典例题,演示如何利用离线排序结合树状数组解决区间不同元素个数及出现次数统计问题。核心在于将复杂查询转化为单点修改加区间查询模型,优化时间复杂度至 O((n+m)logn)。适合算法竞赛及笔试中处理复杂区间统计场景。

女王发布于 2026/3/27更新于 2026/7/2851 浏览
树状数组进阶:在线与离线操作实战,解锁区间统计新姿势

前言

在树状数组的学习中,我们已经掌握了一维、二维的各类基础操作(单点修改、区间查询等),但面对复杂的区间统计问题(如'查询区间内不同元素的个数'),单纯的基础操作已经难以应对。这时候,在线操作与离线操作的思想就成了破局的关键。

在线与离线并非某种具体算法,而是两种截然不同的解题思路——在线操作要求'即时响应查询',离线操作则允许'先收集所有操作,再重新排序处理'。树状数组作为高效的区间统计工具,与这两种思想结合后,能轻松解决各类复杂场景问题。

本文将从'在线/离线的核心概念'入手,结合两道经典例题(HH 的项链、采花),深度解析树状数组在离线操作中的应用,带你掌握'离线排序 + 树状数组统计'的解题模板,让复杂区间问题迎刃而解!

一、先搞懂:在线操作与离线操作的核心区别

在算法题中,所有涉及'修改'和'查询'的操作都可以按'时间轴'和'处理顺序'分为两类——在线操作和离线操作。理解它们的区别,是选择正确解题思路的前提。

1.1 核心概念拆解

(1)在线操作(Online Operation)

定义:每一个查询操作都需要'即时处理、即时输出答案',不能等待后续操作输入后再统一计算。

特点:必须动态响应,处理顺序严格遵循输入顺序,无法改变操作的执行顺序。

适用场景:修改和查询操作穿插紧密,且查询结果依赖于实时的修改状态(如动态区间求和、单点查询等基础树状数组问题)。

例子:之前学的'单点修改 + 区间查询'模板题就是典型的在线操作——每次修改后可能紧跟查询,必须立即返回结果。

(2)离线操作(Offline Operation)

定义:先将所有的修改和查询操作全部读入(收集起来),然后根据问题的特性,重新安排操作的处理顺序,最后按原查询的时间轴输出答案。

特点:不即时响应查询,允许'打乱操作顺序',通过排序、重排等方式简化计算,往往能将复杂问题转化为树状数组可处理的'单点修改 + 区间查询'模型。

适用场景:查询结果不依赖于实时修改顺序,仅依赖于最终的某个状态(如统计区间内不同元素个数、区间内出现次数≥2 的元素个数等)。

例子:后面要讲的'HH 的项链'问题——查询区间内不同贝壳的种类数,若用在线操作会超时,但用离线操作 + 树状数组可将时间复杂度优化至 O(nlogn)。

1.2 关键区别对比表

对比维度在线操作离线操作
处理顺序严格遵循输入顺序可重新排序处理
响应方式即时响应查询,即时输出统一处理后,按原顺序输出
数据依赖依赖实时修改状态依赖最终或特定阶段的状态
代码复杂度较低(直接套用模板)较高(需处理操作排序、结果映射)
时间效率通常为 O(m logn)(m 为操作数)往往更优(通过排序减少冗余计算)
适用问题基础区间统计(求和、单点查询等)复杂区间统计(不同元素数、特定出现次数统计等)

1.3 为什么离线操作能解决复杂问题?

很多复杂问题无法用在线操作直接解决,核心原因是'查询条件复杂'(如统计区间内不同元素个数)——若按在线思路,每次查询都要遍历区间,时间复杂度会达到 O(nm),对于 n=1e6、m=1e6 的场景完全超时。

而离线操作的核心优势的是:通过排序,将'复杂查询'转化为树状数组可高效处理的'单点修改 + 区间查询'模型。例如:

对于'区间内不同元素个数'问题,离线操作可按'查询右端点排序',遍历数组时用树状数组标记元素的最新出现位置,查询时直接统计区间内的标记数,即可得到不同元素的个数。

这种'化繁为简'的转化,正是离线操作的魅力所在。

二、离线操作实战 1:HH 的项链(洛谷 P1972)—— 区间不同元素个数统计

这是离线操作 + 树状数组的经典入门题,也是算法竞赛中的高频考点。通过这道题,我们能掌握离线操作的核心流程:'读入所有操作→排序查询→处理数组→树状数组统计→映射答案'。

题目链接:https://www.luogu.com.cn/problem/P1972

文章配图

2.1 题目信息

  • 来源:洛谷 P1972(SDOI2009)
  • 难度:★★★★
  • 核心考点:离线操作 + 树状数组 + 区间不同元素统计
  • 题目描述:给定一条长度为 n 的项链,项链上每个位置是一种贝壳(用整数表示种类)。有 m 个查询,每个查询给出区间 [l, r],求该区间内包含多少种不同的贝壳。
  • 输入要求:第一行 n(项链长度);第二行 n 个整数(贝壳种类,1≤a[i]≤1e6);第三行 m(查询个数);接下来 m 行,每行两个整数 l、r(1≤l≤r≤n)。
  • 输出要求:对于每个查询,输出区间 [l, r] 内不同贝壳的种类数。

2.2 问题分析(为什么在线操作不行?)

如果用在线思路:对于每个查询 [l, r],遍历区间内所有元素,用哈希表统计不同种类数。时间复杂度为 O(mn),当 n=1e6、m=1e6 时,总操作数达到 1e12,完全超时。

因此必须用离线操作,核心思路是'按查询右端点排序,遍历数组时标记元素最新出现位置,用树状数组统计区间内标记数'。

2.3 离线操作核心思路

步骤 1:转化问题 —— 不同元素个数 = 区间内'最新出现位置'的个数

对于数组中的每个元素 a[i],如果它在之前已经出现过(记上一次出现位置为 last[a[i]]),那么上一次的位置就失去了'贡献'(因为当前位置 i 是更靠右的出现位置,查询右端点≥i 时,只会统计 i 而不会统计 last[a[i]])。

因此,我们可以用一个树状数组维护'元素是否是当前区间内的最新出现位置':

遍历数组到位置 i 时,若 a[i] 之前出现过,先将树状数组中 last[a[i]] 的位置标记为 0(取消贡献);再将树状数组中位置 i 标记为 1(标记为最新出现位置);对于所有右端点为 i 的查询 [l, i],区间内不同元素的个数 = 树状数组中 [l, i] 的区间和(即标记为 1 的位置个数)。

步骤 2:离线处理流程
  1. 读入所有操作:将 m 个查询存储起来,并记录每个查询的原始编号(用于后续按原顺序输出答案);
  2. 排序查询:按查询的右端点 r 从小到大排序(因为我们要按数组顺序遍历,处理到 r 时再回答对应的查询);
  3. 按原查询顺序输出答案。

遍历数组 + 维护树状数组:

用 last 数组记录每个元素的上一次出现位置(初始为 0);遍历数组 a[1..n]: a. 若 last[a[i]]≠0,执行树状数组单点修改(last[a[i]], -1)—— 取消上一次的贡献; b. 执行树状数组单点修改(i, 1)—— 标记当前位置为最新出现位置; c. 更新 last[a[i]] = i; d. 处理所有右端点 r=i 的查询:查询树状数组中 [l, i] 的区间和,将结果存入答案数组(按原始编号映射);

2.4 完整 C++ 代码

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
#define lowbit(x) (x & -x)
const int N = 1e6 + 10; // 适配 n 和 m 的最大范围
int n, m;
int a[N]; // 项链的贝壳种类数组
int tree[N]; // 树状数组:维护位置是否为最新出现位置(1 表示是,0 表示否)
int last[N]; // 记录每个贝壳上一次出现的位置(初始为 0)
int ans[N]; // 存储最终答案,按查询原始编号排列
// 查询结构体:存储 l、r、原始编号 id
struct Query {
    int l, r, id;
} q[N];
// 排序规则:按查询的右端点 r 从小到大排序
bool cmp(Query& x, Query& y) {
    return x.r < y.r;
}
// 树状数组单点修改:位置 x 的数值增加 k(k=1 或 -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;
}
// 区间查询:查询 [l, r] 的和(不同元素个数)
int interval_query(int l, int r) {
    return query(r) - query(l - 1);
}
int main() {
    // 加速输入输出(处理 1e6 级数据必备)
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    cin >> m;
    for (int i = 1; i <= m; i++) {
        cin >> q[i].l >> q[i].r;
        q[i].id = i; // 记录原始查询编号
    }
    // 步骤 1:按查询右端点 r 排序
    sort(q + 1, q + 1 + m, cmp);
    // 步骤 2:遍历数组,维护树状数组并处理查询
    int pos = 1; // 指向当前要处理的查询(按 r 排序后的顺序)
    memset(last, 0, sizeof last); // 初始化 last 数组为 0
    for (int i = 1; i <= n; i++) {
        int current = a[i];
        // 若当前元素之前出现过,取消上一次的贡献
        if (last[current] != 0) {
            modify(last[current], -1);
        }
        // 标记当前位置为最新出现位置
        modify(i, 1);
        // 更新 last 数组
        last[current] = i;
        // 处理所有右端点 r=i 的查询
        while (pos <= m && q[pos].r == i) {
            int l = q[pos].l;
            int id = q[pos].id;
            ans[id] = interval_query(l, i);
            pos++;
        }
    }
    // 步骤 3:按原始查询顺序输出答案
    for (int i = 1; i <= m; i++) {
        cout << ans[i] << endl;
    }
    return 0;
}

2.5 代码详解

查询结构体与排序:用 Query 结构体存储查询的 l、r 和原始编号 id,按 r 排序后,保证遍历数组到 i 时,能处理所有右端点为 i 的查询; last 数组:记录每个元素的上一次出现位置,用于取消旧位置的贡献,确保树状数组中只有'最新出现位置'被标记为 1; 树状数组操作:modify 用于标记或取消标记位置,interval_query 通过前缀和差得到区间内不同元素的个数; 时间复杂度:排序查询的时间为 O(m logm),遍历数组和处理查询的时间为 O(n logn + m logn),总时间复杂度为 O((n+m) logn),可轻松处理 1e6 级数据。

三、离线操作实战 2:采花(洛谷 P4113)—— 区间内出现≥2 次的元素个数统计

这道题是'HH 的项链'的进阶版,要求统计区间内'出现次数≥2'的元素个数,进一步考验对离线操作和树状数组的灵活运用。

题目链接:https://www.luogu.com.cn/problem/P4113

文章配图

3.1 题目信息

  • 来源:洛谷 P4113(HEOI2012)
  • 难度:★★★★
  • 核心考点:离线操作 + 树状数组 + 区间特定出现次数统计
  • 题目描述:花园中有 n 朵花,每朵花有颜色(1~c)。有 m 个行程,每个行程是区间 [l, r],求该区间内'出现次数≥2'的颜色种类数(公主不喜欢只出现一次的颜色)。
  • 输入要求:第一行 n、c、m(花的个数、颜色数、行程数);第二行 n 个整数(花的颜色 x[i]);接下来 m 行,每行两个整数 l、r(1≤l≤r≤n)。
  • 输出要求:对于每个行程,输出区间 [l, r] 内出现次数≥2 的颜色种类数。

3.2 问题分析(比 HH 的项链难在哪?)

HH 的项链统计'出现≥1 次'的元素个数,而本题统计'出现≥2 次'的元素个数,核心区别在于:

元素第一次出现:无贡献; 元素第二次出现:产生 1 点贡献(此时出现次数≥2); 元素第三次及以上出现:贡献不变(仍为 1 点,因为种类数只算一次)。

因此,需要维护两个数组:

last[x]:颜色 x 上一次出现的位置; llast[x]:颜色 x 上上次出现的位置。

通过这两个数组,判断当前元素出现次数是否达到 2 次,进而更新树状数组的贡献。

3.3 离线操作核心思路

步骤 1:贡献规则定义

当颜色 x 第一次出现(last[x] = 0):无贡献,仅更新 last[x] = 当前位置 i; 当颜色 x 第二次出现(llast[x] = 0):产生 1 点贡献,将 last[x](上一次位置)标记为 1(因为查询右端点≥i 时,last[x] 在区间内则 x 出现≥2 次),更新 llast[x] = last[x],last[x] = i; 当颜色 x 第三次及以上出现(llast[x] ≠ 0):贡献不变,先取消 llast[x] 的标记(因为上上次位置已不再影响'出现≥2 次'的统计),再标记 last[x] 的位置,更新 llast[x] = last[x],last[x] = i。

步骤 2:离线处理流程
  1. 读入所有操作:存储 m 个查询,记录原始编号;
  2. 排序查询:按查询右端点 r 从小到大排序;
  3. 按原查询顺序输出答案。

遍历数组 + 维护树状数组:

用 last[x] 记录 x 上一次出现位置,llast[x] 记录 x 上上次出现位置(初始均为 0); 遍历数组 x[1..n]: a. 根据 last[x[i]] 和 llast[x[i]] 的状态,更新树状数组的标记(取消旧贡献、添加新贡献); b. 更新 last[x[i]] 和 llast[x[i]]; c. 处理所有右端点 r=i 的查询:查询区间 [l, i] 的和(即出现≥2 次的颜色种类数),存入答案数组;

3.4 完整 C++ 代码

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
#define lowbit(x) (x & -x)
const int N = 2e6 + 10; // 适配 n=2e6 的范围
int n, c, m;
int x[N]; // 花的颜色数组
int tree[N]; // 树状数组:维护颜色出现≥2 次的贡献(1 表示有贡献,0 表示无)
int last[N], llast[N]; // last[x]:x 上一次出现位置;llast[x]:x 上上次出现位置
int ans[N]; // 存储答案,按查询原始编号排列
// 查询结构体
struct Query {
    int l, r, id;
} q[N];
// 排序规则:按右端点 r 从小到大排序
bool cmp(Query& a, Query& b) {
    return a.r < b.r;
}
// 树状数组单点修改:位置 pos 的数值增加 k(k=1 或 -1)
void modify(int pos, int k) {
    for (int i = pos; i <= n; i += lowbit(i)) {
        tree[i] += k;
    }
}
// 树状数组区间查询:[1, pos] 的前缀和
int query(int pos) {
    int sum = 0;
    for (int i = pos; i > 0; i -= lowbit(i)) {
        sum += tree[i];
    }
    return sum;
}
// 区间查询:[l, r] 的和(出现≥2 次的颜色种类数)
int interval_query(int l, int r) {
    return query(r) - query(l - 1);
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin >> n >> c >> m;
    for (int i = 1; i <= n; i++) {
        cin >> x[i];
    }
    for (int i = 1; i <= m; i++) {
        cin >> q[i].l >> q[i].r;
        q[i].id = i;
    }
    // 步骤 1:按查询右端点排序
    sort(q + 1, q + 1 + m, cmp);
    // 步骤 2:遍历数组,维护树状数组和查询
    int pos = 1; // 指向当前要处理的查询
    memset(last, 0, sizeof last);
    memset(llast, 0, sizeof llast);
    for (int i = 1; i <= n; i++) {
        int color = x[i];
        if (!last[color]) { // 第一次出现:无贡献,仅更新 last
            last[color] = i;
        } else if (!llast[color]) { // 第二次出现:添加 last[color] 的贡献(1)
            modify(last[color], 1);
            llast[color] = last[color];
            last[color] = i;
        } else { // 第三次及以上出现:取消 llast[color] 的贡献,添加 last[color] 的贡献
            modify(llast[color], -1);
            modify(last[color], 1);
            llast[color] = last[color];
            last[color] = i;
        }
        // 处理所有右端点为 i 的查询
        while (pos <= m && q[pos].r == i) {
            int l = q[pos].l;
            int id = q[pos].id;
            ans[id] = interval_query(l, i);
            pos++;
        }
    }
    // 步骤 3:按原始顺序输出答案
    for (int i = 1; i <= m; i++) {
        cout << ans[i] << endl;
    }
    return 0;
}

3.5 代码详解

  1. 树状数组的作用:树状数组中 1 的位置代表'该位置是某颜色的第二次出现位置',区间和即为该区间内出现≥2 次的颜色种类数;
  2. 时间复杂度:与 HH 的项链一致,为 O((n+m) logn),可处理 2e6 级数据。

贡献维护逻辑:

第一次出现:仅记录位置,无贡献; 第二次出现:标记上一次位置为 1(此时该颜色在区间内出现≥2 次,产生贡献); 第三次及以上出现:取消上上次位置的标记(因为上上次位置已被上一次位置覆盖,不再影响'出现≥2 次'的统计),标记上一次位置为 1;

四、在线操作 vs 离线操作:如何选择?

通过两道离线例题的学习,我们可以总结出选择在线或离线操作的核心判断标准:

4.1 优先选择离线操作的场景

  1. 问题涉及'区间内不同元素个数''区间内出现次数≥k 的元素个数'等复杂统计;
  2. 在线操作时间复杂度太高(如 O(nm)),无法通过时间限制;
  3. 查询结果仅依赖于区间的某个状态(如右端点固定时的前缀状态),与操作顺序无关。

4.2 优先选择在线操作的场景

  1. 问题是基础区间统计(如区间求和、单点查询、区间修改等);
  2. 查询需要即时响应(如实时数据监控、在线系统等);
  3. 无法收集所有操作(如输入是流式数据,无法提前获取所有查询)。

4.3 离线操作的通用解题模板

对于树状数组相关的离线问题,可遵循以下模板:

  1. 收集操作:读入所有修改和查询操作,为查询记录原始编号;
  2. 排序操作:根据问题特性排序(如按查询右端点、左端点等);
  3. 遍历处理:按排序后的顺序遍历数组或操作,用树状数组维护所需状态(如标记位置、统计贡献等);
  4. 映射答案:将处理结果按查询的原始编号存入答案数组;
  5. 输出答案:按原始查询顺序输出答案。

总结

在线操作和离线操作是树状数组进阶的核心思想,尤其是离线操作,能让看似复杂的区间统计问题变得简单可解。通过本文的两道例题,我们不仅掌握了'区间不同元素个数''区间出现≥2 次元素个数'的解法,更理解了离线操作'排序 + 树状数组统计'的核心逻辑。

学习离线操作的关键在于'转化思维'——不要被问题的表面复杂度吓住,而是思考如何通过排序、重排操作,将复杂问题转化为树状数组可处理的'单点修改 + 区间查询'模型。多做类似题目(如洛谷 P3605、P4054 等),就能熟练掌握这种思维方式。

希望本文能帮助你打通树状数组的离线操作难关,在算法竞赛和笔试中应对各类复杂区间统计问题!

目录

  1. 前言
  2. 一、先搞懂:在线操作与离线操作的核心区别
  3. 1.1 核心概念拆解
  4. (1)在线操作(Online Operation)
  5. (2)离线操作(Offline Operation)
  6. 1.2 关键区别对比表
  7. 1.3 为什么离线操作能解决复杂问题?
  8. 二、离线操作实战 1:HH 的项链(洛谷 P1972)—— 区间不同元素个数统计
  9. 2.1 题目信息
  10. 2.2 问题分析(为什么在线操作不行?)
  11. 2.3 离线操作核心思路
  12. 步骤 1:转化问题 —— 不同元素个数 = 区间内“最新出现位置”的个数
  13. 步骤 2:离线处理流程
  14. 2.4 完整 C++ 代码
  15. 2.5 代码详解
  16. 三、离线操作实战 2:采花(洛谷 P4113)—— 区间内出现≥2 次的元素个数统计
  17. 3.1 题目信息
  18. 3.2 问题分析(比 HH 的项链难在哪?)
  19. 3.3 离线操作核心思路
  20. 步骤 1:贡献规则定义
  21. 步骤 2:离线处理流程
  22. 3.4 完整 C++ 代码
  23. 3.5 代码详解
  24. 四、在线操作 vs 离线操作:如何选择?
  25. 4.1 优先选择离线操作的场景
  26. 4.2 优先选择在线操作的场景
  27. 4.3 离线操作的通用解题模板
  28. 总结
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • 修复 Python 运行时报错 subprocess.CalledProcessError 缺少 c++ 编译器
  • Coze 低代码 AI 智能体平台:100 个落地场景与发布指南
  • 二级Python基本操作题填空真题与参考代码
  • Toonflow AI 短剧工厂:开源自动化短剧创作方案
  • MATLAB 实现基于多目标粒子群算法(MOPSO)的无人机三维路径规划
  • GitHub Copilot 学生身份认证与激活指南
  • Whisper v0.2 本地语音转文字工具安装与使用指南
  • Z-Image-Turbo 生成写实图像技术指南
  • Windows 安装 Python 环境与配置变量指南
  • OpenClaw QQ 机器人接入指南
  • B/S 架构核心原理与实战指南
  • 解决 NVIDIA RTX 50 系列 (sm_120) 架构下的 PyTorch 与 Unsloth 依赖冲突
  • WebAI2API 将网页版 AI 转换为 OpenAI 兼容 API 接口
  • PetaLinux 配置 Zynq-7000 PS 端外设实战指南
  • MySQL 8.0 Windows 版本压缩包安装与配置指南
  • OpenClaw、猎豹 EasyClaw 与腾讯 WorkBuddy 安装及对比指南
  • 2025 年 AI 产品经理转行指南:职业前景与能力准备
  • OpenCore 安装指南:在 PC 上运行 macOS 的完整教程
  • Java 快速开发平台对比:若依、芋道、Jeesite、JeecgBoot
  • 自然语言处理在社交媒体分析中的应用与实战

相关免费在线工具

  • 加密/解密文本

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