1、前言
字符串的几大主要算法都多少提及过,现在来讲讲一个称不上什么算法, 但是非常常用的东西——字符串Hash。
2、Hash的概念
Hash更详细的概念不多说了,它的作用在于能够对复杂的状态进行简单的表达,更方便的用于判重。在搜索的时候,或是动规的时候,都有过类似的做法。在实际应用中,也是非常重要的,这也就是为什么存在什么暴雪公司的Hash算法等等;加密环节也是Hash的重要之处,MD5码就是一个经典的例子。
字符串Hash的方式多式多样,重点来解释一下最简单的,最常用的。
3、BKDRHash
作为竞赛选手,BKDRHash必定是首选的!通过多种测试与研究,BKDRHash在简单Hash中是冲突量最小的一种Hash方式,处理起来也非常好理解。先看一段简短的代码:
------------------------------------------------------------------------------------------------------
#define x 131
#define MOD 1000000007
int h[MAXN];
char a[MAXN];
void getHash()
{
int len = strlen(a);
for (int i = 0; i <= len - 1; i++) h[i] = (h[i - 1] * x + a[i]) % MOD;
}
------------------------------------------------------------------------------------------------------
这种方式类似于进制数表示。对于一个字符串a的某一位i,乘上一个x^i,在x>=字符串中的字符数的情况下,Hash值和字符串必定是一一映射的,当且仅当在一一对应的情况下,我们的Hash值才有存在的含义。
但是显然的,有一个很严重的问题,倘若某个字符串规定只有10个字符,长度最长为8,则极限情况下Hash值为10^8。放入一个Hash值数组中,当然是可行的。但是在很多时候,字符串的长度和字符区域不会这么小的。这也是我最开始很困惑的地方,似乎无解的样子。这个时候,我们只能选择牺牲完美的正确性来满足条件了。
对于超过某个限度的数,我们给他取一个MOD。这样好像很逗的样子?我们简单分析一下出现重复的情况,在随机造数据求Hash值取模数1e9+7之后,冲突率为0.05%上下。在算法竞赛中,这种错误率几乎可以不计。
这也就有人问了,在非100%正确率的情况下,如果出题人丧心病狂的话,这不同样会出现用来卡你的数据?要知道,字符串Hash的灵活性是非常强的!上面代码中我们可以看出,x和MOD都是我们自己定义的,它并不取决于题目的数据(当然x是必须大于字符集的这个不用多说)。
上述代码中,我们的x设定值为131,我并不清楚为什么要用这个数,确实网上的很多做法和教程也都是131,但是其实局限性并没有这么强。只要这个数是大于等于出现的字符个数的,是都可以满足的,例如某提中提到全部为小写字母,x>=26即可。这个应该是不存在卡数据的。
关键就在于MOD的讲究。其实通过网上的,学长的各种表述,取MOD的方式也是各种花样。数值越大越好这个当然是必然的!但是在这个基础上取质数也是最好的。为什么说这个事有讲究的,可能在NOIP和一些比较基础的比赛/考试中,这个不影响;但是大考试中,如HNOI/CTSC中据说都出现过的专门卡特定MOD数的情况。比如1e9+7,1e6+7,1e9+1都是比较常见的,有些出题人故意尝试着去卡,导致某些选手分数降低。这个时候就可以花式取模了,只要是个较大的数,最好是个质数,比如你的生日之类的,如19990522之类的,谁会知道呢。
在平均情况下,采用伪随机数据,MOD<=5e6才会出现一个错误点,所以自己好好把握吧。
3、多Hash取值
我相信和我一样,许多人还是有后顾之忧,所以又出现了若干的方法使这个冲突率变得更小。因为对于算法竞赛,这个0.05%似乎不值得一提;但是对于大型工程,如软件,系统,游戏等,面对全球大量客户,不不难出现一些BUG。暴雪公司(Bilzzard)就有着自己的高效而巧妙的Hash值取法,这里不提;他们似乎对这个依旧不放心,于是还决定采取多Hash值的判断。
对于一个字符串,我们假定给他两个Hash值(BKDRHash和其他的类型,参见网上若干教程),当且仅当两个字符串的两个Hash值都是一样的,我们才认为这两个字符串相等。根据乘法原理,这已经显而易见了,冲突率就会是两个Hash方法的乘积之和,可见其量之小。
同样地,你也可以选择3个Hash,4个Hash,但是随着个数的增加,复杂度也会升高,所以一般取2~3个是足够了的。暴雪公司的设定冲突率为1:18889465931478580854784,大概是10^(-22.3)%,对一个游戏程序来说足够安全了。
4、Hash挂链
上述的多Hash取值相比挂链是要好理解一些的,暴雪公司就从来不挂链,但是作为一种方法,这里依旧要提及。Hash挂链才是真正的能够保证完美正确性的方法。同样先上一段代码。
------------------------------------------------------------------------------------------------------
vector <int> h[MAXN];
void add(int o)
{
int t = o % MOD;
for (int i = 1; i <= h[t].size(); i++)
if (h[t][i] == o) return;
h[t].push_back(o);
}
------------------------------------------------------------------------------------------------------
存放Hash值的容器为vector以节省空间,它的作用和链表一样(你也可以选择手写链表)。对于某一个数经过取模之后得到的数为t,它对应的vector为h[t]。我们将所有取模之后得到t的原值放入o,这样,可以很容易地发现是否冲突,只需要在vector中扫描一遍。
确实这个正确性是必定保证了的,但是相比之下,时间复杂度和空间复杂度似乎都不够理想,当然在能确保的情况下绝对是可以用的。
5、总结
字符串Hash,作用是无限的。KMP,SA等等,经常出现可以用字符串Hash代替的情况。