CF380A题解,区间频率统计的高效解法探索

susu
《CF380A区间频率统计的高效解法探索》聚焦Codeforces经典区间统计问题,针对暴力查询易超时的痛点,系统剖析多种优化方案,莫队算法通过离线排序查询、分块处理区间,将时间复杂度优化至O(n√n),适配大规模数据场景;若数值范围有限,前缀和数组可实现O(1)单次查询,简洁高效;分块法则兼顾在线查询需求,平衡预处理与查询耗时,这些解法的对比探索,不仅为该问题提供多元最优解思路,更助力开发者深化区间统计类问题的算法选型与优化思维。

在编程竞赛的竞技场中,Codeforces(简称CF)的题目往往是算法思维的试金石,编号为380A的题目,以其对区间内元素频率统计的巧妙考察,成为初学者理解离线处理与数据结构应用的经典范例,这道题看似简单,却隐藏着从暴力到高效优化的思维跃迁,能帮助我们深刻体会算法设计的核心逻辑。 详解描述 给定一个由n个整数组成的序列a,以及m个查询,每个查询包含两个整数l和r(1≤l≤r≤n),要求针对区间[l, r]输出三个结果:

  1. 恰好出现一次的不同整数的个数;
  2. 恰好出现两次的不同整数的个数;
  3. 至少出现三次的不同整数的个数。

数据范围

  • 1 ≤ n, m ≤ 10^5
  • 序列元素值范围为1 ≤ a[i] ≤ 10^5

从数据范围可以直接判断:暴力遍历每个查询区间统计频率的O(qn)解法必然超时,我们需要寻找时间复杂度更优的方案。

CF380A题解,区间频率统计的高效解法探索

解题思路分析

核心问题拆解

要解决这个问题,关键在于高效统计每个查询区间内不同元素的出现次数,并分类计数,直接统计每个元素在区间内的次数成本过高,因此我们需要借助离线处理数据结构(如树状数组)来优化计算流程。

离线处理与贡献标记

我们可以将所有查询按右边界r排序,同步遍历序列元素,动态维护各元素的出现状态,并通过树状数组记录每个位置对不同计数类别的贡献,具体步骤如下:

  1. 预处理元素出现位置:遍历序列,用字典存储每个元素所有出现的索引位置,例如pos[x]是元素x出现的索引列表。

  2. 标记计数贡献

    • 对于元素x的第1次出现位置p1:在p1处给“恰好一次”计数数组+1;若x有第2次出现位置p2,则在p2处给“恰好一次”数组-1,同时给“恰好两次”数组+1;
    • 对于元素x的第2次出现位置p2:若x有第3次出现位置p3,则在p3处给“恰好两次”数组-1;
    • 对于元素x的第3次及以后出现位置pk:无需修改前两个计数数组,因为此时x已属于“至少三次”的类别。
  3. 统计区间不同元素个数:同样采用离线方法,遍历序列时记录每个元素最后一次出现的位置,用树状数组维护“是否为新出现元素”的标记,查询区间[l, r]的和即为该区间内不同元素的总数。

  4. 计算最终结果:对于每个查询[l, r]:

    • ans1 = “恰好一次”数组在[l, r]的区间和;
    • ans2 = “恰好两次”数组在[l, r]的区间和;
    • ans3 = 区间不同元素总数 - ans1 - ans2;

代码实现示例(C++)

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 5;
struct Query {
    int l, r, idx;
};
int a[MAXN];
vector<int> pos[MAXN];
vector<Query> qs;
int ans1[MAXN], ans2[MAXN], ans3[MAXN];
// 树状数组模板
struct FenwickTree {
    vector<int> tree;
    int n;
    FenwickTree(int size) : n(size), tree(size + 1, 0) {}
    void update(int idx, int delta) {
        while (idx <= n) {
            tree[idx] += delta;
            idx += idx & -idx;
        }
    }
    int query(int idx) {
        int res = 0;
        while (idx > 0) {
            res += tree[idx];
            idx -= idx & -idx;
        }
        return res;
    }
    int range_query(int l, int r) {
        return query(r) - query(l - 1);
    }
};
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
        pos[a[i]].push_back(i);
    }
    qs.resize(m);
    for (int i = 0; i < m; ++i) {
        cin >> qs[i].l >> qs[i].r;
        qs[i].idx = i;
    }
    // 按r排序查询,处理恰好一次和恰好两次的计数
    sort(qs.begin(), qs.end(), [](const Query& x, const Query& y) {
        return x.r < y.r;
    });
    FenwickTree ft1(n), ft2(n);
    int ptr = 0;
    for (int i = 1; i <= n; ++i) {
        int x = a[i];
        auto& p = pos[x];
        int idx = lower_bound(p.begin(), p.end(), i) - p.begin();
        if (idx == 0) {
            // 第一次出现
            ft1.update(i, 1);
            if (p.size() >= 2) {
                ft1.update(p[1], -1);
                ft2.update(p[1], 1);
            }
        } else if (idx == 1) {
            // 第二次出现
            if (p.size() >= 3) {
                ft2.update(p[2], -1);
            }
        }
        // 处理当前r=i的查询
        while (ptr < m && qs[ptr].r == i) {
            int l = qs[ptr].l, idx = qs[ptr].idx;
            ans1[idx] = ft1.range_query(l, i);
            ans2[idx] = ft2.range_query(l, i);
            ptr++;
        }
    }
    // 处理区间不同元素个数
    FenwickTree ft_distinct(n);
    vector<int> last_occur(MAXN, 0);
    ptr = 0;
    sort(qs.begin(), qs.end(), [](const Query& x, const Query& y) {
        return x.r < y.r;
    });
    for (int i = 1; i <= n; ++i) {
        int x = a[i];
        if (last_occur[x] != 0) {
            ft_distinct.update(last_occur[x], -1);
        }
        ft_distinct.update(i, 1);
        last_occur[x] = i;
        while (ptr < m && qs[ptr].r == i) {
            int l = qs[ptr].l, idx = qs[ptr].idx;
            int distinct = ft_distinct.range_query(l, i);
            ans3[idx] = distinct - ans1[idx] - ans2[idx];
            ptr++;
        }
    }
    // 输出结果
    for (int i = 0; i < m; ++i) {
        cout << ans1[i] << " " << ans2[i] << " " << ans3[i] << "\n";
    }
    return 0;
}

总结与思考

CF380A的核心考察点在于离线处理思想数据结构的灵活运用,通过将查询按右边界排序,我们可以在遍历序列的过程中动态维护状态,避免重复计算,将时间复杂度从暴力的O(qn)优化到O(n logn + q logn),完美适配大数据范围。

对于初学者而言,这道题是理解“离线处理”和“区间统计”的绝佳案例,它提醒我们,在面对大规模数据时,不能局限于直观的暴力解法,而要学会从问题本质出发,寻找状态维护的规律,借助合适的数据结构降低时间复杂度,这种思维方式不仅适用于编程竞赛,也能迁移到日常开发中的性能优化场景。

文章版权声明:除非注明,否则均为麻团原创文章,转载或复制请以超链接形式并注明出处。

目录[+]