哈希,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(int i=1;i<maxn;i++)
{
p[i]=p[i-1]*P;
}
for(int i=1;i<=n;i++)
{
h[i]=h[i-1]*P+(ull)s[i]-'a';
}
2.求l,r区间的哈希值
ull gethash(int l,int r)
{
return h[r]-h[l-1]*p[r-l+1];
}
注意,以上字符串是从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\)。
//以下是处理next数组
void getnext(string s,int nxt[])
{
int n=s.size();
int j=0;
nxt[0]=0;
for(int i=1;i<n;i++)
{
while(j>0&&s[i]!=s[j])
{
j=nxt[j-1];
}
if(s[i]==s[j]) j++;
nxt[i]=j;
}
}
//以下是匹配
void kmp(string s,string t)
{
int n=s.size(), m=t.size();
for(int i=0,j=0;i<n;i++)
{
while(j>0&&s[i]!=t[j]) j=nxt[j-1];
if(s[i]==t[j]) j++;
if(j==m)
{
j=nxt[j-1];
//i就是t串末尾
}
}
}
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自动机
由于前面学的知识都忘了,一整个复习+回顾代码,发现很难肝,顺便修整了博客(

黄色的边就是fail指针。
对于代码中类似路径压缩的那一部分:son[u][i]=son[fail[u]][i];

点击查看代码
#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;
}
AC自动机上dp大多为刷表法。
练习题
1. [HNOI2006] 最短母串问题
link:https://www.luogu.com.cn/problem/P2322
虽说是AC自动机+dp,但这个题bfs也是可以过的。观察到n的范围很小,考虑状压来记录,id[i] 表示trie树上以i结尾的字符串的 \(2^{编号}\)(i为时间戳),build好后跑bfs,维护一个st表示经过的字符串,并记下答案,若 st=(1<<n)-1 ,输出答案即可。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e7+5;
int n, son[1000][30], idx, T, id[1000], st, tot, vis[1000][(1<<13)+5], c[N], ans[N], fa[N];
char s[100000];
int fail[100000];
void insert(char str[],int pos)
{
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];
}
id[p]|=(1<<pos);
}
void build()
{
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();
q.pop();
for(int i=0;i<26;i++)
{
int v=son[u][i];
if(v)
{
fail[v]=son[fail[u]][i];
id[v]|=id[son[fail[u]][i]];
q.push(v);
}
else son[u][i]=son[fail[u]][i];
}
}
}
void bfs()
{
queue<pair<int,int> > q;
q.push(make_pair(0, 0));
vis[0][0]=0;
int num=0, cnt=0;
while(q.size())
{
int u=q.front().first, st=q.front().second;
q.pop();
if(st==((1<<n)-1))
{
int cnt1=0;
while(num)
{
c[++cnt1]=ans[num];
num=fa[num];
}
for(int i=cnt1;i>=1;i--) cout<<(char)(c[i]+'A');
return ;
}
for(int i=0;i<26;i++)
{
int v=son[u][i];
if(!v) continue;
if(!vis[v][st|id[v]])
{
vis[v][st|id[v]]=1;
q.push(make_pair(v, st|id[v]));
fa[++cnt]=num;
ans[cnt]=i;
}
}
num++;
}
}
int main()
{
cin>>n;
for(int i=0;i<n;i++)
{
cin>>s;
insert(s, i);
}
build();
bfs();
return 0;
}
2. 文本生成器
link:https://www.luogu.com.cn/problem/P4052
真正意义上的第一道AC自动机上跑dp。
正难则反,想到了。即 \(26^m\) 减去不合法的数量。
这种dp设定啥的一般比较套路,比如此题 \(dp[i][j]\) 表示当前在 j,填的串长为 i 的不合法数量,如果 \(son[u][i]\) 合法,\(dp[i+1][son[u][i]]+=dp[i][u]\) ,合法指的就是跳 fail 没有模式串。
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=65*26, mod=1e4+7;
int n, m, cnt[10005], dp[105][10005], son[10005][30], idx, ans=0, ban[10005];
char s[10005];
int fail[100000];
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];
}
ban[p]=1;
}
void build()
{
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();
q.pop();
for(int i=0;i<26;i++)
{
int v=son[u][i];
if(v)
{
fail[v]=son[fail[u]][i];
ban[v]|=ban[fail[v]];
q.push(v);
}
else son[u][i]=son[fail[u]][i];
}
}
}
int qpow(int a,int b)
{
int res=1;
while(b)
{
if(b&1) res=res*a%mod;
a=a*a%mod;
b>>=1;
}
return res;
}
signed main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>s;
insert(s);
}
dp[0][0]=1;
build();
for(int i=0;i<m;i++)
{
for(int j=0;j<=idx;j++)
{
for(int k=0;k<26;k++)
{
if(ban[son[j][k]]==0)
{
dp[i+1][son[j][k]]=(dp[i+1][son[j][k]]+dp[i][j])%mod;
}
}
}
}
ans=qpow(26, m)%mod;
for(int i=0;i<=idx;i++) ans=((ans-dp[m][i])%mod+mod)%mod;
cout<<ans%mod;
return 0;
}
3. [BZOJ 2905]背单词
link:https://www.gxyzoj.com/d/hzoj/p/P433
一个A如果是B的子串,那么必能从一个B的子串跳fail跳到A。所以对于第i个串的答案,就是i以前所有i的子串的答案的最大值+w[i](注意要和0取max)(代码实现的这一部分把串的字母顺序建了一条链)。然后把这个值加到fail树上i串末尾以及他的子树。最后统计即可。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define lid id<<1
#define rid id<<1|1
using namespace std;
const int maxn=5e5+5;
int n, T, w[maxn], pos[maxn], st[maxn], ed[maxn], idx, tot, fa[maxn], son[maxn][28];
string s[maxn];
int head[maxn], edgenum, fail[maxn];
struct edge{
int nxt;
int to;
}edge[maxn<<1];
void add(int from,int to)
{
edge[++edgenum].nxt=head[from];
edge[edgenum].to=to;
head[from]=edgenum;
}
void insert(string str,int id)
{
int p=0;
for(int i=0;str[i];i++)
{
int u=str[i]-'a';
if(!son[p][u]) son[p][u]=++tot, fa[tot]=p;
p=son[p][u];
}
pos[id]=p;
}
void build_ac()
{
queue<int> q;
while(q.size()) q.pop();
for(int i=0;i<26;i++)
{
if(son[0][i]) q.push(son[0][i]);
}
while(q.size())
{
int u=q.front();
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];
}
}
}
struct seg_tree{
int l, r, max, lazy;
}t[maxn<<2];
void pushup(int id)
{
t[id].max=max(t[lid].max, t[rid].max);
}
void build(int id, int l, int r)
{
t[id].l=l, t[id].r=r;
if(l==r)
{
t[id].max = 0;
return ;
}
int mid=(l+r)>>1;
build(lid, l, mid);
build(rid, mid+1, r);
pushup(id);
}
void pushdown(int id)
{
if(t[id].lazy&&t[id].l!=t[id].r)
{
t[lid].max=max(t[lid].max, t[id].lazy);
t[rid].max=max(t[rid].max, t[id].lazy);
t[lid].lazy=max(t[lid].lazy, t[id].lazy);
t[rid].lazy=max(t[rid].lazy, t[id].lazy);
t[id].lazy=0;
}
}
void modify(int id, int l, int r, int val)
{
pushdown(id);
if(t[id].l==l&&t[id].r==r)
{
t[id].max=max(t[id].max, val);
t[id].lazy=max(t[id].lazy, val);
return ;
}
int mid=(t[id].l+t[id].r)>>1;
if(r<=mid) modify(lid, l, r, val);
else if(l>mid) modify(rid, l, r, val);
else modify(lid, l, mid, val), modify(rid, mid+1, r, val);
pushup(id);
}
int query(int id, int x)
{
pushdown(id);
if(t[id].l==t[id].r&&t[id].l==x)
{
return t[id].max;
}
int mid=(t[id].l+t[id].r)>>1;
if(x<=mid) return query(lid, x);
else return query(rid, x);
}
void dfs(int u,int fa)
{
if(u)
st[u]=++idx;
for(int i=head[u];i;i=edge[i].nxt)
{
int v=edge[i].to;
if(v==fa) continue;
dfs(v, u);
}
ed[u]=idx;
}
void clear()
{
tot=idx=edgenum=0;
memset(son, 0, sizeof(son));
memset(ed, 0, sizeof ed);
memset(st, 0, sizeof st);
memset(fa, 0, sizeof fa);
memset(head, 0, sizeof head);
memset(pos, 0, sizeof pos);
memset(w, 0, sizeof w);
memset(edge, 0, sizeof edge);
memset(fail, 0, sizeof fail);
memset(t, 0, sizeof t);
}
signed main()
{
//freopen("1.txt","r",stdin);
//freopen("2.txt","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin>>T;
while(T--)
{
cin>>n;
clear();
for(int i=1;i<=n;i++)
{
cin>>s[i]>>w[i];
insert(s[i], i);
}
build_ac();
for(int i=1;i<=tot;i++)//建fail树
{
add(fail[i], i);
}
dfs(0, -1);
build(1, 1, idx);
int ans=0;
for(int i=1;i<=n;i++)
{
int x=pos[i];
int res=0;
while(x)
{
res=max(res, query(1, st[x]));
x=fa[x];//所有前面的串的字母
}
modify(1, st[pos[i]], ed[pos[i]], max(0ll, max(res, res+w[i])));
ans=max({ans, max(0ll, max(res, res+w[i]))});
}
cout<<ans<<"\n";
}
return 0;
}
4. [JSOI2009] 密码
link:https://www.luogu.com.cn/problem/P4045
自己应该是能做出来的,但想复杂了,果然AC自动机的dp都很套路,设 \(dp[i][j][k]\) 表示到字符串第i位,AC自动机上j节点,模式串集合位k的方案数,注意用刷表法。转移很简单,和T2一样。
考虑输出,显然方案数≤42的直接用记忆化搜索判断可行性再输出即可,具体需要两个搜索:
点击查看代码
int dfs(int l,int u,int st)//记搜判断可行性
{
if(l==len)
{
vis[l][u][st]=1;
g[l][u][st]=(st==tot);
return g[l][u][st];
}
if(vis[l][u][st]) return g[l][u][st];
vis[l][u][st]=1;
int res=0;
for(int i=0;i<26;i++)
{
res|=dfs(l+1, son[u][i], st|id[son[u][i]]);
}
return g[l][u][st]=res;
}
void print(int l,int u,int st)
{
if(!g[l][u][st]) return ;
if(l==len)
{
for(int i=1;i<=l;i++) cout<<(char)(a[i]+'a');
cout<<endl;
return ;
}
for(int i=0;i<26;i++)
{
a[l+1]=i;
print(l+1, son[u][i], st|id[son[u][i]]);
}
}
5. [BJWC2011] 禁忌
link:https://www.luogu.com.cn/problem/P4569
AC自动机dp+矩阵快速幂。

拼尽全力无法战胜!!!记得回来看看我
总结
这个题单刷完了,总体来说(只AC自动机+dp这部分)dp状态很好想,转移也很套路,包括标记通过fail转移什么的(ban[u]|=ban[fail[u]];)没有太大问题,但是,我的矩阵快速幂学得很不好!我已经三次尝试去自己理解,但无法战胜,于是我决定暂时跳过这方面的题,我还会回来的。
即使所有人都放弃你,仍有几十亿细胞为你而活。

浙公网安备 33010602011771号