哈希表碰撞(溢出)概率计算
这篇文章简单谈谈目前业界使用的密码学库中,是如何计算哈希表(Hash table)的桶(bin)碰撞(溢出)概率的。
昨天遇到这样一个问题:在GitHub中,osu-crypto/BaRK-OPRF这个密码学相关repo中,有如下代码:
1 | double getBinOverflowProb(u64 numBins, u64 numBalls, u64 binSize, double epsilon = 0.0001) |
从函数名能看出是在计算哈希表的桶溢出概率(溢出和碰撞其实是一个意思),但就是这个简简单单的while
循环,看的人云里雾里。但稍加分析,我们就可以还原出来这段代码其实是在计算这样一个式子: \[P\{Hash\; overflows\}=numBins \cdot \sum_{k=binSize+1}^{numBalls}C_{numBalls}^{i}(1/numBins)^i(1-1/numBins)^{numBalls - i}.\] 最后的输出结果其实是\(-\log_2 P\{Hash\; overflows\}\),至于为何如此,后文详谈。先来说说这个式子是怎么来的。
为了方便叙述,我们规范化一下这个问题。
问题设置
一个哈希表可以看成是\(M\)个桶(bin),元素可以看成放进桶里的球(ball),假设每个桶容量(size)相同且固定为\(k\)(很多情况下\(k=1\))。假设我们已知共有\(N\)个元素(球)要放进桶里,那么哈希表的碰撞概率就等价于
\(N\)个球放进\(M\)个桶里,每个桶最多放\(k\)个球,有任意一个桶溢出(放了超过\(k\)个球)的概率。
下面分步求解。
选定一个桶\(bin_j\),里面放了\(i\leq N\)个球的概率?
这就是一个简单的二项分布:重复\(N\)次把球放进桶的实验,成功(球放进桶\(bin_j\))的次数为\(i\)的概率为 \[P\{X=i\}=C_{N}^{i}(1/M)^i(1-1/M)^{N - i},\] 不用多说,请查阅“二项分布”相关资料。
一定注意,这里的前提是我们选定了一个桶,就这个桶而言讨论“成功”。
选定一个桶\(bin_j\),这个桶溢出的概率?
溢出,即这个桶里放了\(i>k\)个球,那么对上面的\(P\\{X=i\\}\)求和 \[P\{bin_j\; overflows\}=\sum_{i=k+1}^{N} P\{X=i\}=\sum_{i=k+1}^{N}C_{N}^{i}(1/M)^i(1-1/M)^{N - i}.\] 是不是已经看起来很像最开始的式子了?但是还差个系数\(M\),这是哪里来的?
任意一个桶溢出的概率(的上界)?
前面的讨论都是基于已经选定了某一个桶,但事实上我们有\(M\)个桶,其中任意一个溢出都算哈希表溢出,那么我们要用的工具就是取并集。因此哈希表溢出概率为 \[P\{Hash\; overflows\}=P\{\cup_{j\in[M]}bin_j\; overflows\}.\]
那么问题来了,这个并集怎么算呢?
事实上,很多情况下我们讨论哈希表溢出问题,并不需要确切地知道这个数值,而是要利用这个概率估计哈希表容量应该有多大。我们只需要估计溢出概率的上界,只要容量能满足这个上界,就可以满足我们对哈希表的要求。
为了解决事件的并集概率问题,我们需要借助布尔不等式(Boole's inequality),也叫联合边界(Union Bound)(其实我之前只知道Union Bound这个名字,其他都是现查的)。其实很简单,事件并集的概率不超过单独事件发生的概率之和(不要求事件相互独立)。那么上式进一步化为 \[\begin{aligned} P\{Hash overflows\}&=P\{\cup_{j\in[M]}bin_j\; overflows\}\leq \sum_{j=1}^{M}P\{bin_j\; overflows\}\\ &=\sum_{j=1}^{M}\sum_{i=k+1}^{N}C_{N}^{i}(1/M)^i(1-1/M)^{N - i}\\ &=M\sum_{i=k+1}^{N}C_{N}^{i}(1/M)^i(1-1/M)^{N - i}. \end{aligned} \] 这不就是我们要求的式子吗?
不过代码里为了加速,用了一个小量epsilon
控制精度,如果精度够了就可以及时截断。
为什么要取负对数?
这个问题要继续往代码下面看。在这里:
1 | u64 get_bin_size(u64 numBins, u64 numBalls, u64 statSecParam) |
这里的statSecParam
是密码学里的统计安全参数,类似经常听说的SHA256、SHA512里的数字,数字代表bit位数,越大越安全。在这个实现里,这一参数取值范围是uint64
的非负整数。
安全参数越大越安全,密码学里碰撞概率(两个本来不同的明文Hash之后结果相同,其实也就是溢出概率)越小越安全;安全参数是非负整数,概率是\([0,1]\)间的实数。如何建立映射关系呢?很简单,就是对概率取负对数,就变成了\([0,\infty)\)的实数,而且越大越安全。这就和安全参数产生了对应关系。
因此,在前面算概率时,就直接返回这个映射过的“概率”,稍后求哈希表容量时,就可以直接和安全参数比较了。
题外话
这个问题其实最早是我在CMU的15-451/651 Algorithms课上,Sleator给我们布置的作业题。当时我真是毫无头绪,多亏了John和Wael两位大哥认真讲解,我才搞明白为啥要用那个之前听都没听说过的Union Bound。没想到这次又在实际中遇到了这个问题,第一次发现Sleator讲的东西是有用的,而不是为了难为我们。
离开美国时走得太仓促,很多东西都没能带走,似乎Algo的lecture notes我是扔下了。想到这里,多少有点惋惜。只能安慰自己,东隅已逝桑榆非晚。