字符串学习总结(Hash & Manacher & KMP)
前言
终于开始学习新的东西了,总结一下字符串的一些知识。
NO.1 字符串哈希(Hash)
定义
即将一个字符串转化成一个整数,并保证字符串不同,得到的哈希值不同,这样就可以用来判断一个该字串是否重复出现过。
所以说\(Hash\)就是用来求字符串是否相同或者包含的。(包含关系就可以枚举区间,但是通常用\(KMP\),不会真的有人用看脸的\(Hash\)做字符串匹配吧,不会吧不会吧)。
实现
实现方式也是比较简单的,其实就是把一个字符串转化为数字进行比较,到这里可能有人就会说,直接比较长度和\(ASCII\)码不就行了,也是转化成数字啊(放屁)。这样显然是不行的,就好比说"ab"和“ba“,这两个显然不一样,但是如果按上边说的进行比较就是一样的,这样就错了,所以我们要换一种方式:改变一下进制。
如果是一个纯字符串的话,那么我们应该把进制调到大于\(131\),因为如果小于,就不能给每一种的字符一个值,那么正确性也就无法保证了。所以取一个\(233\),合情合理,还很sao(逃。因为这个值至少能保证不会炸。我们求出来每个字符串对应的数字,然后进行比较就好了。
对于哈希而言,我们认为对一个数取模后一样,那么就是一样的,所以可以偷点懒,也就是自然溢出,使用\(unsigned\ long\ long\),相当于自动对\(2^{64}\)取模,然后进行比较即可,当然,可以自己背一个\(10^{18}\)的质数进行取模(毕竟也是能卡的,也不知道哪个毒瘤会卡),各有优缺点。
代码
ull Hash(char s[]){//ull自然溢出
ull res = 0;
int len = strlen(s);
for(int i=0;i<len;++i){//计算每一位,用自己定义的进制base乘(也就是233 qwq)
res = (res*base + (ull)s[i])%mod;//这里我是取了个玄学mod
}
return res;
}
以上就是整个字符串之间的对比。下边说一说字符串里某个区间的对比
区间对比
意思就是直接给出你几个字符串,对比每个字符串里给定的区间\([l,r]\),这样的话如果直接一个个的扫,肯定会慢好多,如果直接求整个串然后相减,那么肯定是错误的,因为每一位都是要乘以一个进制的,如果直接计算,那么肯定就会乱掉,也就\(WA\)了。所以要用到之前说的东东:前缀和。
我们记录每一位的前缀和,而记算的时候需要乘以当前位的进制,这样就会避免上边说到的那种迷惑错误。记录的时候就照常按照前缀和记录,只需要最后改一下判断就行。
定义\(pw[len]\)为长度为\(len\)时的需要乘以的进制,前缀和就用\(sum\)来表示,求前缀和就是这样:
int main(){
cin>>s;
int len = strlen(s);
sum[0] = (ull)s[0];
for(int i=1;i<len;++i){
sum[i] = sum[i-1]*base+(ull)a[i];//乘以进制不能忘
}
}
下边是判断是否合法:
while(n--){
int l,r,s,t,len;
cin>>l>>r>>s>>t;
len = r-l+1;//计算第几位来乘以进制,pw数组提前可以快速幂处理好
if(sum[r] - sum[l-1]*pw[len] == sum[t]-sum[s-1]*pw[len])printf("YES\n");//如果这样计算出来值相等就合法
else printf("NO\n");
}
模板例题
例题代码
#include<bits/stdc++.h>
using namespace std;
#define ull unsigned long long
const ull mod = 1926081719260817;
const int maxn = 1e4+10;
ull base = 233;
int a[maxn];
char s[maxn];
ull Hash(char s[]){
ull res = 0;
int len = strlen(s);
for(int i=0;i<len;++i){
res = (res*base + (ull)s[i])%mod;
}
return res;
}
int main(){
int n;
scanf("%d",&n);
for(int i=1;i<=n;++i){
cin>>s;
a[i] = Hash(s);
}
int ans = 1;
sort(a+1,a+n+1);
for(int i=1;i<n;++i){
if(a[i] != a[i+1])ans++;
}
printf("%d\n",ans);
}
NO.2 Manacher算法
学长说很不常用,所以理解一个思想即可。
定义
\(1975\)年,\(Manacher\)发明了\(Manacher\)算法(中文名:马拉车算法),是一个可以在\(O(n)\)的复杂度中返回字符串\(s\)中最长回文子串长度的算法,十分巧妙。
例如这个字符串:“abaca”,它可以处理每一位的回文字串,以\(O(n)\)的效率处理最大值(当然还是有扩展的,只不过它不太常用,就只是分析一下算法过程)
实现
因为回文串分为奇回文串和偶回文串,处理起来比较麻烦,所以我们要用到一个小(sao)技(cao)巧(zuo),在每两个字符之间插入一个不会出现的字符,但是要求插入的字符一样,这样才能保证不影响回文串的长度。
举个例子:“abbadcacda”这个字符串,我们需要插入新的字符,这里用’#',那么就有了如下对应关系:
其中定义\(p[i]\)为以\(i\)为半径的回文半径,也就是从中心向两边最长能拓展多少,而根据这个可以推出来以它为中心的真正的回文串的长度。也就是\(p[i]-1\),根据这个就可以得到最长的回文串的长度了。
但是复杂度为什么是\(O(n)\)呢,那么就涉及到了他的实现方法,我们定义一个回文中心\(C\)和这个回文的右侧\(R\),也就是当前中心的最长回文的右端点,如果枚举到的\(i\)大于\(R\),那么直接更新就行,但是如果在里边,那么会分出来三种情况:
\(1\)、枚举到的\(i\)关于\(C\)对称到\(i'\),这时候\(i'\)的回文区域在\([L,R]\),那么\(i\)的回文半径就是\(i'\):
证明:因为此时的\([L,R]\)就是一个回文区间,所以左右对称过来是一样的,所以得到\(i\)的回文半径。
\(2\)、枚举到\(i\),此时对称点\(i'\)的回文区域超出了\(L\),那么\(i\)的回文区域就一定是从\(i\)到\(R\)。
证明:借用一张图片便于解释:
(图好丑……)首先我们设\(L\)点关于\(i'\)对称的点为\(L'\),\(R\)点关于\(i\)点对称的点为\(R'\),\(L\)的前一个字符为\(x\),\(L’\)的后一个字符为\(y\),\(k\)和\(z\)同理,此时我们知道\(L - L'\)是\(i'\)回文区域内的一段回文串,故可知\(R’ - R\)也是回文串,因为\(L - R\)是一个大回文串。所以我们得到了一系列关系,\(x = y,y = k,x != z\),所以 \(k != z\)。这样就可以验证出\(i\)点的回文半径是\(i - R\)。
\(3\)、\(i'\) 的回文区域左边界恰好和\(L\)重合,此时\(i\)的回文半径最少是\(i\)到\(R\),回文区域从\(R\)继续向外部匹配。
证明:因为 \(i'\) 的回文左边界和L重合,所以已知的\(i\)的回文半径就和\(i'\)的一样了,我们设\(i\)的回文区域右边界的下一个字符是\(y\),\(i\)的回文区域左边界的上一个字符是\(x\),现在我们只需要从\(x\)和\(y\)的位置开始暴力匹配,看是否能把\(i\)的回文区域扩大即可。
小小总结一下,其实就是先进行暴力匹配,然后根据\(i'\)回文区域和左边界的关系进行查找。
例题+代码
#include<bits/stdc++.h>
using namespace std;
const int maxn = 11e6;
char s[maxn];
int Manacher(char s[]){
int len = strlen(s);
if(len == 0)return 0;//长度为0就return
int len1 = len * 2 + 1;
char *ch = new char[len1];//动态数组
int *par = new int[len1];
int head = 0;
for(int i=0;i<len1;++i){
ch[i] = (i & 1) == 0 ? '#' : s[head++];//插入不一样的字符
}
int C = -1;
int R = -1;
int Max = 0;
par[0] = 1;
for(int i=0;i<len1;++i){//枚举三种情况
par[i] = (i < R)? min(par[C*2-i],R-i) : 1;//取最小的回文半径
while(i + par[i] < len1 && i - par[i] > -1&& ch[i + par[i]] == ch[i - par[i]]){//暴力匹配
par[i] ++ ;
}
if(i + par[i] > R){//如果超过右边界就更新
R = i + par[i];
C = i;
}
Max = max(Max,par[i]);//更新最大半径
}
delete[] ch;//清空动态数组
delete[] par;
return Max - 1;//因为这个是添了字符的最大回文半径,所以回文串的最长是它-1
}
int main(){
cin>>s;
cout<<Manacher(s);
return 0;
}
NO.3 KMP算法
正常我们查找字符串是否为子串的时候,往往都是暴力枚举,效率为\(O(n^2)\),但是字符串长了或者多了,肯定就是不行的了,所以有了\(KMP\)算法。
定义
\(KMP\)算法是一种改进的字符串匹配算法,由\(D.E.Knuth,J.H.Morris\)和\(V.R.Pratt\)同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称\(KMP\)算法)。\(KMP\)算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个\(next\)函数,函数本身包含了模式串的局部匹配信息。时间复杂度\(O(m+n)\)。
通俗的来说就是在需要匹配的那个串上给每个位置一个失配指针\(fail[j]\),表示在当前位置\(j\)失配的时候需要返回到\(fail[j]\)位置继续匹配,而这就是\(KMP\)算法优秀复杂度的核心。
实现
失配数组的匹配就是把需要查找的那个字符串进行一遍前缀和后缀之间的匹配。我们举个例子"ababa"这里真前缀分别为"a","ab","aba","abab",真后缀为"a","ba","aba","baba",找到他们的最大相同位置,就是\(fail\)指针,
我们设\(kmp[i]\) 用于记录当匹配到模式串的第 \(i\) 位之后失配,该跳转到模式串的哪个位置,那么对于模式串的第一位和第二位而言,只能回跳到 \(1\),因为是 \(KMP\)是要将真前缀跳跃到与它相同的真后缀上去(通常也可以反着理解),所以当 \(i=0\) 或者 \(i=1\) 时,相同的真前缀只会是 \(str1(0)\)这一个字符,所以\(kmp[0]=kmp[1]=1\)。
模板+代码
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e6+10;
char a[maxn],b[maxn];
int kmp[maxn];
int main(){
cin>>a+1>>b+1;
int lena = strlen(a+1);
int lenb = strlen(b+1);
int j = 0;
for(int i=2;i<=lenb;++i){//自己跟自己匹配处理出kmp数组
while(j && b[i] != b[j+1]){
j = kmp[j];
}
if(b[i] == b[j+1])j++;
kmp[i] = j;
}
j = 0;
for(int i=1;i<=lena;++i){
while(j && a[i] != b[j+1]){
j = kmp[j];
}
if(a[i] == b[j+1])j++;
if(j == lenb){//匹配完了就输出位置
printf("%d\n",i-lenb+1);
j = kmp[j];//返回失配位置
}
}
for(int i=1;i<=lenb;++i){
printf("%d ",kmp[i]);
}
return 0;
}
字符串学习总结(Hash & Manacher & KMP)的更多相关文章
- 「学习笔记」字符串基础:Hash,KMP与Trie
「学习笔记」字符串基础:Hash,KMP与Trie 点击查看目录 目录 「学习笔记」字符串基础:Hash,KMP与Trie Hash 算法 代码 KMP 算法 前置知识:\(\text{Border} ...
- 字符串的模板 Manacher kmp ac自动机 后缀数组 后缀自动机
为何scanf("%s", str)不需要&运算 经常忘掉的字符串知识点,最好不加&,不加&最标准,指针如果像scanf里一样加&是错的,大概是未定 ...
- E. Compress Words(Hash,KMP)
E. Compress Words time limit per test 1 second memory limit per test 256 megabytes input standard in ...
- hdu 4333"Revolving Digits"(KMP求字符串最小循环节+拓展KMP)
传送门 题意: 此题意很好理解,便不在此赘述: 题解: 解题思路:KMP求字符串最小循环节+拓展KMP ①首先,根据KMP求字符串最小循环节的算法求出字符串s的最小循环节的长度,记为 k: ②根据拓展 ...
- Redis支持的数据类型及相应操作命令:String(字符串),Hash(哈希),List(列表),Set(集合)及zset(sorted set:有序集合)
help 命令,3种形式: help 命令 形式 help @<group> 比如:help @generic.help @string.help @hash.help @list.hel ...
- CH1401 兔子与兔子【字符串】【HASH】
1401 兔子与兔子 0x10「基本数据结构」例题 描述 很久很久以前,森林里住着一群兔子.有一天,兔子们想要研究自己的 DNA 序列.我们首先选取一个好长好长的 DNA 序列(小兔子是外星生物,DN ...
- HDU 5763 Another Meaning dp+字符串hash || DP+KMP
题意:给定一个句子str,和一个单词sub,这个单词sub可以翻译成两种不同的意思,问这个句子一共能翻译成多少种不能的意思 例如:str:hehehe sub:hehe 那么,有**he.he** ...
- hpuoj回文串问题(manacher+kmp)
1699: 回文串问题 时间限制: 1 Sec 内存限制: 128 MB 提交: 22 解决: 3 [提交][状态][讨论版] 题目描述 还是回文串问题,字符串是啥,大家应该都知道,就是满足 S[ ...
- 【模板】字符串匹配的三种做法(Hash、KMP、STL)
题目描述 如题,给出两个字符串s1和s2,其中s2为s1的子串,求出s2在s1中所有出现的位置. 输入输出格式 输入格式: 第一行为一个字符串,即为s1 第二行为一个字符串,即为s2 输出格式: 1行 ...
随机推荐
- 小师妹学JVM之:JIT中的PrintCompilation
目录 简介 PrintCompilation 分析PrintCompilation的结果 总结 简介 上篇文章我们讲到了JIT中的LogCompilation,将编译的日志都收集起来,存到日志文件里面 ...
- python3 闭包函数 装饰器
闭包函数 1.闭:定义在函数内部的函数 2.包:内部函数引用了外部函数作用域的名字 在函数编程中经常用到闭包.闭包是什么,它是怎么产生的及用来解决什么问题呢.给出字面的定义先:闭包是由函数及其相关的引 ...
- 基于 Angular Material 的 Data Grid 设计实现
自 Extensions 组件库发布以来,Data Grid 成为了使用及咨询最多的组件.最开始 Data Grid 的设计非常简陋,经过一番重构,组件质量有了质的提升. Extensions 组件库 ...
- Python实用笔记 (18)面向对象编程——类和实例
类和实例 面向对象最重要的概念就是类(Class)和实例(Instance),必须牢记类是抽象的模板,比如Student类,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各 ...
- 关于 urlencode 的使用和 json 模块的介绍
先附上一段 “百度翻译” 的爬虫代码 # python爬虫实现百度翻译 # urllib和request POST参数提交 from urllib import request,parse impor ...
- 为什么 group by后面 必须跟selecte 后面的除了聚集函数外的所有字段
如:SELECT store_name, SUM(Sales) FROM Store_Information GROUP BY store_name 可以而SELECT store_name, add ...
- Flutter 中那么多组件,难道要都学一遍?
在 Flutter 中一切皆是 组件,仅仅 Widget 的子类和间接子类就有 350 多个,整理的 Flutter组件继承关系图 可以帮助大家更好的理解学习 Flutter,回归正题,如此多的组件到 ...
- .netcore 网站启动后 502.5
网站启动后,报错 HTTP Error 502.5 - ANCM Out-Of-Process Startup Failure 请检查安装的.netcore runtime版本和hosting版本是否 ...
- idea 快速生成返回值快捷方式
idea java快速生成返回值 ctrl+alt+V
- Python-模块XlsxWriter将数据写入excel
1.目的 用xlwt来生成excel的,生成的后缀名为xls,在xlwt中生成的xls文件最多能支持65536行数据.python XlsxWriter模块创建aexcel表格,生成的文件后缀名为.x ...