卡掉hash的方法
大质数hash
通常,这个质数会选择在 附近,如 ,。
考虑生日碰撞,欲达到 50% 成功率,需要尝试的次数为
可以参考概率表
所以我们可以生成 左右个较短的字符串,即可有很大的概率发生hash冲突。
Code
#include<iostream>
#include<algorithm>
#include<cstring>
#include<map>
#include<vector>
#include<limits.h>
#define LL long long
#define ULL unsigned long long
using namespace std;
vector<string> create(unsigned num,unsigned int sze)
{
vector<string>ans;
while(num--)
{
string str;
for(unsigned int i=0;i<sze;i++)
{
str.push_back('a'+rand()%26);
}
ans.push_back(str);
}
return ans;
}
bool check(vector<string>strs,ULL base,ULL p)
{
// sort(strs.begin(),strs.end());
map<int,string>Map;
for(unsigned int i=0;i<strs.size();i++)
{
ULL x=0;
for(unsigned int j=0;j<strs[i].size();j++)
{
x=x*base+strs[i][j]-'a';
x%=p;
}
if(Map.find(x)==Map.end()) Map[x]=strs[i];
else
{
if(Map[x]!=strs[i]) return 1;
}
}
return 0;
}
int main()
{
srand(time(0));
int T=100,succ=0;
for(int t=0;t<T;t++)
{
vector<string>strs=create(100000,10); // 生成100000个长度为10的随机字符串
bool c=check(strs,31,998244853); // base=31,p=998244353,检查是否存在hash冲突
cout<<c<<endl;
succ+=c;
}
cout<<succ<<"/"<<T<<endl;
return 0;
}
试运行发现,设置字符串数量为时,发生hash冲突的概率近似,符合预期。而当设置字符串数量为时,次测试中只有次没有发生hash冲突。所以设置个字符串就差不多可以卡掉绝大多数单大质数hash了。
64位无符号整数自然溢出
首先需要对base奇偶性分类讨论。
当base是偶数时比较简单:设第 位指的是字符串从右往左数第 个字符,设有相同串 ,其长度不小于64.构造字符串 ,这两个字符串的后64位上均相同,更高位上不相同。
字符串中第 位的权重为 ,则高于64位上的字符的权重一定可以被 整除。也就是说,高于64位上的字符不会对hash值产生影响。
下面着重说一下base为奇数的情况。
构造方法
考虑使用字符a
和b
构造字符串:
记 表示字符串 中所有 a
变成 b
,所有 b
变成 a
。
记 ,
例如
那么
可以证明,当 大于某个数时,
证明
由于我们的hash函数使用的是64位无符号整数自然溢出,所以相当于我们需要证明
设
根据递推公式可得
则有
设
则有
由于 是奇数,所以 也是奇数,故 是偶数。
故有
为了达到,需取即可,但是这样会构造两个长度为的字符串,是不可行的。
由于
所以有
我们需要,则只需取,构造出字符串 和 ,即可卡掉base为奇数的自然溢出。
最后,在这两字符串后再加上长度大于等于64的相同串,即可同时卡掉base为偶数的自然溢出。
Code
#include<iostream>
#include<string>
#include<cmath>
#include<map>
#include<vector>
#include<limits.h>
#define ULL unsigned long long
using namespace std;
string C;
string create()
{
string str="a";
for(int i=2;i<=11;i++) // 会产生长度为2^(i-1)长度的字符串,而我们需要i_max=12
{
for(int j=0;j<(1<<(i-2));j++) //延拓字符串长度为1<<(i-1)
{
str.push_back(str[j]=='a'?'b':'a');
}
}
return str;
}
string Not(string str)
{
for(unsigned int i=0;i<str.size();i++)
{
str[i]=(str[i]=='a'?'b':'a');
}
return str;
}
bool check(string a,string b,ULL base)
{
ULL aa=0,bb=0;
for(unsigned int i=0;i<a.size();i++)
{
aa=aa*base+a[i]-'a';
}
for(unsigned int i=0;i<b.size();i++)
{
bb=bb*base+b[i]-'a';
}
return aa==bb;
}
int main()
{
for(int i=1;i<=65;i++) C.push_back('a');
string str=create();
string A=str+C,B=Not(str)+C;
cout<<"构造的字符串的长度为"<<A.size()<<endl;
int T=10000,succ=0;
for(int t=0;t<T;t++)
{
bool c=check(A,B,t*2+1);
cout<<c<<endl;
succ+=c;
}
cout<<succ<<"/"<<T<<endl;
return 0;
}
疑惑:为什么这里取 就可以了?
我在研究的过程中,发现取 时,对于测试的所有奇数base都成功了。但是明明证明的是 最小取 ?最后由lzh揭开了谜团。
很特别:
由于base是奇数,设,有
一定是8的倍数。故 。
再结合递推公式,有
所以有
当 时,刚好是(真巧!)
如何避免被卡
- 随机base。相当于让不同位置上的权重不一样
- 双模数hash。
- 超大质数hash。既能像自然溢出一样有着大值域不易生日攻击,又不会被特殊的构造卡掉。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本