字符串算法大整理!你能想到的都能找到(吧)。
2018.7.16 Chengdu
今天学习了字符串相关的一些算法,种类挺多的,特来整理一波。
字符串哈希(Hash)
简介
哈希( HashHash 所远不能及的。
第一次了解到哈希这一种技术是在吴军 dalaodalao 的借阅!)讲解网络爬虫的数学原理的一章。有兴趣的话可以去读一读。
原理
哈希查找
为什么哈希查找的时间复杂度可以这么低呢?考虑下面一个问题:
设计一个程序,以完成以下输入输出:
第一行,输入两个数字 n,mn,m 。
对于朴素算法,我们把第二行输入的数字储存在一个数组中。对于每一次询问,我们从左到右遍历整个数组来查询,每次询问的时间复杂度为 O(n)O(n) 。
对于二分查找算法,我们把第二行输入的数字进行排序。对于每一次询问,我们用二分查找的方法找到这个数字处在的位置,排序的时间复杂度为 O(logn)O(logn) 。
可是这一题真的有这么复杂吗?我们可以使用一个大小为 105105 数组,就可以得到询问的结果了。而哈希,就是运用了这样的思想:我们多开一个数组,用这些多申请的空间去解决时间上的复杂。这也正是应了那句名言:
以空间换时间。 —-蒋介石
字符串哈希
那么如果我们把题改一下呢?
设计一个程序,以完成以下输入输出:
第一行,输入两个数字 n,mn,m 。
如果输入的是字符串的话,我们不就不能用 visvis 这一神奇的模板库,那么你一定会毫不犹豫地打出一行这样的代码:
map<string,bool>vis;
- 1
是的, mapmap 可以完美的解决这个问题。不过,我们可以试试用自己的力量来完成对字符串的处理:将字符串转换为数字。
怎样转换呢?我们知道,每一个字符都有它对应的 ASCIIASCII 的增大而逐步增大。
接下来给出一个函数 HashHash ,来获取一个字符串的哈希值:
const int P=131;
int Hash(string tmp)
{
int hash=0;
for(int i=0;i<tmp.length();i++)
hash=hash*P+tmp[i];
return hash;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
可是我们又面临了一个问题,这是因为题目中写到:
数据说明:输入的字符串的长度范围在 [1,105][1,105] 。
字符串的最大长度是 105105 不就爆炸了吗?
所以我们还要定义一个大数 MM 。当然,如果你乐意,取其它的值也是可以的。那么这个函数的运算就变成了:
const int P=131;
const int M=99991;
int Hash(string tmp)
{
int hash=0;
for(int i=0;i<tmp.length();i++)
hash=(hash*P+tmp[i])%M;
return hash;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
这样我们就得到了一个字符串的哈希值,不过问题又出现了:怎样保证两个字符串的哈希各不相同?
这确实是哈希的一个大问题,具体来说有三种解决方法:
- 通过增加哈希池大小来降低两个字符串的哈希值冲突的概率。哈希池就是储存字符哈希值的地方,对应上述问题中的 visvis 的方式来达到这一结果,不过这样就会使得哈希计算过于复杂而且难以储存。
- 对于每个字符串的哈希值记录原有字符串,在冲突的情况下对原有字符串进行逐次比较。
第二种方法是我的常用方法,不过怎么实现呢?这里我们可以不使用 visvis ,具体来说这样写:
vector<string>v[M];
bool Query(string tmp)
{
int pos=Hash(tmp);
for(int i=0;i<v[pos].size();i++)
if(v[pos][i]==tmp) return true;
return false;
}
void Add(string tmp)
{
if(Query(tmp)) return ;
int pos=Hash(tmp);
G[pos].push_back(tmp);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
这样就达到了我们的目的。这里给出一道字符串哈希的模板题:
哈希的弊端 & 如何卡哈希
可是还有的人为了图代码书写方便而拒绝使用强大的 vectorvector 来进行储存,这样就会被毒瘤出题人卡,因为会有人思考出卡掉各种字符串哈希的方法,具体可见几道神题:
BZOJ 3097 Hash Killer I
BZOJ 3098 Hash Killer II
BZOJ 3099 Hash Killer III
BZOJ 4917 [Lydsy1706月赛]Hash Killer IV (付费警告!)
在这些题目里,你被要扮演一个毒瘤出题人。因为你的后缀自动机神题被人用字符串哈希水掉了,所以你很不开心,决定用一组自造数据来卡掉他的代码。在这些题目中,给出水题的人的C++代码,要求你输出一组 hackhack 数据。
题目挺有意思,但是我们如何来操作呢?即,我们如何构造出两个相同的字符串,使它们的哈希值相同呢?
先拿第一题做例子。下面是题目大意:
Hash killer 1:
本来的神题:给你一个长度为 NN 。
自然溢出是什么意思呢?就是不去取 MM 。他的代码如下:
typedef unsigned long long u64;
const int MaxN = 100000;
inline int hash_handle(const char *s, const int &n, const int &l, const int &base)
{
u64 hash_pow_l = 1;
for(int i=1;i<=l;i++)
hash_pow_l *= base;
int li_n = 0;
static u64 li[MaxN];
u64 val = 0;
for(int i=0;i<l;i++)
val=val*base+s[i]-'a';
li[li_n++]=val;
for(int i=l;i<n;i++)
{
val=val*base+s[i]-'a';
val-=(s[i-l]-'a')*hash_pow_l;
li[li_n++]=val;
}
sort(li,li+li_n);
li_n=unique(li,li+li_n)-li;
return li_n;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
需要注意的事,这一代码中的基数 basebase 值)是随机的。那么我们就需要分类讨论:
- 如果基数 basebase 。
恭喜你,经过了层层计算,终于卡掉了他的代码!(体会到毒瘤出题人的艰辛)
接下来我们再来看看相对与计算无关的第二题吧。神题还是一模一样,只不过水题的人不打算使用自然溢出了,而是打算用模大质数的方法(这是剽窃我的代码啊喂)。而且喜欢大数字的他打算加大哈希池,令 M=109+7M=109+7 。他的代码是这样的:
typedef unsigned long long u64;
const int MaxN = 100000;
inline int hash_handle(const char *s, const int &n, const int &l, const int &base)
{
const int Mod=1000000007;
u64 hash_pow_l=1;
for(int i=1;i<=l;i++)
hash_pow_l=(hash_pow_l*base)%Mod;
int li_n=0;
static int li[MaxN];
u64 val=0;
for(int i=0;i<l;i++)
val=(val*base+s[i]-'a')%Mod;
li[li_n++]=val;
for(int i=l;i<n;i++)
{
val=(val*base+s[i]-'a')%Mod;
val=(val+Mod-((s[i-l]-'a')*hash_pow_l)%Mod)%Mod;
li[li_n++]=val;
}
sort(li,li+li_n);
li_n =unique(li,li+li_n)-li;
return li_n;
}
- 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
这道题的做法也不难,但是要考验你的欧气了,先来了解一个神奇的悖论:
那么同样的,如果你用大量的数据去卡他,他不就废了吗?不过如果你足够非,你还是可以WA到飞起的。
KMP算法
简介
三名算法大佬克努特( D.E.KnuthD.E.Knuth 算法。网上对于这种算法有很多的讲解,而讲的最好的非这一篇莫属了。感谢这位大佬详尽的讲解。摘录于此,方便大家学习:
1. 引言
本 KMPKMP 的理解始终不够,故才迟迟没有修改本文。
然近期因开了个算法班,班上专门讲解数据结构、面试、算法,才再次仔细回顾了这个 KMPKMP ,在综合了一些网友的理解、以及算法班的两位讲师朋友曹博、邹博的理解之后,写了