1709. 哈希表判重

本题使用较为稳定的伪随机数生成 (mt19937生成器),所以生成的数据是比较均匀的伪随机数。

不同哈希策略的比较(仅供参考):

设哈希函数为 H(x)=x\bmod len ,即除留余数法

使用开放定址法,并使用线性探测法 key=key+d_i\bmod m ,设 d_i=2333

一:实践检验 len 的影响

len10^7 ,冲突率过高, TLE

len2\times 10^7 或其附近的素数,或更大的数字,用时均为 4.2s

二:实践检验 d_i 的影响

len 固定为 2\times10^7+3 ,已知 d_i=2333 ( 2333 是质数)时表现为 4.2s

更改 d_i1 , TLE

更改 d_i2332 ,用时为 4.1s

更改 d_i1,2,3,\cdots , TLE

更改线性探测法为幂次方探测法:

更改 d_i1^2,2^2,3^2,\cdots ,用时为 2.8s

更改 d_i1^3,2^3,3^3,\dots ,用时为 1.3s

更改 d_i1^4,2^4,3^4,\dots ,用时为 0.985s

继续提升 d_i1^5,2^5,3^5,\cdots 等继续升幂,没有明显改进,甚至可能变慢。

更改 d_i1^2,-1^2,2^2,-2^2,\cdots ,用时为 0.76s (二次探测法)

更改 d_i1^3,-1^3,2^3,-2^3,\cdots ,用时没有改善

三:实践检验拉链法

使用 vector 拉链,用时为 2.5s ,空间开销自带几倍常数 (约 450MB )

使用 set 拉链,用时为 3.3s ,空间开销是十多倍常数(接近 1GB)

使用 unordered_set 拉链,用时同上,但直接 MLE

使用 deque 拉链,直接 RE 和几乎 MLE ,实践表明约 7.5\times 10^5 个 deque 就直接 500MB

使用 pb_ds 的红黑树,用时约 2.7s , MLE

使用 pb_ds 的splay树,用时约 2.7s , MLE

使用 pb_ds 的有序向量树,用时约 3.5s ,约 900MB

综上所述,实践表明:对随机数据和一倍空间冗余的除留余数法哈希函数,开放定址最优,最优冲突处理策略是二次探测法。

参考代码:(一倍空间冗余的除留余数法+开放定址法+二次探测法)

#include <bits/stdc++.h> using namespace std; #define mn 20000003 int n, a[mn], seed, m, ans; signed main() { scanf("%d%d%d", &n, &m, &seed); mt19937 mt(seed); uniform_int_distribution<int> dist(0, m); for (int i = 1; i <= n; ++i) { int v = dist(mt) + 1; //防止哈希值为0跟空重复,使其值域为[1,m+1] int hi = v % mn; bool repeat = a[hi] == v; int dt = 1, dv = 1; while (a[hi] != 0) { hi = (hi + dv * dt * dt) % mn; dv = -1 * dv; if (dv == 1) { ++dt; } if (a[hi] == v) { repeat = true; break; } } if (!repeat) { ++ans; a[hi] = v; } } printf("%d", ans); return 0; }

参考代码:(除留余数法+vector拉链法)

#include <bits/stdc++.h> using namespace std; #define mn 10000010 int n, seed, m, ans; vector<int> a[mn]; signed main() { scanf("%d%d%d", &n, &m, &seed); mt19937 mt(seed); uniform_int_distribution<int> dist(0, m); for (int i = 1; i <= n; ++i) { int v = dist(mt); int hi = v % mn; bool repeat = false; for (auto &j : a[hi]) { if (j == v) { repeat = true; break; } } if (!repeat) { ++ans; a[hi].emplace_back(v); } } printf("%d", ans); return 0; }

附:从理论而言,散列表的装填因子load factor,设 \alpha=\dfrac nm ,平均查找长度是 a 的函数,且有:

处理冲突的方法\平均查找长度查找成功时查找失败时
线性探测法\dfrac12\left(1+\dfrac1{1-\alpha}\right)\dfrac12\left(1+\dfrac1{(1-\alpha)^2}\right)
二次探测法-\dfrac1\alpha\ln(1+\alpha)\dfrac1{1-\alpha}
拉链法1+\dfrac\alpha2\alpha+e^{-\alpha}

为获得空间效率,建议保持 \alpha > 0.5 ,至少是半满的,一般上限设为 0.9