translate from The Mathematics Behind Rolling Hashes and Anti-hash Tests

设计难卡的滚动哈希

滚动哈希

我们使用二元组来描述滚动哈希。其中模数(modulo)基数(base)

对于长度为的字符串,它的滚动哈希函数为

对于等长的两个字符串,若,则称这是等长冲突(equal-length collision)

随机基数

考虑我们固定一个质数模数,在中随机一个整数作为基数来做滚动哈希。

对于两个长度为的串,考虑计算他们等长冲突的概率:

在这里是一个关于度的多项式。那么的概率就是的概率,即的根的概率。

由于是质数,因此这构成一个域。在域中,任何度的多项式最多有个根。因此

对于,这个概率是级别的。

的范围其实比较紧的。如果。考虑,则。根据一些群论知识,这个方程有个根。

随机模数

考虑固定基数,然后在的范围内随机选择一个整数作为模数

同样地,对于两个长度为的串,考虑计算他们等长冲突的概率:

则可以通过一些数学知识得到

对于,这个概率是级别的。

如何随机

以下三种可以考虑:

//by Sshwy
//#define DEBUGGER
#include<chrono>
#include<iostream>
using namespace std;

int main(){
    cout<<chrono::duration_cast<chrono::nanoseconds>(chrono::high_resolution_clock::now().time_since_epoch()).count()<<endl;
    cout<<chrono::duration_cast<chrono::nanoseconds>(chrono::steady_clock::now().time_since_epoch()).count()<<endl;
    cout<<__builtin_ia32_rdtsc()<<endl;
    return 0;
}

多重哈希

我们可以使用多重的随机滚动哈希。如果重哈希的冲突概率分别是,则你哈希碰撞的概率就是

更大的模数

你哈希的模数越大,冲突的概率就越小。但是更大的模数在计算过程中会很吃力。使用__int128会降低计算速度。不过有一个质数例外,那就是梅森质数,我们可以通过位运算来计算

constexpr uint64_t mod = (1ull<<61) - 1;
uint64_t mul(uint64_t a, uint64_t b){
	uint64_t l1 = (uint32_t)a, h1 = a>>32, l2 = (uint32_t)b, h2 = b>>32;
	uint64_t l = l1*l2, m = l1*h2 + l2*h1, h = h1*h2;
	uint64_t ret = (l&mod) + (l>>61) + (h << 3) + (m >> 29) + (m << 35 >> 3) + 1;
	ret = (ret & mod) + (ret>>61);
	ret = (ret & mod) + (ret>>61);
	return ret-1;
}

卡哈希

接下来就是关于如何卡哈希的教程了。

如何卡哈希

在介绍具体的方法之前,先思考一下,面对一道哈希问题,如果去卡它?

本质上,分为两个步骤:

  1. 对于该哈希方式找一个等长冲突;
  2. 利用这个等长冲突来造数据卡他。

单哈希

Thue–Morse 序列攻击:ULL 自然溢出

考虑任意的情况。我们可以使用 Thue–Morse 序列来卡。它的生成方式如下:

const int Q = 10;
const int N = 1 << Q;
std::string S(N, ' '), T(N, ' ');

for (int i = 0; i < N; i++)
    S[i] = 'A' + __builtin_popcount(i) % 2;
for (int i = 0; i < N; i++)
    T[i] = 'B' - __builtin_popcount(i) % 2;

这时就发生了等长冲突(不论的值)。具体参见 这篇文章

生日攻击:32 位模数,固定模数和基数

考虑使用生日攻击。固定长度,令。并且等概率随机(uniformly at random)生成个长度为的字符串。

如果的值不是特别小,则这个串的哈希值可以近似看作等概率随机分布,使用生日悖论,则这个数全部两两不同的概率是

因此我们重复这个过程,可以在的时间内找到等长冲突。

生日攻击不依赖于哈希函数(异或哈希也可以)。但它不能卡回文串。

代码

树攻击:更大模数,固定模数和基数

对于更大的模数,生日攻击将会变得很慢。

考虑树攻击(tree-attack)

对于两个长度为的的字符串,我们知道

不妨设。显然。树攻击会尝试寻找一个的序列使得

考虑维护个集合。定义

如果我们合并两个集合,则我们可以:

  1. 直接合并,则
  2. 都乘再合并,则
  3. 类似 2,可以令
  4. 都变成,则
  5. 类似 4,可以令

一开始我们有个集合,设每个集合的都是。不妨设。则我们每个阶段,都把集合按排序,然后使用 2 或者 3 方法来合并相邻两个集合(要保证合并完后非负)。一轮完成后,集合数量减半。如果出现了一个的集合,那么我们把其他集合的都置为即可。这样会最多做轮。如果没有找到,那么就对更大的继续这个过程。

如果一开始个集合的是等概率随机分布于,则期望为时可以找到。那么我们就可以地构造长度为的等长冲突的串了。

注意,树攻击是依赖于哈希函数的,即你的哈希函数必须是多项式函数。

卡回文串

树攻击可以卡回文串。以偶回文串为例,具体地说,我们要构造一个长度为偶数的串,使得表示反串。

那么这等价于

因此我们设,得到

那么我们对这个做一次正常的树攻击,就可以构造出一组的值。

那么用字符来构造串即可。

代码

多重树攻击

尽管树攻击速度很快,生成的字符串可能会过长(对于,通常)。实际上我们可以花更多的计算时间来生成一个尽量短的等长冲突。对于每个集合,我们可以维护个最小的可能的和(单树攻击是的情况)。合并两个集合可以使用堆在的时间内完成。

一通鬼畜分析后,我们得到

这个东西 Dls 也没写过,所以不知道好不好使。

多重哈希

卡多重哈希的方式有两种。

逐个击破

以双哈希为例,考虑双哈希。首先我们使用生日攻击或者树攻击找到的一个等长冲突。然后我们以作为字符集对求出等长冲突(以作为字符集的意思是,我们用拼接出一个更大的串)。这样就可以把两个哈希都冲突掉。

使用这个方法,时间复杂度是每次攻击的复杂度之和。但生成的串长会随着哈希的重数而指数级增长。

不过这个方法不用考虑模数的大小。

中国剩余定理(CRT)

对于树攻击,我们可以使用中国剩余定理来卡哈希。本质上,我们要求

。然后我们使用 CRT 求出一个,使得

那么我们就只需要

即可。 那么我们对此做一次树攻击即可。

这个方法的要求模数的 LCM 不能太大。不过它的优点是,双哈希的回文串它也能卡。