如何在 OI 中使用哈希表

First Post:

Last Update:

Word Count:
1k

Read Time:
4 min

Page View: loading...

是在某次模拟赛的时候写字符串哈希被卡常了搞出来的科技,其实挺简单的,但是好像很多人不知道?

STL

众所周知,我们有 unordered_map<> 它标准,坚如磐石,对于大多数情况来说足够快(原话来自 Martin 的哈希表基准测试),但是对于某些出题人来说还是太慢了。

直接手写

众所周知,我们有如下手写哈希表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const int MO=10000019;
struct HashMap{
int p[MO],a[MO];
int& operator[](int x){
int tmp=x%MO;
for(int i=tmp;i<MO;++i)
if(!p[i]||p[i]==x){
p[i]=x;
return(a[i]);
}
for(int i=0;i<tmp;++i)
if(!p[i]||p[i]==x){
p[i]=x;
return(a[i]);
}
}
};

这个哈希表非常适合用在字符串哈希这样,键值可以近似地看作完全随机的情况。但是只要数据不太随机(主要在数据结构方面)性能很容易迅速下降。

白嫖

众所周知,我们可以使用如 emhash 这样的第三方哈希表,跑得非常非常快,但是显然这个东西并不太能在合理时间内手写出来。

神秘科技

但是我们有一个不会退化的严格 无序关联结构。

考虑键值范围为 ,有 ,不妨假设 ,我们开两级结构,第一级结构只有一个大小为 的桶,第二级结构为 大小的桶。

对于键值拆成两部分,前 位查找第一级结构的桶,若无该键值则新开一个第二级结构桶,并令第一级结构的桶的对应位置指向该桶。

对于后 位直接在前 位对应的第二级结构桶中查询即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
flowchart TB
subgraph ton[一级]
direction LR
00[0]
01[1]
02[2]
03[3]
04[4]
05[5]
06[6]
07[7]
end
subgraph ton2[二级]
subgraph 1
10[0]
11[1]
12[2]
13[2]
end
subgraph 2
20[0]
21[1]
22[2]
23[3]
end
subgraph 3
30[0]
31[1]
32[2]
33[3]
end
subgraph 4
40[0]
41[1]
42[2]
43[3]
end
subgraph 5
50[0]
51[1]
52[2]
53[3]
end
end
01 --> 1
03 --> 4
06 --> 2
04 --> 3
02 --> 5

查询/插入/删除均为两次寻址操作,缺点是空间占用是 的(实际上需要对 的空间占用具体分析,由于简单此处略过),以及遍历是 的(虽然是有序遍历,但是实在太慢了)。

时需要

看起来非常炫酷,但其实跑不过上面提到的 emhash。

1
2
3
4
5
6
7
8
9
const int N=100010,A=7;
template<typename value>struct HashMap{
int a[1<<(32-A)],size;
value t[N][1<<A];
value& operator[](int x){
if(!a[x>>A]) a[x>>A]=++size;
return(t[a[x>>A]][x&((1<<A)-1)]);
}
};

Anything More Exciting?

显然上述方案差不多适用到 unsigned 键值,虽然大多数情况下是足够的,但是局限性还是很大。

扩域的方法我想了很久,在处理冲突上一直没有好的方案,直到我换了一个思路。

考虑键值 ,前 位与后 位分开处理,使用两个上述的哈希表。

先在第一个哈希表中查找前 位得到权值 (如果没有就赋一个新权值),然后在第二个哈希表查找后 位得到 (两个哈希表的权值独立)。

然后当前键值就被映射为了 ,再使用一个上述哈希表即可。

每次操作是 次寻址,空间占用为 ,考虑到空间占用极大(估计常数也不小),我觉得这个东西完全没有优势区间(雾)。

个人认为最多扩域到 ,拆成前 位(直接等价的转为 )与后 位,这样在 的时候可以把最后一个桶的键值值域缩小到

并且真的有用

显然,对于一些自动机,我们需要 的空间来储存,这有时候会被卡空间。于是我们把 当作键值放到上述结构中,就可以以两倍时间常数的代价换取 的空间。