洛谷P1972 [SDOI2009] HH 的项链

17 阅读4分钟

原题[www.luogu.com.cn/problem/P19…]

题面

P1972 [SDOI2009] HH 的项链

题目描述

HH 有一串由各种漂亮的贝壳组成的项链。HH 相信不同的贝壳会带来好运,所以每次散步完后,他都会随意取出一段贝壳,思考它们所表达的含义。HH 不断地收集新的贝壳,因此,他的项链变得越来越长。

有一天,他突然提出了一个问题:某一段贝壳中,包含了多少种不同的贝壳?这个问题很难回答……因为项链实在是太长了。于是,他只好求助睿智的你,来解决这个问题。

输入格式

一行一个正整数 nn,表示项链长度。
第二行 nn 个正整数 aia_i,表示项链中第 ii 个贝壳的种类。

第三行一个整数 mm,表示 HH 询问的个数。
接下来 mm 行,每行两个整数 l,rl,r,表示询问的区间。

输出格式

输出 mm 行,每行一个整数,依次表示询问对应的答案。

输入输出样例 #1

输入 #1

6
1 2 3 4 3 5
3
1 2
3 5
2 6

输出 #1

2
2
4

说明/提示

【数据范围】

对于 20%20\% 的数据,1n,m50001\le n,m\leq 5000
对于 40%40\% 的数据,1n,m1051\le n,m\leq 10^5
对于 60%60\% 的数据,1n,m5×1051\le n,m\leq 5\times 10^5
对于 100%100\% 的数据,1n,m,ai1061\le n,m,a_i \leq 10^61lrn1\le l \le r \le n

本题可能需要较快的读入方式,最大数据点读入数据约 20MB。

SolutionSolution

一道非常经典的查询区间不同元素个数的模板题,由于本题没有修改操作,为静态查询,可以离线操作,故使用树状数组方法来解决 (其实是我不会主席树)

首先我们要知道为什么树状数组能够处理这道题。考虑每个查询的区间 [l,r][l,r] ,对于里面的每一种出现的元素,只有其中的一个可以贡献答案。如果我们可以做到在每个时刻,出现的每种元素至多只被计数一次,那我们就可以通过维护总体和的方式来获取答案。

如何做到每种元素至多只被计数一次呢?考虑一个元素 xx ,如果它在整个序列中是第一次出现,那么我们就直接计数;如果在它前面已经出现过一次同样的元素 xx ,我们可以选择保留其中的一个 xx ,来使得计数唯一。

那么我们要选择保留哪一个 xx 呢?那就要看如何保留能使我们的区间统计更加方便。可以看出我们是在不断地从左往右计数、删除对应元素。对于这一次的更改,它会影响哪些查询区间的结果?就是那些 lprl\le p\le r 的区间,其中 pp 为当前位置。

我们不妨将全部区间按 rr 从小到大排序,然后从左到右依次处理全部区间,当当前位置 pp 更新到当前右端点 rr 时,前面的所有位置都已经被处理过,这时候我们更新当前区间的答案。

问题回到 xx 的保留上,想一想,如果此时我们保留前面的 xx ,我们不能保证那个 xx 所处的位置在当前区间 [l,r][l,r] 上,如果此时我们只统计了区间 [l,r][l,r] 的答案,就可能会造成缺漏导致答案错误。相反的,我们如果每一次都保留相对靠右的元素 xx ,那么当我们查询区间 [l,r][l,r] 时,如果对应的位置在区间内,则我们可以进行一次计数;如果对应的位置在区间的左边,则区间 [l,r][l,r] 内不可能有其他的对应元素 xx 。通过这样的操作,我们可以做到对每个区间 [l,r][l,r] 进行准确计数。

具体的,我们通过树状数组的单点修改和区间查询来实现每个区间内出现元素个数的计数,同时使用一个 preipre_i 数组来维护元素 ii 前一次出现的位置,然后迭代更新。

CodingCoding

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;
    max_r = max(max_r, q[i].r); // 记录最右边的端点
}

sort(q + 1, q + m + 1, [](auto &x, auto &y)
     { return x.r < y.r; }); // 按r排序

int now_r = 1;
fenwick_tree ft(max_r); // 构建树状数组

for (int i = 1; i <= m; i++) // 遍历每个区间求解
{
    int val;
    while (now_r <= q[i].r) // 在对当前区间求解前,需要先把前面的位置处理一遍
    {
        val = a[now_r];
        if (pre[val])
            ft.update(pre[val], -1); // 若在前面出现过,消去那个位置的计数

        pre[val] = now_r;    // 更新前一次出现位置
        ft.update(now_r, 1); // 记录当前位置的计数
        now_r++;
    }

    q[i].ans = ft.query_range(q[i].l, q[i].r);
}

sort(q + 1, q + m + 1, [](auto &x, auto &y)
     { return x.id < y.id; }); // 恢复原来查询的顺序

for (int i = 1; i <= m; i++)
    cout << q[i].ans << "\n";

总体时间复杂度:O(mlogm+nlog(maxr))O(mlogm+nlog(max_r)) ,主要在区间的排序和迭代 rr 的位置更新树状数组计数和查询上。