怎么卡飞哈希

哈希

我们定义一个把字符串映射到整数的函数 \(f\),这个 \(f\) 称为是 Hash 函数,我们希望这个函数 \(f\) 可以方便地帮我们判断两个字符串是否相等,这就是哈希。

一般来说,哈希值都是使用 \(hash_i=(base\cdot hash_{i-1} +s_i)\%mod\) 这个转移方程得到的。

在这个转移方程中,\(s\) 为字符串,\(base\) 被称为底数,而 \(mod\) 被称为模数。

生日悖论

如果一个班级有 \(23\) 个人, 那么其中有两个人生日相同的概率超过 \(50\%\)。因为这与自己的直觉相不符,所以被称为生日悖论。

其实出现这样问题的原因是并没有将“两个人的生日相同”和“有人和自己的生日相同”很好的区分开。

定义两人生日重复的情况叫同生缘。

假设一年有 \(N\) 天,那么不发生同生缘的概率可以写作:

\[1-P=\prod_{i=1}^{n-1} (1-\frac{i}{N}) \]

\(n\le N\) 时,因为 \((1-\frac{1}{N})^n\approx 1-\frac{n}{N}\),所以

\[1-P\approx \prod_{i=1}^{n-1}(1-\frac{1}{N})^i =(1-\frac{1}{N})^{\frac{n\cdot(n-1)}{2}} \]

\[P=1-(1-\frac{1}{N})^{\frac{n\cdot(n-1)}{2}} \]

\(N=365\) 时,函数图像可点击此链接查看。

可以观察到,在 \(n=23\)\(P\) 已经接近了 \(50\%\)

卡大质数哈希

假设模数为 \(10^9+7\),构造一个长度为 \(10^5\) 的字符串。

将这个数据带入上方函数,得到成功让字符串哈希冲突的概率为 \(P=0.993261715159\)

所以直接使用随机构造的方式,在一般情况下就可以将其 hack。

#include<bits/stdc++.h>
using namespace std;
int main(){
    printf("100000 20\n");
    for(int i = 1;i <= 100000;i++){
        putchar(rand() % 26 + 'a');
    }
    return 0;
}

自然溢出

自然溢出,即 hash 数组使用 unsigned long long,也就是对于 \(2^{64}\) 取模。不光其值域不难出现哈希冲突,而且代码长度与常数都会大大减小,得到了不少的同学的青睐。

\(N=2^{64},n=10^5\) 带入上面的函数后,我们发现出现哈希冲突的可能性 \(P\) 无限接近与 \(0\),所以使用生日攻击成功的可能性极小。

底数为偶数

可以构造全部为 \(\texttt{a}\) 的子串和第一个为 \(\texttt{b}\) 其余均为 \(\texttt{a}\) 的两个长度相等且长苏大于 \(64\) 的两个不一样的字符串。

因为底数的 \(64\) 次方以上模 \(2^{64}\) 都是 \(0\),所以即是两个字符串不同,他们的哈希值也都会一样。

底数为奇数

设一些串 \(s\)\(s_i\) 表示第 \(i\) 个串,\(s_i\) 的哈希值为 \(hash(s_i)\)

定义 \(f(s)\) 为字符串 \(s\) 内全部的 \(\texttt{a}\) 都变为 \(\texttt{b}\),所有的 \(\texttt{b}\) 都变成 \(\texttt{a}\)

定义 \(s_i+s_j\) 的意思为将 \(s_j\) 添加到 \(s_i\) 的末尾形成的新的字符串。

构造方法为:\(s_1=\texttt{a}\)\(s_i=s_{i-1}+f(s_{i-1})\),所以 \(|s_i|=2^{i-1}\)

所以:

\[hash(s_i)=hash(s_{i-1})\cdot base^{|s_{i-1}|}+hash(f(s_{i-1}))=hash(s_{i-1})\cdot base^{2^{i-2}}+hash(f(s_{i-1})) \]

\[hash(f(s_{i-1}))=hash(f(s_{i-2}))\cdot base^{2^{i-2}}+hash(s_{i-1}) \]

\[hash(s_i)-hash(f(s_{i-1}))=(hash(s_{i-1})-hash(f(s_{i-2})))\cdot base^{2^{i-2}}-(hash(s_{i-1})-hash(f(s_{i-2}))) \]

\[hash(s_i)-hash(f(s_{i-1}))=(hash(s_{i-1})-hash(f(s_{i-2})))\cdot (base^{2^{i-2}}-1) \]

因为希望产生哈希冲突,即 \(2^{64}\mid hash(s_i)-hash(f(s_i))\)

\(g_i\) 表示 \(hash(s_i)-hash(f(s_i))\),那么 \(g\) 满足一下性质:

\[g_i=g_{i-1}\cdot (base^{i-2}-1) \]

因为每一个 \(base^{2^{i-1}}-1\) 都是偶数,所以是的 \(g\) 到达第 \(64\) 项就可以 hack 了。

因为 \(base^{2^{i-1}}-1=(base^{2^{i-2}}-1)\cdot(base^{2^{i-2}}+1)\) 且为一个偶数乘一个偶数, 而左边的可以继续递归下去, 所以到第 \(12\) 位其实就可以 hack 了。

#include <iostream>
#include <cstring>
using namespace std;
char s[10000];
int main(){
	cout<<(1<<12)+65<<' '<<(1<<11)<<'\n';
	int now=1;
	s[1]='a';
	for (int i=1;i<=12;i++){
		for (int j=1;j<=now;j++) s[now+j]=s[j]=='a'?'b':'a';
		now<<=1;
	}
	for (int i=1;i<=now;i++) printf("%c",s[i]);
	for (int i=1;i<=65;i++) putchar('a');
	return 0;
}

参考文章 博客园

posted @ 2024-07-14 13:46  未抑郁的刘大狗  阅读(90)  评论(0编辑  收藏  举报