卡掉hash的方法

大质数hash

通常,这个质数会选择在 109 附近,如 998244353109+7

考虑生日碰撞,欲达到 50% 成功率,需要尝试的次数为

(1)Q(H)π2H39623

可以参考概率表

所以我们可以生成 105 左右个较短的字符串,即可有很大的概率发生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;
}

试运行发现,设置字符串数量为39623时,发生hash冲突的概率近似50%,符合预期。而当设置字符串数量为100000时,1000次测试中只有4次没有发生hash冲突。所以设置105个字符串就差不多可以卡掉绝大多数单大质数hash了。

64位无符号整数自然溢出

首先需要对base奇偶性分类讨论。

当base是偶数时比较简单:设第 i 位指的是字符串从右往左数第 i 个字符,设有相同串 C ,其长度不小于64.构造字符串 A=a+C,B=b+C,这两个字符串的后64位上均相同,更高位上不相同。

字符串中第 i 位的权重为 basei1,则高于64位上的字符的权重一定可以被 264 整除。也就是说,高于64位上的字符不会对hash值产生影响。

下面着重说一下base为奇数的情况。

构造方法

考虑使用字符ab构造字符串:

A¯ 表示字符串 A 中所有 a 变成 b ,所有 b 变成 a

A1=aAi=Ai1+Ai1¯

例如 A2=ab,A3=abba,A4=abbabaab

那么len(Ai)=2i1

可以证明,当 i 大于某个数时,hash(Ai)=hash(Ai¯)

证明

由于我们的hash函数使用的是64位无符号整数自然溢出,所以相当于我们需要证明

(2)264(hash(Ai)hash(Ai¯))

f(i)=hash(Ai)hash(Ai¯)

根据递推公式可得

(3)hash(Ai)=hash(Ai1)×baselen(Ai1)+hash(Ai1¯)(4)=hash(Ai1)×base2i2+hash(Ai1¯)

则有

(5)f(i)=(hash(Ai1)hash(Ai1¯))×base2i2+(hash(Ai1¯)hash(Ai1))(6)=(hash(Ai1)hash(Ai1¯))×(base2i21)(7)=f(i1)×(base2i21)

g(i)=base2i21(i2)

则有

(8)f(i)=f(i1)×g(i)(9)=f(i2)×g(i)×g(i1)(10)=f(1)×g(i)×g(i1)×g(i2)××g(2)

由于 base 是奇数,所以 base2i2 也是奇数,故 g(i) 是偶数。

故有

(11)2i1f(i)

为了达到264f(i),需取i=65即可,但是这样会构造两个长度为2641020的字符串,是不可行的。

由于g(i)=base2i21=(base2i3+1)(base2i31)=g(i1)

所以有

(12)2i1g(i)(13)2i(i1)2f(i)

我们需要i(i1)264,则只需取i=12,构造出字符串 A12A12¯,即可卡掉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;
}

疑惑:为什么这里取 i=11 就可以了?

我在研究的过程中,发现取 i=11 时,对于测试的所有奇数base都成功了。但是明明证明的是 i 最小取 12?最后由lzh揭开了谜团。

g(3) 很特别:g(3)=base2321=base21

由于base是奇数,设base=2n1(n1),有

(14)g(3)=(2n1)21=4n24n=4n(n+1)

一定是8的倍数。故 23g(3)

再结合递推公式,有

(15){2ig(i)i321g(i)i=2

所以有

(16)2(3+i)(i2)2+1f(x)

i=11时,刚好是264(真巧!)

如何避免被卡

  • 随机base。相当于让不同位置上的权重不一样
  • 双模数hash。
  • 超大质数hash。既能像自然溢出一样有着大值域不易生日攻击,又不会被特殊的构造卡掉。
posted @   Vanilla_chan  阅读(335)  评论(3编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
点击右上角即可分享
微信分享提示