转载:字符串hash总结(hash是一门优雅的暴力!)

转载自:远航休息栈

字符串Hash总结

Hash是什么意思呢?某度翻译告诉我们:

hash 英[hæ?] 美[hæ?]
n. 剁碎的食物; #号; 蔬菜肉丁;
vt. 把…弄乱; 切碎; 反复推敲; 搞糟;

我觉得Hash是引申出 把...弄乱 的意思。

今天就来谈谈Hash的一种——字符串hash。

据我的理解,Hash就是一个像函数一样的东西,你放进去一个值,它给你输出来一个值。输出的值就是Hash值。一般Hash值会比原来的值更好储存(更小)或比较。

那字符串Hash就非常好理解了。就是把字符串转换成一个整数的函数。而且要尽量做到使字符串对应唯一的Hash值。

字符串Hash的种类还是有很多种的,不过在信息学竞赛中只会用到一种名为“BKDR Hash”的字符串Hash算法。

它的主要思路是选取恰当的进制,可以把字符串中的字符看成一个大数字中的每一位数字,不过比较字符串和比较大数字的复杂度并没有什么区别(高精数的比较也是O(n)O(n)的),但只要把它对一个数取模,然后认为取模后的结果相等原数就相等,那么就可以在一定的错误率的基础上O(1)O(1)进行判断了。

那么我们选择什么进制比较好?

首先不要把任意字符对应到数字0,比如假如把a对应到数字0,那么将不能只从Hash结果上区分ab和b(虽然可以额外判断字符串长度,但不把任意字符对应到数字0更加省事且没有任何副作用),一般而言,把a-z对应到数字1-26比较合适。

关于进制的选择实际上非常自由,大于所有字符对应的数字的最大值,不要含有模数的质因子(那还模什么),比如一个字符集是a到z的题目,选择27、233、19260817都是可以的。

模数的选择(尽量还是要选择质数):

绝大多数情况下,不要选择一个109109级别的数,因为这样随机数据都会有Hash冲突,根据生日悖论,随便找上109−−−√109个串就有大概率出现至少一对Hash 值相等的串(参见BZOJ 3098 Hash Killer II)。

最稳妥的办法是选择两个109109级别的质数,只有模这两个数都相等才判断相等,但常数略大,代码相对难写,目前暂时没有办法卡掉这种写法(除了卡时间让它超时)(参见BZOJ 3099 Hash Killer III)。

如果能背过或在考场上找出一个10181018级别的质数(Miller-Rabin),也相对靠谱,主要用于前一种担心会超时,后一种担心被卡。

偷懒的写法就是直接使用unsigned long long,不手动进行取模,它溢出时会自动对264264进行取模,如果出题人比较良心,这种做法也不会被卡,但这个是完全可以卡的,卡的方法参见BZOJ 3097 Hash Killer I。

用luogu P3370为例。

这是自然溢出hash(100)

C++

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

#include <cstdio>

#include <cstring>

#include <algorithm>

using namespace std;

typedef unsigned long long ull;

ull base=131;

ull a[10010];

char s[10010];

int n,ans=1;

ull hashs(char s[])

{

int len=strlen(s);

ull ans=0;

for (int i=0;i<len;i++)

ans=ans*base+(ull)s[i];

return ans&0x7fffffff;

}

main()

{

scanf("%d",&n);

for (int i=1;i<=n;i++)

{

scanf("%s",s);

a[i]=hashs(s);

}

sort(a+1,a+n+1);

for (int i=2;i<=n;i++)

if (a[i]!=a[i-1])

ans++;

printf("%d\n",ans);

}

这是单模数hash(80)

C++

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

#include <cstdio>

#include <cstring>

#include <algorithm>

using namespace std;

typedef unsigned long long ull;

ull base=131;

ull a[10010];

char s[10010];

int n,ans=1;

ull mod=19260817;

ull hashs(char s[])

{

int len=strlen(s);

ull ans=0;

for (int i=0;i<len;i++)

ans=(ans*base+(ull)s[i])%mod;

return ans;

}

main()

{

scanf("%d",&n);

for (int i=1;i<=n;i++)

{

scanf("%s",s);

a[i]=hashs(s);

}

sort(a+1,a+n+1);

for (int i=2;i<=n;i++)

if (a[i]!=a[i-1])

ans++;

printf("%d\n",ans);

}

这是双hash(100)

C++

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

#include <cstdio>

#include <cstring>

#include <algorithm>

using namespace std;

