简介
| Resources | |||||
|---|---|---|---|---|---|
| IUSACO | 本模块以此为基础 | ||||
| CPH | 涵盖类似内容 | ||||
集合是由互不相同的元素构成的容器。集合有三种主要操作:
- 添加元素;
- 删除元素;
- 检查某个元素是否存在。
映射是由若干条目构成的容器,每个条目包含一个键和一个值。映射中的键 必须互不相同(即构成一个集合),但值可以重复。映射有三种主要操作:
- 添加指定的键值对;
- 删除键值对;
- 获取给定键对应的值。
C++ 和 Java 都有两种集合与映射的实现:一种使用排序,另一种使用哈希。 Python 的集合与映射使用哈希实现。
集合
Focus Problem – try your best to solve this problem before continuing!
View Internal Solution有序集合
有序集合按排序后的顺序存储元素。所有主要操作(添加、删除和检查)最坏情况下均以 时间运行,其中 为集合中的元素数量。
请忽略本节,因为 Python 没有实现有序集合。
哈希集合
哈希集合使用哈希存储元素。粗略来说,哈希集合由 个桶组成,每个元素通过哈希 函数映射到某个桶。如果 ,且哈希函数把每个不同元素独立、均匀随机地 映射到桶中,那么任何桶预计都不会包含很多元素,所有主要操作的期望时间都是 。
最坏情况下,Python 哈希集合每次操作可能需要与 成正比的时间。本模块稍后会演示。
Python 内置的 set 使用哈希,支持 的插入、删除和查找。对名为 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 setprint(1 in s) # Trues.remove(1) # {4, 2}print(5 in s) # Falses.remove(0) # {4, 2}# if the element to be removed does not exist, nothing happens
解答——Distinct Numbers
本题要求计算给定列表中不同值的数量。
方法一——有序集合
由于集合只存储每个值的一份副本,可以把所有数插入集合,再输出集合大小。
请忽略本节,因为 Python 没有实现有序集合。
方法二——哈希集合
n = int(input()) # unusednums = [int(x) for x in input().split()]distinct_nums = set(nums)print(len(distinct_nums))
也可以跳过列表创建,直接使用集合推导式,写得更加简洁:
n = int(input()) # unuseddistinct_nums = {int(x) for x in input().split()}print(len(distinct_nums))
不过,可以构造测试用例使上述解法以 时间运行,因此该解法无法获得 满分。
攻击数据生成器
一种极大概率规避此问题的方法是引入随机性;详情参见 这条评论。
import randomRANDOM = random.randrange(2**62)def Wrapper(x):return x ^ RANDOMn = int(input()) # unuseddistinct_nums = {Wrapper(int(x)) for x in input().split()}print(len(distinct_nums))
另一种效率较低的方法是用字符串代替整数,因为字符串的哈希函数经过随机化。
n = int(input()) # unuseddistinct_nums = set(input().split())print(len(distinct_nums))
需要担心 USACO 中的反哈希测试吗?
不需要。历史上没有 USACO 题目包含反哈希测试。不过,这类测试经常出现在 Codeforces,尤其是允许公开攻击的教育场。
方法三——排序
请参阅使用排序的题解。
映射
Focus Problem – try your best to solve this problem before continuing!
在有序映射中,键值对按键排序。与有序集合相同,所有主要操作最坏情况下均以 时间运行,其中 是映射中的键值对数量。
在哈希映射中,键值对根据键被哈希到各个桶中。与哈希集合相同,在对哈希函数作出 某些假设后,所有主要操作的期望时间均为 。
在 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]) # 5print(7 in d) # Falseprint(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 循环中修改集合(Set、Map 等)会导致
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最大可达 )。 - 快速获取任意下标
k处的值。
普通数组不可行,因为下标可能极大,无法分配足够内存。不过,所有值初始均为 0,
而且只有一小部分下标会被赋值或查询,因此可以使用映射(也称关联数组或字典),
只存储已经被赋值的下标。
- 收到
0 k v查询时,在映射中令a[k] = v。 - 收到
1 k查询时,如果映射中存在a[k]就输出它,否则输出0。
这种方法很高效,因为映射中的两种操作都很快,而且只存储实际用到的键。
注意,由于 和 可能很大,需要使用 64 位整数。
遗憾的是,直接解法无法通过几个专门让 Python dict 运行缓慢的测试点:
a = dict() # Dictionary to store assigned indicesfor _ 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 0print(a.get(nums[1], 0))
要通过所有测试,可以使用前面为 set 提到的某种规避方法。
import randomRANDOM = random.randrange(2**62)def wrap(x):return x ^ RANDOMa = dict() # Dictionary to store assigned indices
题目
其中一些题目只用排序就能解决,不过集合或映射可以让实现更简单。
| Status | Source | Problem Name | Difficulty | Tags | ||
|---|---|---|---|---|---|---|
| CSES | Easy | Show TagsMap | ||||
| Bronze | Easy | Show TagsSet | ||||
| Bronze | Medium | Show TagsSet, Simulation | ||||
| Bronze | Medium | Show TagsMap | ||||
| Bronze | Medium | Show TagsMap, Sorting | ||||
| Bronze | Medium | Show TagsMap, Set | ||||
| Silver | Medium | Show TagsMap | ||||
| CF | Medium | Show TagsPrefix Sums, Set | ||||
| Bronze | Hard | Show TagsMap, Set | ||||
| AC | Hard | Show TagsMap | ||||
| CF | Hard | Show TagsMap, Set | ||||
小测验
在大小为 的 set 中,插入、删除和查找的最坏时间复杂度是多少?