哈希,kmp,trie树和AC自动机
1.哈希
字符串哈希实际上就是把一个字符串转化为一个数字
比如 \(abc=1*2^2+2*2^1+3*2^0\) (也就是把 a 映射为1,b 映射为2,c 映射为3)
然后就没啥了。。。
关于自然溢出:
但在有时候,会出现两个字符串不相同但是整数相同的情况,此时我们把“进制”取为131/1331/13331...,模数一般取为 \(2^{64}-1\) ,因为 \(unsigned\) \(long\) \(long\) 可以自然溢出,所以就不用取模了。
核心代码:
1.准备工作
p[0]=1;
for(reg int i=1;i<N;i++)
{
p[i]=p[i-1]*P;
}
for(reg int i=1;i<=size2;i++)
{
h[i]=h[i-1]*P+(ull)s2[i]-'a';
}
2.求l,r区间的哈希值
ull gethash(int l,int r)
{
return h[r]-h[l-1]*mi[r-l+1];
}
2.KMP
显然,我还没有学懂
upd on:2024/7/2
这个算法主要解决的是字符串中关键字搜索。
暴力:从左到右一个一个匹配,时间复杂度为 \(O(nm)\) ,不够优秀。
KMP算法:利用已经部分匹配的有效信息,只修改模式串的指针,让模式串尽量移动到有效位置。
由于我没有图并且自己也不想画,下面的请自行脑补,见谅。
会发现在主串(i 指针)和模式串(j 指针)不匹配的情况下 \(j\) 指针要移动到 最长相同前后缀 的 前缀 的下一个位置(假设这一位为 \(k\) )。因为前后缀都相同了,所以就不用匹配了,直接从下一个位置开始,利用了已经匹配的有效信息。
数学公式表达:\(p[0\) ~ \(k-1]==p[j-k\) ~ \(j-1]\)(p 为模式串)
定义: \(next\) 数组表示当 \(i,j\) 指针失配时 \(j\) 要跳转的位置。
当 \(p[k]==p[j]\) 的时候,\(next[j+1]=next[j]+1\)。
void getnxt(int next[],string s)
{
int j=0;
for(int i=2,j=0;i<=l;i++)
{
while(j&&s[i]!=s[j+1])
{
j=next[j];
}
if(s[i]==s[j+1])
{
j++;
}
next[i]=j;
num[i]=num[j]+1;
}
}
int KMP(string s,string t)
{
int next[10], i=0, j=0;
getnext(next,t);
while(i<s.size()&&j<t.size())
{
if(j==-1||s[i]==t[j])
{
i++;
j++;
}
else j=next[j];
}
if(j>=s.size()) return i-s.size();
else return -1;
}
3.trie树
差不多就是把一些字符串转化为一棵树
是一种用于快速查询某个字符串/字符前缀是否存在的数据结构。
插入/生成
//idx代表当前字符的编号,根节点为0
//son数组一维下标是父节点的idx,二维下标是这个父节点的直接子节点的str[i]-'a'的值
//cnt数组表示以该idx结尾的字符串的个数,例如:有几个'abc'的字符串
void insert(char str[])
{
int p=0;
for(int i=0;str[i];i++)
{
int u=str[i]-'a';
if(!son[p][u]) son[p][u]=++idx;
p=son[p][u];
}
cnt[p]++;
}
查询
int query(char str[])//查询字符串出现的次数
{
int p=0;
for(int i=0;str[i];i++)
{
int u=str[i]-'a';
if(!son[p][u]) return 0;
p=son[p][u];
}
return cnt[p];
}
例如:\(sea,she,sell\) 我们可以得到下面的\(Trie\):
关于trie树,第2,3两道题,推荐这篇题解 》》》( ⊙ o ⊙ )啊!
4.AC自动机
由于前面学的知识都忘了,一整个复习+回顾代码,发现很难肝,顺便修整了博客(
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5, M=1e6+5;
int n, son[N][26], cnt[N], idx, T;
char s[100], s1[M];
int fail[N];//失配指针
void insert(char str[])
{
int p=0;
for(int i=0;str[i];i++)
{
int u=str[i]-'a';
if(!son[p][u]) son[p][u]=++idx;
p=son[p][u];
}
cnt[p]++;
}
void build()//建AC自动机
{
queue<int> q;
for(int i=0;i<26;i++)
{
if(son[0][i]) q.push(son[0][i]);
}
while(q.size())
{
int u=q.front();//用于BFS遍历字典树
q.pop();
for(int i=0;i<26;i++)
{
int v=son[u][i];
if(v) //存在
{
fail[v]=son[fail[u]][i];//失配指针就是父节点的失配指针的同样的儿子
q.push(v);
}
else son[u][i]=son[fail[u]][i];//一种特殊处理,很像并查集里的路径压缩
}
}
}
int query(char str[])
{
int ans=0, u=0;
for(int i=0;str[i];i++)
{
u=son[u][str[i]-'a'];
for(int j=u;j&&cnt[j]!=-1;j=fail[j])
{
ans+=cnt[j], cnt[j]=-1;//防止重复计算
}
}
return ans;
}
int main()
{
cin>>T;
while(T--)
{
memset(fail, 0, sizeof(fail));
memset(son, 0, sizeof(son));
memset(cnt, 0, sizeof(cnt));
cin>>n;
for(int i=0;i<n;i++)
{
cin>>s;
insert(s);
}
cin>>s1;
build();
cout<<query(s1)<<endl;
}
return 0;
}