typedef unsigned long long ull;

ull base=131;

struct data

{

ull x,y;

}a[10010];

char s[10010];

int n,ans=1;

ull mod1=19260817;

ull mod2=19660813;

ull hash1(char s[])

{

int len=strlen(s);

ull ans=0;

for (int i=0;i<len;i++)

ans=(ans*base+(ull)s[i])%mod1;

return ans;

}

ull hash2(char s[])

{

int len=strlen(s);

ull ans=0;

for (int i=0;i<len;i++)

ans=(ans*base+(ull)s[i])%mod2;

return ans;

}

bool comp(data a,data b)

{

return a.x<b.x;

}

main()

{

scanf("%d",&n);

for (int i=1;i<=n;i++)

{

scanf("%s",s);

a[i].x=hash1(s);

a[i].y=hash2(s);

}

sort(a+1,a+n+1,comp);

for (int i=2;i<=n;i++)

if (a[i].x!=a[i-1].x || a[i-1].y!=a[i].y)

ans++;

printf("%d\n",ans);

}

这是只用一个10^18质数的hash(100)

C++

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

#include <cstdio>

#include <cstring>

#include <algorithm>

using namespace std;

typedef unsigned long long ull;

ull base=131;

ull a[10010];

char s[10010];

int n,ans=1;

ull mod=212370440130137957ll;

ull hashs(char s[])

{

int len=strlen(s);

ull ans=0;

for (int i=0;i<len;i++)

ans=(ans*base+(ull)s[i])%mod;

return ans;

}

main()

{

scanf("%d",&n);

for (int i=1;i<=n;i++)

{

scanf("%s",s);

a[i]=hashs(s);

}

sort(a+1,a+n+1);

for (int i=2;i<=n;i++)

if (a[i]!=a[i-1])

ans++;

printf("%d\n",ans);

}

Hash还有一方面,就是它可以处理子串信息。对于一个字符串,我们可以预处理它1−l1−l的hash值,这样l+1l+1的hash值就可以O(1)O(1)的递推出来。

对于一个字符串l−rl−r的子串,我们可以用f[r]−br−l+1f[l−1]f[r]−br−l+1f[l−1]来求出来,其中b表示进制。

这样的话hash就可以水过字符串匹配的题目

cogs1570

【题目描述】

法国作家乔治·佩雷克(Georges Perec,1936-1982)曾经写过一本书,《敏感字母》(La disparition),全篇没有一个字母‘e’。他是乌力波小组(Oulipo Group)的一员。下面是他书中的一段话:

Tout avait Pair normal, mais tout s’affirmait faux. Tout avait Fair normal, d’abord, puis surgissait l’inhumain, l’affolant. Il aurait voulu savoir où s’articulait l’association qui l’unissait au roman : stir son tapis, assaillant à tout instant son imagination, l’intuition d’un tabou, la vision d’un mal obscur, d’un quoi vacant, d’un non-dit : la vision, l’avision d’un oubli commandant tout, où s’abolissait la raison : tout avait l’air normal mais…

佩雷克很可能在下面的比赛中得到高分(当然,也有可能是低分)。在这个比赛中,人们被要求针对一个主题写出甚至是意味深长的文章,并且让一个给定的“单词”出现次数尽量少。我们的任务是给评委会编写一个程序来数单词出现了几次,用以得出参赛者最终的排名。参赛者经常会写一长串废话,例如500000个连续的‘T’。并且他们不用空格。

因此我们想要尽快找到一个单词出现的频数,即一个给定的字符串在文章中出现了几次。更加正式地,给出字母表{‘A‘,‘B‘,‘C‘,...,‘Z‘}和两个仅有字母表中字母组成的有限字符串:单词W和文章T,找到W在T中出现的次数。这里“出现”意味着W中所有的连续字符都必须对应T中的连续字符。T中出现的两个W可能会部分重叠。

【输入格式】

输入包含多组数据。

输入文件的第一行有一个整数,代表数据组数。接下来是这些数据,以如下格式给出:

第一行是单词W,一个由{‘A‘,‘B‘,‘C‘,...,‘Z‘}中字母组成的字符串,保证1<=|W|<=10000(|W|代表字符串W的长度)

第二行是文章T,一个由{‘A‘,‘B‘,‘C‘,...,‘Z‘}中字母组成的字符串,保证|W|<=|T|<=1000000。

【输出格式】

对每组数据输出一行一个整数,即W在T中出现的次数。

【样例输入】

