[HNOI2009] 梦幻布丁

发布时间 2023-12-09 18:04:37作者: onlyblues

[HNOI2009] 梦幻布丁

题目描述

$n$ 个布丁摆成一行,进行 $m$ 次操作。每次将某个颜色的布丁全部变成另一种颜色的,然后再询问当前一共有多少段颜色。

例如,颜色分别为 $1,2,2,1$ 的四个布丁一共有 $3$ 段颜色.

输入格式

第一行是两个整数,分别表示布丁个数 $n$ 和操作次数 $m$。  
第二行有 $n$ 个整数,第 $i$ 个整数表示第 $i$ 个布丁的颜色 $a_i$。  
接下来 $m$ 行,每行描述一次操作。每行首先有一个整数 $op$ 表示操作类型:

  • 若 $op = 1$,则后有两个整数 $x, y$,表示将颜色 $x$ 的布丁全部变成颜色 $y$。
  • 若 $op = 2$,则表示一次询问。

输出格式

对于每次询问,输出一行一个整数表示答案。

样例 #1

样例输入 #1

4 3
1 2 2 1
2
1 2 1
2

样例输出 #1

3
1

提示

样例 1 解释

初始时布丁颜色依次为 $1, 2, 2, 1$,三段颜色分别为 $[1, 1], [2, 3], [4, 4]$。  
一次操作后,布丁的颜色变为 $1, 1, 1, 1$,只有 $[1, 4]$ 一段颜色。

数据规模与约定

对于全部的测试点,保证 $1 \leq n, m \leq 10^5$,$1 \leq a_i ,x, y \leq 10^6$。

提示

请注意,不保证颜色的编号不大于 $n$,也不保证 $x \neq y$,$m$ 不是颜色的编号上限。

 

解题思路

  容易想到的暴力做法是,用 std::vector 维护各个颜色的下标有那些,对于询问的 $x$ 和 $y$,把颜色是 $x$ 的下标 $i$ 都改成 $a_i = y$,并将这些下标都存到颜色是 $y$ 的 std::vector 中,同时清空颜色是 $x$ 的 std::vector,最后枚举数组 $a$ 统计颜色段即可。

  可以发现随着询问的增加,答案会逐渐变小,这是因为每次将颜色 $x$ 修改成 $y$,颜色 $y$ 的下标会增多,颜色 $x$ 的下标清零,意味着颜色段只可能变少,不会变多。为此我们只关心当颜色是 $x$ 的下标 $i$ 变成颜色 $y$ 时,是否会减少颜色段,即如果 $a_{i - 1} = y$,$a_{i+1} = y$ 时,颜色段就会减少。

  这时就可以用启发式合并了,启发式合并的概念就是如果要合并两个集合的元素,那么应该把元素少的集合合并到元素多的集合,这样可以保证合并次数为 $O(n \log{n})$ 的级别。考虑每个元素对合并次数的贡献是多少,由于该元素只会合并到元素数量比它大的集合中,因此合并后该元素所在集合的大小至少为原来集合的两倍,假设一共有 $n$ 个元素,那么这个元素最多能合并 $O(\log{n})$ 次,因此所有元素的合并次数即总共的合并次数就是 $O(n \log{n})$ 级别。

  在这题中我们应该把颜色 $x$ 和 $y$ 中下标数量少的合并到另外一个中,如果颜色 $x$ 的 std::vector 小于颜色 $y$ 的 std::vector 直接合并即可,同时把颜色是 $x$ 的 $a_i$ 修改成 $y$。否则我们应该把颜色 $y$ 的下标合并到颜色 $x$ 中,这样做的话要把颜色是 $y$ 的 $a_i$ 修改成 $x$,这样做是没问题的,并不会影响颜色段的数量,但问题是如果下次询问的颜色是 $y$ 就会出错,因为此时 $a$ 中没有颜色 $y$,都用 $x$ 来表示了。为此我们可以维护一个数组 $c$,$c_x$ 表示颜色 $x$ 实际上用 $c_x$ 来表示。所以如果遇到颜色 $x$ 的 std::vector 大于颜色 $y$ 的 std::vector 的情况,我们应该让颜色 $x$ 用下标少的颜色 $c_y$ 来表示,为此只需交换 $c_x$ 和 $c_y$,然后把颜色是 $c_x$ 的下标合并到颜色 $c_y$ 中即可,同时把颜色是 $c_x$ 的 $a_i$ 修改成 $c_y$。

  AC 代码 时间复杂度为 $O(n \log{n})$:

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

typedef long long LL;

const int N = 1e5 + 10, M = 1e6 + 10;

int a[N];
vector<int> p[M];
int c[M];

int main() {
    int n, m;
    scanf("%d %d", &n, &m);
    int ret = 0;
    for (int i = 1; i <= n; i++) {
        scanf("%d", a + i);
        c[a[i]] = a[i];
        p[a[i]].push_back(i);
        if (a[i] != a[i - 1]) ret++;
    }
    while (m--) {
        int op;
        scanf("%d", &op);
        if (op == 1) {
            int x, y;
            scanf("%d %d", &x, &y);
            if (x == y) continue;
            if (p[c[x]].size() > p[c[y]].size()) swap(c[x], c[y]);
            for (auto &i : p[c[x]]) {
                if (a[i - 1] == c[y]) ret--;
                if (a[i + 1] == c[y]) ret--;
            }
            for (auto &i : p[c[x]]) {
                a[i] = c[y];
                p[c[y]].push_back(i);
            }
            p[c[x]].clear();
        }
        else {
            printf("%d\n", ret);
        }
    }
    
    return 0;
}

 

参考资料

  数据结构学习笔记(8) 启发式合并:https://zhuanlan.zhihu.com/p/560661911

  题解 P3201 【[HNOI2009]梦幻布丁】:https://siyuan.blog.luogu.org/solution-p3201