洛谷 P4305 [JLOI2011] 不重复数字 题解

洛谷 P4305 [JLOI2011] 不重复数字 题解

题目描述

原题链接:[洛谷 P4305 [JLOI2011] 不重复数字 题解

给定 \(n\) 个数,要求把其中重复的去掉,只保留第一次出现的数。(\(1 \le T\le 50\),\(1 \le n \le 5 \times 10^4\),给出的数在 \(32\) 位有符号整数范围内)

题意分析

很显然是一道考察哈希表的题。

哈希 (Hash) 表

以这道题为例,如果给出的数值域较小,这道题可以使用计数排序的方法解决。但是给出的数范围太大,开不了这么大的数组,怎么办?

可以定义一个常量 mod,如果两个数对 mod 取模得到的值相同,就认为这两个数相同,这样只需要开一个大小为 mod 的数组即可。但很明显,这个算法是错误的,所以要考虑优化算法。

可以开一个 vector<int> linker[mod],将每个数字插入到下标为它对 mod 取模的位置。这样既能保证算法的正确性,又能降低代码的时间复杂度。这种数据结构就叫哈希 (Hash) 表。代码如下:

#include <iostream>
#include <cstdio>
#include <vector>
#define mod 999983
using namespace std;

inline void insert(vector<int> linker[], int x) {
    int hash = x % mod;
    for (int i = 0; i < linker[hash].size(); i++)
        if (linker[hash][i] == x) return; // 如果找到和它相同的数,则不输出
    cout << x << " ";
    linker[hash].push_back(x); // 插入这个数字
}

int T, n, x;

int main() {
    scanf("%d", &T);
    while (T--) {
        vector<int> linker[mod];
        scanf("%d", &n);
        for (int i = 0; i < n; i++) {
            scanf("%d", &x);
            insert(linker, x);
        }
        cout << endl;
    }
    return 0;
}

这里的 mod 会取一个比较大的素数,而且在空间足够的情况下越大越好。常用的 mod 有 \(10007\)、\(23333\)、\(999983\) 等。

但是这份代码提交上去之后,会有 4 个测试点 RE,所以我们还要改正算法。

算法改进

题目中说:给出的数在 \(32\) 位有符号整数范围内,也就是这个数有可能是负数。负数取模之后是非正数,但是数组没有负下标,所以会 RE。

负数对 mod 取模后得数会在 \((-mod,0]\) 区间内,加上 mod 之后,得数就会变成正数,此时再对 mod 取模就能保证得数在 \([0,mod)\) 区间内。只需要修改 insert 函数。

inline void insert(vector<int> linker[], int x) {
    int hash = (x % mod + mod) % mod; // 防止负下标越界
    for (int i = 0; i < linker[hash].size(); i++)
        if (linker[hash][i] == x) return; // 如果找到和它相同的数,则不输出
    cout << x << " ";
    linker[hash].push_back(x); // 插入这个数字
}

使用 STL 容器

没想到吧,这道题竟可以用 STL 来解!我们要使用的 STL 容器叫做 map。(需要 #include <map>)map 关联集合本质是一棵红黑树(一种平衡二叉树),可以将它看作是下标为任意类型的数组【常用的下标类型有整型(可以是负数)和字符串类型】。下面介绍 map 的用法。

  1. map <A, B> ds:建立一个名叫 ds、下标类型为 A、元素类型为 B 的数组。
  2. ds[A] = B:将数组中下标为 A 的位置的值变成 B。
  3. ds[A]:访问数组中下标为 A 的元素。注意:如果这个下标对应的元素不存在,则会自动创建下标为 A、值为默认值(所有数值类型为 \(0\)、string 类型为空串)的元素。
  4. ds.end():返回数组末尾地址。
  5. ds.find(x):查询下标 x 在数组中的地址。若这个数不存在,则返回 ds.end()。因此,这条语句经常和 ds.end() 搭配使用,用来判断某一个下标是否存在。
  6. ds.empty():判断数组是否为空。
  7. ds.size():判断数组元素个数。
  8. ds.erase(A):删除数组下标为 A 的元素。

所以,这道题就可以使用 map 轻松解决。代码实现如下:

#include <cstdio>
#include <map>
using namespace std;

int T, n, x;

int main() {
    scanf("%d", &T);
    while (T--) {
        map <int, bool> ds; // 元素为什么类型都行
        scanf("%d", &n);
        for (int i = 0; i < n; i++) {
            scanf("%d", &x);
            if (ds.find(x) != ds.end()) continue; // 如果这个数已经存在,则跳过
            printf("%d ", x);
            ds[x] = 1;
        }
        printf("\n");
    }
    return 0;
}
posted @ 2026-09-02 21:49  zzx321  阅读(21)  评论(0)    收藏  举报