3
BAPC
BAPC
AZA
AZAZAZA
VERDI
AVERDXIVYERDIAN

【样例输出】

1
3
0

代码

C++

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

#include <cstdio>

#include <cstring>

#include <algorithm>

using namespace std;

typedef unsigned long long ull;

ull base=131;

ull po[100010],hs[100010*100];

char s1[100010],s2[100010*100];

int n,ans=1,T;

ull geth(int l,int r)

{

return (ull)hs[r]-po[r-l+1]*hs[l-1];

}

main()

{

freopen("oulipo.in","r",stdin);

freopen("oulipo.out","w",stdout);

po[0]=1;

for (int i=1;i<=10010-5;i++)

po[i]=po[i-1]*base;

scanf("%d",&T);

while(T--)

{

scanf("%s%s",s1+1,s2+1);

int l1=strlen(s1+1),l2=strlen(s2+1);

ull a1=0,ans=0;

for (int i=1;i<=l1;i++)

a1=a1*base+(ull)s1[i];

for (int i=1;i<=l2;i++)

hs[i]=hs[i-1]*base+s2[i];

for (int i=1;i+l1-1<=l2;i++)

if (a1==geth(i,i+l1-1))

ans++;

printf("%d\n",ans);

}

}

写到这里突然发现hash好像可以暴力水过很多字符串算法。。

1、kmp

问题:给两个字符串S1,S2,求S2是否是S1的子串,并求S2在S1中出现的次数

把S2 Hash出来,在S1里找所有长度为|S2||S2|的子串,Hash比较。效率O(|S1|)O(|S1|)

2、AC自动机

问题:给N个单词串,和一个文章串,求每个单词串是否是文章串的子串,并求每个单词在文章中出现的次数。

把每一个单词hash成整数,再把文章的每一个子串hash成整数,接下来只需要进行整数上的查找即可。

复杂度:O(|A|2+|S|)O(|A|2+|S|)

用AC自动机可以做到O(|A|+|S|)O(|A|+|S|)的复杂度,|S||S|是单词串总长,|A||A|是文章长度

3、后缀数组

问题:给两个字符串S1,S2,求它们的最长公共子串的长度。

将S1的每一个子串都hash成一个整数,将S2的每一个子串都hash成一个整数

两堆整数,相同的配对,并且找到所表示的字符串长度最大的即可。

复杂度:O(|S1|2+|S2|2)O(|S1|2+|S2|2)

用后缀数组可以优化到O(|S|log|S|)O(|S|log|S|)

4、马拉车

问题:给一个字符串S,求S的最长回文子串。

先求子串长度位奇数的,再求偶数的。枚举回文子串的中心位置,然后二分子串的长度,直到找到一个该位置的最长回文子串,不断维护长度最大值即可。

复杂度:O(|S|log|S|)O(|S|log|S|)

用manacher可以做到O(|S|)O(|S|)的复杂度

5、扩展kmp

问题:给一个字符串S,求S的每个后缀与S的最长公共前缀

枚举每一个后缀的起始位置,二分长度,求出每个后缀与S的最长公共前缀。

复杂度:O(|S|log|S|)O(|S|log|S|)

用extend-kmp可以做到O(|S|)O(|S|)的复杂度

后记

hash真是一种优雅的暴力。

因为字符串特殊的性质,我们可以二分得处理它,一般都有单调性。

时间: 2024-11-08 16:36:21

转载:字符串hash总结(hash是一门优雅的暴力!)的相关文章

转载 字符串hash

转载自:http://www.cnblogs.com/jiu0821/p/4554352.html 求一个字符串的hash值: ?现在我们希望找到一个hash函数,使得每一个字符串都能够映射到一个整数上 ?比如hash[i]=(hash[i-1]*p+idx(s[i]))%mod ?字符串:abc,bbc,aba,aadaabac ?字符串下标从0开始 ?先把a映射为1,b映射为2,c->3,d->4,即idx(a)=1, idx(b)=2, idx(c)=3,idx(d)=4: ?好!开始对

BZOJ 1090 SCOI2003 字符串折叠 动态规划+Hash

题目大意:给定一个字符串,求按照题中所给的压缩方式最短能压缩到多长 区间DP 令f[i][j]表示[i,j]区间内的字符串最短能压缩到多长 普通的区间DP:f[i][j]=min{f[i][k]+f[k+1][j]} (i<=k<=j-1) 此外如果对这段字符串进行压缩,那么我们可以枚举循环节,用Hash来判断 如果k是一个循环节,那么有f[i][j]=min(f[i][j],f[i][i+k-1]+digit[len/k]+2) 其中len=j-i+1,digit表示一个数在十进制下的长度

