PrevNext

简介

Resources
IUSACO

本模块以此为基础

CPH

涵盖类似内容

集合是由互不相同的元素构成的容器。集合有三种主要操作:

  • 添加元素;
  • 删除元素;
  • 检查某个元素是否存在。

映射是由若干条目构成的容器,每个条目包含一个和一个。映射中的键 必须互不相同(即构成一个集合),但值可以重复。映射有三种主要操作:

  • 添加指定的键值对;
  • 删除键值对;
  • 获取给定键对应的值。

C++ 和 Java 都有两种集合与映射的实现:一种使用排序,另一种使用哈希。 Python 的集合与映射使用哈希实现。

集合

Focus Problem – try your best to solve this problem before continuing!

View Internal Solution

有序集合

有序集合按排序后的顺序存储元素。所有主要操作(添加、删除和检查)最坏情况下均以 O(logN)\mathcal{O}(\log N) 时间运行,其中 NN 为集合中的元素数量。

Warning!

请忽略本节,因为 Python 没有实现有序集合。

哈希集合

哈希集合使用哈希存储元素。粗略来说,哈希集合由 BB 个桶组成,每个元素通过哈希 函数映射到某个桶。如果 BNB\approx N,且哈希函数把每个不同元素独立、均匀随机地 映射到桶中,那么任何桶预计都不会包含很多元素,所有主要操作的期望时间都是 O(1)\mathcal O(1)

Warning!

最坏情况下,Python 哈希集合每次操作可能需要与 NN 成正比的时间。本模块稍后会演示。

Python 内置的 set 使用哈希,支持 O(1)\mathcal{O}(1) 的插入、删除和查找。对名为 s 的 Python set,一些操作包括:

  • s.add(x):如果 x 尚不存在,就将其加入 s
  • s.remove(x):如果 x 存在,就将其从 s 中删除。
  • x in s:检查 s 是否包含 x
s = set()
s.add(1) # {1}
s.add(4) # {1, 4}
s.add(2) # {1, 4, 2}
s.add(1) # {1, 4, 2}
# the add method did nothing because 1 was already in the set
print(1 in s) # True
s.remove(1) # {4, 2}
print(5 in s) # False
s.remove(0) # {4, 2}
# if the element to be removed does not exist, nothing happens

解答——Distinct Numbers

本题要求计算给定列表中不同值的数量。

方法一——有序集合

由于集合只存储每个值的一份副本,可以把所有数插入集合,再输出集合大小。

Warning!

请忽略本节,因为 Python 没有实现有序集合。

方法二——哈希集合

n = int(input()) # unused
nums = [int(x) for x in input().split()]
distinct_nums = set(nums)
print(len(distinct_nums))

也可以跳过列表创建,直接使用集合推导式,写得更加简洁:

n = int(input()) # unused
distinct_nums = {int(x) for x in input().split()}
print(len(distinct_nums))

不过,可以构造测试用例使上述解法以 Θ(N2)\Theta(N^2) 时间运行,因此该解法无法获得 满分。

攻击数据生成器

一种极大概率规避此问题的方法是引入随机性;详情参见 这条评论

import random
RANDOM = random.randrange(2**62)
def Wrapper(x):
return x ^ RANDOM
n = int(input()) # unused
distinct_nums = {Wrapper(int(x)) for x in input().split()}
print(len(distinct_nums))

另一种效率较低的方法是用字符串代替整数,因为字符串的哈希函数经过随机化。

n = int(input()) # unused
distinct_nums = set(input().split())
print(len(distinct_nums))

需要担心 USACO 中的反哈希测试吗?

不需要。历史上没有 USACO 题目包含反哈希测试。不过,这类测试经常出现在 Codeforces,尤其是允许公开攻击的教育场。

方法三——排序

请参阅使用排序的题解

映射

Focus Problem – try your best to solve this problem before continuing!

在有序映射中,键值对按键排序。与有序集合相同,所有主要操作最坏情况下均以 O(logN)\mathcal{O}(\log N) 时间运行,其中 NN 是映射中的键值对数量。

在哈希映射中,键值对根据键被哈希到各个桶中。与哈希集合相同,在对哈希函数作出 某些假设后,所有主要操作的期望时间均为 O(1)\mathcal O(1)

在 Python 中,哈希映射通常称为字典(dict)

d = {}
d[1] = 5 # {1: 5}
d[3] = 14 # {1: 5, 3: 14}
d[2] = 7 # {1: 5, 2: 7, 3: 14}
del d[2] # {1: 5, 3: 14}
print(d[1]) # 5
print(7 in d) # False
print(1 in d) # True

