[SDOI2009] HH 的项链

GitHub跳转原题关系图返回列表

按右端点离线处理询问,用树状数组只保留每种颜色在当前前缀中的最后出现位置。

OJ: luogu

题目 ID: P1972

难度:普及+/提高

标签:离线树状数组数据结构

日期: 2026-06-22 23:16

题意

给定一个颜色序列,多次询问区间 [l,r][l,r] 中有多少种不同颜色。

思路

朴素做法是每次询问直接扫描区间,用桶统计不同颜色。

先看一个可以直接验证想法的朴素解:

cpp
#include <bits/stdc++.h>
using namespace std;

// brute.cpp:每个询问直接用桶统计不同颜色,只适合小数据。

const int MAXN = 505;
const int MAXC = 1005;

int n, m;
int a[MAXN];
int seen[MAXC];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    cin >> m;
    for (int i = 1; i <= m; i++) {
        int l, r;
        cin >> l >> r;
        int answer = 0;
        for (int c = 0; c < MAXC; c++) {
            seen[c] = 0;
        }
        for (int j = l; j <= r; j++) {
            if (!seen[a[j]]) {
                seen[a[j]] = 1;
                answer++;
            }
        }
        cout << answer << '\n';
    }

    return 0;
}

朴素做法在大数据下会超时。考虑把询问离线,按照右端点 rr 从小到大处理。

当我们已经处理到前缀 1..r1..r 时,对每种颜色只保留它最后一次出现的位置,并在这个位置放 11。这样对于询问 [l,r][l,r]

  • 如果某种颜色在 [l,r][l,r] 中出现,它在前缀 1..r1..r 的最后出现位置一定在 [l,r][l,r] 内;
  • 如果它最后出现位置小于 ll,说明它没有出现在 [l,r][l,r] 中。

所以答案就是当前树状数组中 [l,r][l,r] 的和。

每加入一个新位置 ii,如果颜色 a[i]a[i] 之前出现过,就把旧的最后位置减 11;再把 ii 位置加 11

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1000005;

struct Query {
    int l, r, id;
};

int n, m;
int a[MAXN];
int last_pos[MAXN]; // last_pos[color] 表示这种颜色最近一次出现的位置。
int bit_tree[MAXN]; // 树状数组维护每种颜色在当前前缀中的最后一次出现位置。
int answer[MAXN];
Query queries[MAXN];

bool cmp_query(const Query &x, const Query &y) {
    return x.r < y.r;
}

void add(int pos, int value) {
    while (pos <= n) {
        bit_tree[pos] += value;
        pos += pos & -pos;
    }
}

int prefix_sum(int pos) {
    int sum = 0;
    while (pos > 0) {
        sum += bit_tree[pos];
        pos -= pos & -pos;
    }
    return sum;
}

int range_sum(int l, int r) {
    return prefix_sum(r) - prefix_sum(l - 1);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    cin >> m;
    for (int i = 1; i <= m; i++) {
        cin >> queries[i].l >> queries[i].r;
        queries[i].id = i;
    }
    sort(queries + 1, queries + m + 1, cmp_query);

    int now_r = 0;
    for (int i = 1; i <= m; i++) {
        while (now_r < queries[i].r) {
            now_r++;
            // 每种颜色只在它最新出现的位置贡献 1。
            if (last_pos[a[now_r]] != 0) {
                add(last_pos[a[now_r]], -1);
            }
            add(now_r, 1);
            last_pos[a[now_r]] = now_r;
        }
        answer[queries[i].id] = range_sum(queries[i].l, queries[i].r);
    }

    for (int i = 1; i <= m; i++) {
        cout << answer[i] << '\n';
    }

    return 0;
}

复杂度

时间复杂度 O((n+m)logn)O((n+m)\log n),主要来自排序、树状数组更新和查询。

空间复杂度 O(n+m+C)O(n+m+C)CC 是颜色值范围。

总结

这题的关键是“只保留最后一次出现位置”。离线按右端点推进后,区间不同颜色数就变成了树状数组上的区间求和。