BZOJ 1090 字符串折叠(Hash + DP)

题目链接 字符串折叠 区间DP.f[l][r]为字符串在区间l到r的最小值 正常情况下 f[l][r] = min(f[l][r], f[l][l + k - 1] + f[l + k][r]); 当l到r以k为周期时 f[l][r] = min(f[l][r], 2 + sz(k) + f[l][l + (r - l + 1) / k - 1]); 判重的时候为了方便我用了哈希……当然其他方法应该也是可以的~ #include <bits/stdc++.h> using namespace

【转载】对一致性Hash算法,Java代码实现的深入研究

原文地址:http://www.cnblogs.com/xrq730/p/5186728.html 一致性Hash算法 关于一致性Hash算法,在我之前的博文中已经有多次提到了,MemCache超详细解读一文中"一致性Hash算法"部分,对于为什么要使用一致性Hash算法.一致性Hash算法的算法原理做了详细的解读. 算法的具体原理这里再次贴上: 先构造一个长度为232的整数环(这个环被称为一致性Hash环),根据节点名称的Hash值(其分布为[0, 232-1])将服务器节点放置在这

转载 C#使用Salt + Hash来为密码加密

转载 http://www.csharpwin.com/csharpspace/13412r9615.shtml (一) 为什么要用哈希函数来加密密码 如果你需要保存密码(比如网站用户的密码),你要考虑如何保护这些密码数据,象下面那样直接将密码写入数据库中是极不安全的,因为任何可以打开数据库的人,都将可以直接看到这些密码. 解决的办法是将密码加密后再存储进数据库,比较常用的加密方法是使用哈希函数(Hash Function).哈希函数的具体定义,大家可以在网上或者相关书籍中查阅到,简单地说,它的

luoguP3370 【模板】字符串哈希 [hash]

题目描述 如题,给定N个字符串(第i个字符串长度为Mi,字符串内包含数字.大小写字母,大小写敏感),请求出N个字符串中共有多少个不同的字符串. 友情提醒:如果真的想好好练习哈希的话,请自觉,否则请右转PJ试炼场:) 输入输出格式 输入格式: 第一行包含一个整数N,为字符串的个数. 接下来N行每行包含一个字符串,为所提供的字符串. 输出格式: 输出包含一行,包含一个整数,为不同的字符串个数. 输入输出样例 输入样例#1: 5 abc aaaa abc abcc 12345 输出样例#1: 4 说明

大数据处理算法三:分而治之/hash映射 + hash统计 + 堆/快速/归并排序

百度面试题1.海量日志数据,提取出某日访问百度次数最多的那个IP. IP 是32位的,最多有个2^32个IP.同样可以采用映射的方法,比如模1000,把整个大文件映射为1000个小文件,再找出每个小文中出现频率最大的 IP(可以采用hash_map进行频率统计,然后再找出频率最大的几个)及相应的频率.然后再在这1000个最大的IP中,找出那个频率最大的IP,即 为所求. 百度面试题2.搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节. 假设目前有一

为什么Java的hash表的长度一直是2的指数次幂?为什么这个(hash&amp;(h-1)=hash%h)位运算公式等价于取余运算?

1.什么是hash表? 答:简单回答散列表,运算在hash结构散列(分散)存放. 2.如何散列排布,如果均匀排布? 答:取余运算 3.Java中如何实现? 答:hash&(h-1) 4.为什么hash&(h-1)=等价于hash%h java的h(表长)一定是2的指数次幂,2的指数次幂2n 2n的结果:一定长这样10000...(n个0) 2n-1的结果:一定这样1111(n-1)个1 举个例子: 当h=16,对应的二进制:00010000 h-1=15,对应的二进制:00001111 可

海量数据面试题----分而治之/hash映射 + hash统计 + 堆/快速/归并排序

1.从set/map谈到hashtable/hash_map/hash_set 稍后本文第二部分中将多次提到hash_map/hash_set,下面稍稍介绍下这些容器,以作为基础准备.一般来说,STL容器分两种: 序列式容器(vector/list/deque/stack/queue/heap), 关联式容器.关联式容器又分为set(集合)和map(映射表)两大类,以及这两大类的衍生体multiset(多键集合)和multimap(多键映射表),这些容器均以RB-tree完成.此外,还有第3类关