哈希
我们定义一个把字符串映射到整数的函数 f,这个 f 称为是 Hash 函数,我们希望这个函数 f 可以方便地帮我们判断两个字符串是否相等,这就是哈希。
一般来说,哈希值都是使用 hashi=(base⋅hashi−1+si)%mod 这个转移方程得到的。
在这个转移方程中,s 为字符串,base 被称为底数,而 mod 被称为模数。
生日悖论
如果一个班级有 23 个人, 那么其中有两个人生日相同的概率超过 50%。因为这与自己的直觉相不符,所以被称为生日悖论。
其实出现这样问题的原因是并没有将“两个人的生日相同”和“有人和自己的生日相同”很好的区分开。
定义两人生日重复的情况叫同生缘。
假设一年有 N 天,那么不发生同生缘的概率可以写作:
1−P=i=1∏n−1(1−Ni)
当 n≤N 时,因为 (1−N1)n≈1−Nn,所以
1−P≈i=1∏n−1(1−N1)i=(1−N1)2n⋅(n−1)
P=1−(1−N1)2n⋅(n−1)
当 N=365 时,函数图像可点击此链接查看。
可以观察到,在 n=23 时 P 已经接近了 50%。
卡大质数哈希
假设模数为 109+7,构造一个长度为 105 的字符串。
将这个数据带入上方函数,得到成功让字符串哈希冲突的概率为 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
,也就是对于 264 取模。不光其值域不难出现哈希冲突,而且代码长度与常数都会大大减小,得到了不少的同学的青睐。
将 N=264,n=105 带入上面的函数后,我们发现出现哈希冲突的可能性 P 无限接近与 0,所以使用生日攻击成功的可能性极小。
底数为偶数
可以构造全部为 a 的子串和第一个为 b 其余均为 a 的两个长度相等且长苏大于 64 的两个不一样的字符串。
因为底数的 64 次方以上模 264 都是 0,所以即是两个字符串不同,他们的哈希值也都会一样。
底数为奇数
设一些串 s,si 表示第 i 个串,si 的哈希值为 hash(si)。
定义 f(s) 为字符串 s 内全部的 a 都变为 b,所有的 b 都变成 a。
定义 si+sj 的意思为将 sj 添加到 si 的末尾形成的新的字符串。
构造方法为:s1=a,si=si−1+f(si−1),所以 ∣si∣=2i−1。
所以:
hash(si)=hash(si−1)⋅base∣si−1∣+hash(f(si−1))=hash(si−1)⋅base2i−2+hash(f(si−1))
hash(f(si−1))=hash(f(si−2))⋅base2i−2+hash(si−1)
hash(si)−hash(f(si−1))=(hash(si−1)−hash(f(si−2)))⋅base2i−2−(hash(si−1)−hash(f(si−2)))
hash(si)−hash(f(si−1))=(hash(si−1)−hash(f(si−2)))⋅(base2i−2−1)
因为希望产生哈希冲突,即 264∣hash(si)−hash(f(si))。
设 gi 表示 hash(si)−hash(f(si)),那么 g 满足一下性质:
gi=gi−1⋅(basei−2−1)
因为每一个 base2i−1−1 都是偶数,所以是的 g 到达第 64 项就可以 hack 了。
因为 base2i−1−1=(base2i−2−1)⋅(base2i−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;
}
参考文章 博客园