遍历映射

遍历 dict 有三种方式,都使用 for 循环。在 Python 3.6 及以上版本中,字典按 插入顺序返回元素。可以遍历键:

for key in d:
print(key)

遍历值:

for value in d.values():
print(value)

也可以遍历键值对:

for key, value in d.items():
print(key, value)

还可以在遍历键时修改值(如果值本身可变,也可直接遍历值):

for key in d:
d[key] = 1234 # Change all values to 1234

如上所示,遍历映射时可以自由修改其中的_值_,但通常不应在遍历期间插入或删除 映射元素。

for-each 循环中修改集合(SetMap 等)会导致 ConcurrentModificationException. 。下面的代码片段给出了示例:

Map<Integer, Integer> m = new TreeMap<>();
// m starts as {0: 0, 1: 1, 2: 2}
m.put(0, 0);
m.put(1, 1);
m.put(2, 2);
for (int key : m.keySet()) {
m.remove(key); // ConcurrentModificationException thrown!!
}

一种解决方法是使用 Iterator.remove(),在循环遍历元素时删除它们,如下面 的代码片段所示:

Map<Integer, Integer> m = new TreeMap<>();
// m starts as {0: 0, 1: 1, 2: 2}
m.put(0, 0);
m.put(1, 1);
m.put(2, 2);
Iterator<Map.Entry<Integer, Integer>> iter = m.entrySet().iterator();
while (iter.hasNext()) {
int key = iter.next().getKey();
if (key == 0 || key == 2) { iter.remove(); }

不过,Iterator 超出了本模块的范围。

如果要一次删除或插入多个条目,大多数情况下最简单的方法是使用容器的 .addAll(c).removeAll(c)。也就是说,把所有要删除(或添加)的元素放入新集合,再把这个 新集合作为参数,对原集合调用 .addAll(c).removeAll(c)。下面的代码片段给出 了示例,其效果与上面的代码相同:

Map<Integer, Integer> m = new TreeMap<>();
// m starts as {0: 0, 1: 1, 2: 2}
m.put(0, 0);
m.put(1, 1);
m.put(2, 2);
Set<Integer> keysToRemove = new TreeSet<>();
for (Map.Entry<Integer, Integer> entry : m.entrySet()) {
int key = entry.getKey();
if (key == 0 || key == 2) { keysToRemove.add(key); }

解答——Associative Array

要高效解决本题,需要一种能够完成以下操作的数据结构:

  • 为任意下标 k 赋值(k 最大可达 101810^{18})。
  • 快速获取任意下标 k 处的值。

普通数组不可行,因为下标可能极大,无法分配足够内存。不过,所有值初始均为 0, 而且只有一小部分下标会被赋值或查询,因此可以使用映射(也称关联数组或字典), 只存储已经被赋值的下标。

  • 收到 0 k v 查询时,在映射中令 a[k] = v
  • 收到 1 k 查询时,如果映射中存在 a[k] 就输出它,否则输出 0

这种方法很高效,因为映射中的两种操作都很快,而且只存储实际用到的键。

注意,由于 kkvv 可能很大,需要使用 64 位整数。

遗憾的是,直接解法无法通过几个专门让 Python dict 运行缓慢的测试点:

a = dict() # Dictionary to store assigned indices
for _ in range(int(input())):
nums = list(map(int, input().split()))
if nums[0] == 0:
a[nums[1]] = nums[2]
elif nums[0] == 1:
# Print a[k] if present, else 0
print(a.get(nums[1], 0))

要通过所有测试,可以使用前面为 set 提到的某种规避方法。

import random
RANDOM = random.randrange(2**62)
def wrap(x):
return x ^ RANDOM
a = dict() # Dictionary to store assigned indices

题目

其中一些题目只用排序就能解决,不过集合或映射可以让实现更简单。

StatusSourceProblem NameDifficultyTags
CSESEasy
Show TagsMap
BronzeEasy
Show TagsSet
BronzeMedium
Show TagsSet, Simulation
BronzeMedium
Show TagsMap
BronzeMedium
Show TagsMap, Sorting
BronzeMedium
Show TagsMap, Set
SilverMedium
Show TagsMap
CFMedium
Show TagsPrefix Sums, Set
BronzeHard
Show TagsMap, Set
ACHard
Show TagsMap
CFHard
Show TagsMap, Set

小测验

在大小为 NNset 中,插入、删除和查找的最坏时间复杂度是多少?

Question 1 of 7

Module Progress:

PrevNext