字符串系列——KMP、AC自动机、回文自动机
2018-02-03 20:28
351 查看
KMP
code
例题
题解
code
AC自动机
code
例题
题解
code
回文自动机
例题
题解
code
参考资料
个人感觉字符串系列是比较蛋疼的算法(相对于我来说)。。。
设匹配串长度为n,模式串长度为m。
显然暴力的时间复杂度是O(nm)
但是想想可以发现,每次匹配时一旦失配所有的相同信息全部丢掉。
其中绿色部分是已匹配部分,红色则是失配部分
我们假设四块蓝色部分都相同
那么可以直接这样匹配
实际上就是从这里开始
至于预处理前后段相同部分,其实类似上面,只不过是自己匹配自己
如果失配就不断迭代,具体不细述因为太水
时间复杂度O(n+m)
Description
涛涛最近准备要结婚了,但这在这之前他需要买套房子。买房子的确是人生大事哟,所以涛涛要好好斟酌。
于是他去房屋中介网上爬到了各种房子的数据,并得到了这些房子的特征,但是现在有一个问题感到很困惑, 但他知道你编程贼 6,所以希望你能帮帮他。
现在有 N 幢房子,每幢房子用一个字符串 si 来描述。但同样的房子不同的开发商会用不同的词汇来描述。
某些字符串存在缩写,例如 swimmingpool 可以简写为 pool 。
现在有 M 条特征的简写规则,每条规则包含两个字符串 ai , bi , 表示将所有子串中的 ai 替换成 bi。
一个字符串可能会被同一条规则匹配多次,优先替换最左边的,且新生成的字符串会不会被重新用于该规则 的匹配。不同的规则之间按照严格的顺序关系执行 (详见样例)。
现在你需要对已有的 N 条字符串通过 M 条有顺序的替换规则进行缩写。
Input
第一行有两个正整数 N,M,代表 N 幢房子,和 M 条替换规则。
接下来 N 行,每行一个字符串 si
接下来 M 行,每行两个字符串 ai , bi,中间用空格隔开。
保证所有输入的字符串只会出现小写字母。
Output
输出 N 行每行一个字符串,代表特征替换后的字符串。
Sample Input
Sample Input1:
1 1
aaaaaaa
aaa ba
Sample Input2:
1 1
ababababc
aba a
Sample Input3:
3 3
swimmingswimmingpool
catallow
dogallow
cat pet
dog pet
swimmingpool pool
Sample Input4:
2 3
aaaabbb
bbbbaaa
aaaa cc
cbbb a
bbbb a
Sample Output
Sample Output1:
babaa
Sample Output2:
ababc
Sample Output3:
swimmingpool
petallow
petallow
Sample Output4:
ca
aaaa
Data Constraint
20% 的数据:1 ≤ |si |, |ai |, |bi | ≤ 100 (|s| 表示字符串 s 的长度)
50% 的数据:1 ≤ |si |, |ai |, |bi | ≤ 30000
100% 的数据:1 ≤ |si |, |ai |, |bi | ≤ 100000, 1 ≤ N, M ≤ 20, |ai | ≥ |bi |。
可以支持多模式串匹配(相比之下,KMP只能支持单模式串匹配)
思想类似在trie上建KMP
(不懂trie可以自己去找资料或脑补)
AC自动机中最重要的思想就是fail指针。
定义fail[x]=y,则满足y节点是x节点的最长后缀
比如”bac“是”aba bac“的后缀。
特殊的,如果x节点是根节点的儿子,则将fail[x]设为根节点。
那么上图的fail指针如下图所示
fail指针类似KMP的next数组,从父节点不断往上跳,如果跳到某个节点有和当前结点一样的儿子,就把fail设为那个儿子。
所以AC自动机=trie+KMP
摘自http://blog.csdn.net/a_crazy_czy/article/details/48029883
设匹配串长度为n,模式串共m个,第i个记为si。
可以证明AC自动机时间复杂度为O(n+∑length(si))
Description
给定k个字符串以及长度为n的母串可选字母的集合,问母串要完整出现给定的k个字符串的方案数,答案模1000000007,字符仅包含小写字母。
Input
第一行两个整数n、k,表示字符串的长度和给定字符串的个数。
接下来k行每行一个字符串。
接下来一行1个整数m表示可选字母集合内元素个数。
接下来一行给出一个长为m的字符串,表示字母的集合(可能有重复)。
Output
一个整数ans,表示方案数。
Sample Input
3 2
cr
rh
4
acrh
Sample Output
1
【样例解释】
只有crh符合。
Data Constraint
30%的数据n<=10,m<=3。
60%的数据n<=40。
另有10%的数据k=0。
另有10%的数据m=1。
100%的数据n<=100,m<=10,k<=8,给定字符串长度<=30。
设f[i][j][k]表示当前枚举到字符串第i位,在AC自动机上位置为j,匹配成功的字符串状态为k(状压)
然后建好AC自动机后搞一遍就行了。
1、求回文子串的种类。
2、求每种回文子串出现次数。
3、求匹配串的前缀中的回文子串。
4、求以下标i为结尾的回文子串个数。
思想跟AC自动机类似,每个节点都代表一个回文串
则每个节点都可以向两边同时加一个字符来变成新的回文串。
定义fail[x]=y表示x的最长后缀位置是y
因为回文串分奇偶性,所以定义两个根
偶数根长度为0,奇数根长度为-1(没错就是-1,因为可以通过扩展得到长度为1的串)
然后偶数根的fail设为奇数根。
每次从当前节点(初始设为偶数根)扩展时,沿着fail指针一直跳,直到发现某个串可以扩展就扩展。
如果扩展了节点,那么新节点的cnt(计数)设为1,否则+1
每次扩展长度+2
如果扩展了一个新节点,怎样求它的fail指针?
比如说现在找到了一个可以扩展的节点
那么它的后缀一定是这样的
根据回文的性质,蓝色部分一定是一个回文串
所以只需要沿着fail指针继续向上跳来找一个能扩展的点
能扩展的点不仅是要有相应的儿子,还要能在当前情况下扩展
(就是上面这点坑了我一个小时)
找到后把fail指针设为其儿子。
(如果没有找到就把fail设为偶数根)
还有一点,因为每个长串包含了短串,所以最后要从后往前沿着fail来累加cnt。
其实理解了AC自动机后学这个并不难
时间复杂度O(|S|)即O(n)
然而我并不会证
也就是本算法的出处
(补充一下,回文树是由战斗民族的大佬于2014年发明的)
Description
考虑一个只包含小写拉丁字母的符串 s。我们定义 s的一个子串 t的“出现值”为 t在 s中的出现次数乘以t的长度。 请你求出s的所有 回文子串中的最大出现值。
Input
输入只有一行,为一个只包含小写字母 (a−z) 的非空字符串 s。
Output
输出 一个整数,为 所有 回文子串 的最大 出现 值。
Sample Input
输入1:
abacaba
输入2:
www
Sample Output
输出1:
7
输出2:
4
论如何优雅的处理回文串 - 回文自动机详解.
回文树介绍(Palindromic Tree)
code
例题
题解
code
AC自动机
code
例题
题解
code
回文自动机
例题
题解
code
参考资料
个人感觉字符串系列是比较蛋疼的算法(相对于我来说)。。。
KMP
给出匹配串和模式串,求模式串在匹配串中出现的位置。设匹配串长度为n,模式串长度为m。
显然暴力的时间复杂度是O(nm)
但是想想可以发现,每次匹配时一旦失配所有的相同信息全部丢掉。
其中绿色部分是已匹配部分,红色则是失配部分
我们假设四块蓝色部分都相同
那么可以直接这样匹配
实际上就是从这里开始
至于预处理前后段相同部分,其实类似上面,只不过是自己匹配自己
如果失配就不断迭代,具体不细述因为太水
时间复杂度O(n+m)
code
#include <iostream> #include <cstdio> #include <cstring> #define fo(a,b,c) for (a=b; a<=c; a++) #define fd(a,b,c) for (a=b; a>=c; a--) using namespace std; char s[100000]; char t[100000]; int next[100000]; int i,j,k,l,len; int main() { scanf("%s",s); scanf("%s",t); len=strlen(t)-1; j=-1; next[0]=-1; fo(i,1,len) { while ((j>-1) && (t[j+1]!=t[i]))//不断迭代 j=next[j]; if (t[j+1]==t[i])//如果匹配成功就扩展 j++; next[i]=j; } j=-1; fo(i,0,strlen(s)-1) { while ((j>-1) && (t[j+1]!=s[i]))//不断迭代 j=next[j]; if (t[j+1]==s[i])//如果匹配成功就扩展 j++; if (j==len)//找到就输出,并重新匹配 { cout<<i-len<<endl; j=next[j]; } } return 0; }
例题
JZOJ5096. 【GDOI2017 day1】房屋购置Description
涛涛最近准备要结婚了,但这在这之前他需要买套房子。买房子的确是人生大事哟,所以涛涛要好好斟酌。
于是他去房屋中介网上爬到了各种房子的数据,并得到了这些房子的特征,但是现在有一个问题感到很困惑, 但他知道你编程贼 6,所以希望你能帮帮他。
现在有 N 幢房子,每幢房子用一个字符串 si 来描述。但同样的房子不同的开发商会用不同的词汇来描述。
某些字符串存在缩写,例如 swimmingpool 可以简写为 pool 。
现在有 M 条特征的简写规则,每条规则包含两个字符串 ai , bi , 表示将所有子串中的 ai 替换成 bi。
一个字符串可能会被同一条规则匹配多次,优先替换最左边的,且新生成的字符串会不会被重新用于该规则 的匹配。不同的规则之间按照严格的顺序关系执行 (详见样例)。
现在你需要对已有的 N 条字符串通过 M 条有顺序的替换规则进行缩写。
Input
第一行有两个正整数 N,M,代表 N 幢房子,和 M 条替换规则。
接下来 N 行,每行一个字符串 si
接下来 M 行,每行两个字符串 ai , bi,中间用空格隔开。
保证所有输入的字符串只会出现小写字母。
Output
输出 N 行每行一个字符串,代表特征替换后的字符串。
Sample Input
Sample Input1:
1 1
aaaaaaa
aaa ba
Sample Input2:
1 1
ababababc
aba a
Sample Input3:
3 3
swimmingswimmingpool
catallow
dogallow
cat pet
dog pet
swimmingpool pool
Sample Input4:
2 3
aaaabbb
bbbbaaa
aaaa cc
cbbb a
bbbb a
Sample Output
Sample Output1:
babaa
Sample Output2:
ababc
Sample Output3:
swimmingpool
petallow
petallow
Sample Output4:
ca
aaaa
Data Constraint
20% 的数据:1 ≤ |si |, |ai |, |bi | ≤ 100 (|s| 表示字符串 s 的长度)
50% 的数据:1 ≤ |si |, |ai |, |bi | ≤ 30000
100% 的数据:1 ≤ |si |, |ai |, |bi | ≤ 100000, 1 ≤ N, M ≤ 20, |ai | ≥ |bi |。
题解
直接搞就行了。。。code
由于是N久前Pascal写的,所以可(wu)能(bi)不优美var a:array[1..20,0..200000] of longint; b:array[1..20,0..200000] of longint; s1,s2:array[0..200000] of longint; next:array[0..200000] of longint; n,m,i,j,k,l,len,ii,last:longint; bz,bz2:boolean; ch:char; begin assign(Input,'house.in'); reset(Input); assign(Output,'house.out'); rewrite(Output); readln(n,m); for i:=1 to n do begin read(ch); while ch in['a'..'z'] do begin inc(a[i,0]); a[i,a[i,0]]:=ord(ch); read(ch); end; readln; end; for i:=1 to m do begin s1[0]:=0; s2[0]:=0; read(ch); while ch<>' ' do begin inc(s1[0]); s1[s1[0]]:=ord(ch); read(ch); end; read(ch); while ch in['a'..'z'] do begin inc(s2[0]); s2[s2[0]]:=ord(ch); read(ch); end; readln; k:=0; for j:=2 to s1[0] do begin while (k>0) and (s1[k+1]<>s1[j]) do k:=next[k]; if s1[k+1]=s1[j] then inc(k); next[j]:=k; end; for k:=1 to n do begin l:=0; len:=a[k,0]; a[k,0]:=0; while l<=len do begin bz:=false; bz2:=false; last:=l; j:=0; while (j<s1[0]) and (l<=len) do begin inc(l); while (j>0) and (a[k,l]<>s1[j+1]) do j:=next[j]; if a[k,l]=s1[j+1] then inc(j); end; if j<s1[0] then break; j:=next[j]; for ii:=last+1 to l-s1[0] do begin inc(a[k,0]); a[k,a[k,0]]:=a[k,ii]; end; for ii:=1 to s2[0] do begin inc(a[k,0]); a[k,a[k,0]]:=s2[ii]; end; end; if last<len then for l:=last+1 to len do begin inc(a[k,0]); a[k,a[k,0]]:=a[k,l]; end; end; end; for i:=1 to n do begin for j:=1 to a[i,0] do write(chr(a[i,j])); writeln; end; close(Input); close(Output); end.
AC自动机
全称是Aho-Corasick可以支持多模式串匹配(相比之下,KMP只能支持单模式串匹配)
思想类似在trie上建KMP
(不懂trie可以自己去找资料或脑补)
AC自动机中最重要的思想就是fail指针。
定义fail[x]=y,则满足y节点是x节点的最长后缀
比如”bac“是”aba bac“的后缀。
特殊的,如果x节点是根节点的儿子,则将fail[x]设为根节点。
那么上图的fail指针如下图所示
fail指针类似KMP的next数组,从父节点不断往上跳,如果跳到某个节点有和当前结点一样的儿子,就把fail设为那个儿子。
所以AC自动机=trie+KMP
摘自http://blog.csdn.net/a_crazy_czy/article/details/48029883
设匹配串长度为n,模式串共m个,第i个记为si。
可以证明AC自动机时间复杂度为O(n+∑length(si))
code
#include <iostream> #include <cstdio> #include <cstring> #define fo(a,b,c) for (a=b; a<=c; a++) #define fd(a,b,c) for (a=b; a>=c; a--) using namespace std; int n,i,j,k,l,len,h,t; char s[1000]; char tt[1000]; int T[100000][26]; char ch[100000]; bool bz[100000]; int fa[100000]; int fail[100000]; int d[100000]; void _new(int t,char c) { len++; T[t][int(c)-97]=len; fa[len]=t; ch[len]=c; } void _printf(int t) { if (t>1) _printf(fa[t]); else { printf("\n"); printf("%d\n",i+1); return; } printf("%c",ch[t]); } int main() { scanf("%d",&n); scanf("%s",s); len=1; fo(i,1,n)//建trie { scanf("%s",tt); k=1; fo(j,0,strlen(tt)-1) { if (!T[k][int(tt[j])-97]) _new(k,tt[j]); k=T[k][int(tt[j])-97]; } bz[k]=1; } fail[1]=1; fo(i,0,25)//初始化根节点的儿子的fail fail[T[1][i]]=1; h=0; t=1; d[1]=1; while (h<t)//建fail指针 { h++; fo(i,0,25) if (T[d[h]][i]) { d[++t]=T[d[h]][i]; if (d[h]>1) { j=fail[d[h]]; while ((j>1) && (!T[j][i]))//不断迭代 j=fail[j]; if (T[j][i]) fail[d[t]]=T[j][i]; else fail[d[t]]=1; } } } j=1; fo(i,0,strlen(s)-1)//匹配 { k=int(s[i])-97; while ((j>1) && (!T[j][k])) j=fail[j]; if (T[j][k])//如果找到就往下走 { j=T[j][k]; if (bz[j]) _printf(j); } l=j; while (l>1)//判断当前位置的后缀是否存在于模式串中(可能会重叠) { l=fail[l]; if (bz[l]) _printf(l); } } return 0; }
例题
JZOJ3472. 【NOIP2013模拟联考8】匹配(match)Description
给定k个字符串以及长度为n的母串可选字母的集合,问母串要完整出现给定的k个字符串的方案数,答案模1000000007,字符仅包含小写字母。
Input
第一行两个整数n、k,表示字符串的长度和给定字符串的个数。
接下来k行每行一个字符串。
接下来一行1个整数m表示可选字母集合内元素个数。
接下来一行给出一个长为m的字符串,表示字母的集合(可能有重复)。
Output
一个整数ans,表示方案数。
Sample Input
3 2
cr
rh
4
acrh
Sample Output
1
【样例解释】
只有crh符合。
Data Constraint
30%的数据n<=10,m<=3。
60%的数据n<=40。
另有10%的数据k=0。
另有10%的数据m=1。
100%的数据n<=100,m<=10,k<=8,给定字符串长度<=30。
题解
状压Dp+AC自动机设f[i][j][k]表示当前枚举到字符串第i位,在AC自动机上位置为j,匹配成功的字符串状态为k(状压)
然后建好AC自动机后搞一遍就行了。
code
#include <iostream> #include <cstdio> #include <cstring> #define fo(a,b,c) for (a=b; a<=c; a++) #define fd(a,b,c) for (a=b; a>=c; a--) #define mod 1000000007 using namespace std; int p[9]={0,1,2,4,8,16,32,64,128}; int n,m,i,j,k,l,len,h,t,L,J,K,I,ii; bool ch[26]; char tt[1000]; int T[90][26]; int bz[90]; int CH[90]; int fa[90]; int fail[90]; int d[90]; int f[101][90][256]; long long ans; char Ch; void _new(int t,char c) { len++; T[t][int(c)-97]=len; fa[len]=t; } void Aho_Corasick() { scanf("%d%d",&n,&l); L=p[l]*2-1; len=1; fo(i,1,l) { scanf("%s",tt); k=1; fo(j,0,strlen(tt)-1) { if (!T[k][int(tt[j])-97]) _new(k,tt[j]); k=T[k][int(tt[j])-97]; } bz[k]=i; } fail[1]=1; fo(i,0,25) fail[T[1][i]]=1; h=0; t=1; d[1]=1; while (h<t) { h++; fo(i,0,25) if (T[d[h]][i]) { d[++t]=T[d[h]][i]; if (d[h]>1) { j=fail[d[h]]; while ((j>1) && (!T[j][i])) j=fail[j]; if (T[j][i]) fail[d[t]]=T[j][i]; else fail[d[t]]=1; } } } } int main() { Aho_Corasick(); scanf("%d\n",&m); fo(i,1,m) { scanf("%c",&Ch); CH[i]=int(Ch)-97; } if (len==1) { ans=1; fo(i,1,n) ans=(ans*m)%mod; printf("%d\n",ans); return 0; } f[0][1][0]=1; fo(i,0,n-1) { fo(j,1,len) { fo(k,0,L) if (f[i][j][k]) { fo(ii,1,m) { l=CH[ii]; J=j; K=k; while ((J>1) && (!T[J][l])) J=fail[J]; if (T[J][l]) J=T[J][l]; I=J; while (I>1) { if (bz[I]) K|=p[bz[I]]; I=fail[I]; } f[i+1][J][K]=(f[i+1][J][K]+f[i][j][k])%mod; } } } } ans=0; fo(i,1,len) ans=(ans+f [i][L])%mod; printf("%d\n",ans); return 0; }
回文自动机
用来处理回文子串的问题。1、求回文子串的种类。
2、求每种回文子串出现次数。
3、求匹配串的前缀中的回文子串。
4、求以下标i为结尾的回文子串个数。
思想跟AC自动机类似,每个节点都代表一个回文串
则每个节点都可以向两边同时加一个字符来变成新的回文串。
定义fail[x]=y表示x的最长后缀位置是y
因为回文串分奇偶性,所以定义两个根
偶数根长度为0,奇数根长度为-1(没错就是-1,因为可以通过扩展得到长度为1的串)
然后偶数根的fail设为奇数根。
每次从当前节点(初始设为偶数根)扩展时,沿着fail指针一直跳,直到发现某个串可以扩展就扩展。
如果扩展了节点,那么新节点的cnt(计数)设为1,否则+1
每次扩展长度+2
如果扩展了一个新节点,怎样求它的fail指针?
比如说现在找到了一个可以扩展的节点
那么它的后缀一定是这样的
根据回文的性质,蓝色部分一定是一个回文串
所以只需要沿着fail指针继续向上跳来找一个能扩展的点
能扩展的点不仅是要有相应的儿子,还要能在当前情况下扩展
(就是上面这点坑了我一个小时)
找到后把fail指针设为其儿子。
(如果没有找到就把fail设为偶数根)
还有一点,因为每个长串包含了短串,所以最后要从后往前沿着fail来累加cnt。
其实理解了AC自动机后学这个并不难
时间复杂度O(|S|)即O(n)
然而我并不会证
例题
JZOJ3654. 【APIO2014】回文串也就是本算法的出处
(补充一下,回文树是由战斗民族的大佬于2014年发明的)
Description
考虑一个只包含小写拉丁字母的符串 s。我们定义 s的一个子串 t的“出现值”为 t在 s中的出现次数乘以t的长度。 请你求出s的所有 回文子串中的最大出现值。
Input
输入只有一行,为一个只包含小写字母 (a−z) 的非空字符串 s。
Output
输出 一个整数,为 所有 回文子串 的最大 出现 值。
Sample Input
输入1:
abacaba
输入2:
www
Sample Output
输出1:
7
输出2:
4
题解
裸题瞎搞。code
#include <iostream> #include <cstdio> #include <cstring> #define fo(a,b,c) for (a=b; a<=c; a++) #define fd(a,b,c) for (a=b; a>=c; a--) #define max(x,y) (x>y?x:y) using namespace std; int tr[300010][26]; int fa[300010]; long long len[300010]; long long cnt[300010]; char s[300010]; int i,j,k,l,n,last,L; long long ans; char ch[300010]; int f[300010]; void New(int t,int x) { n++; tr[t][x]=n; len =len[t]+2; ch =char(x+97); f =t; } int main() { freopen("palindrome.in","r",stdin); freopen("palindrome.out","w",stdout); scanf("%s",&s); L=strlen(s); fd(i,L,1) s[i]=s[i-1]; s[0]=' '; n=1; fa[0]=1; fa[1]=1; len[0]=0; len[1]=-1; last=0; fo(i,1,L) { k=int(s[i])-97; while (s[i]!=s[i-len[last]-1])//不断迭代查找 last=fa[last]; j=fa[last]; while ((len[j]>-1) && (s[i]!=s[i-len[j]-1]))//继续向上扩展 j=fa[j]; j=tr[j][k]; if (!tr[last][k])//新建节点 { New(last,k); fa[tr[last][k]]=j; } last=tr[last][k];//从当前点转移到子树 cnt[last]++; } fd(i,n,2) { cnt[fa[i]]+=cnt[i];//累加答案 ans=max(len[i]*cnt[i],ans); } printf("%lld\n",ans); fclose(stdin); fclose(stdout); return 0; }
参考资料
Palindromic Tree——回文树【处理一类回文串问题的强力工具】论如何优雅的处理回文串 - 回文自动机详解.
回文树介绍(Palindromic Tree)
相关文章推荐
- 字符串系列(三)——匹配算法KMP
- 字符串系列1 Rabin-Karp, 有限自动机, KMP, 扩展 KMP
- 字符串专题(trie,KMP,AC自动机,manacher)
- 字符串 KMP Trie AC自动机 后缀数组
- KMP求匹配字符串位置
- 挑战程序竞赛系列(63):4.7字符串上的动态规划(1)
- 字符串系列2 Manacher 算法
- 算法系列之四:字符串的相似度
- PHP学习系列(1)——字符串处理函数(5)
- Boost学习系列5-字符串处理-(下)
- [LeetCode] Valid Anagram - 字符串排序比较系列
- 字符串(扩展KMP):HDU 4333 Revolving Digits
- [BZOJ]4974: 字符串大师 KMP
- 逆序输出字符串的单词——Leetcode系列(二)
- KMP应用----求两个字符串的最长公共子串
- kmp的next数组的运用(求字符串的最小循环节)
- (原创)Python字符串系列(1)——str对象
- KMP字符串模式匹配详解
- KMP字符串模式匹配详解
- Solmyr 的小品文系列之一:字符串放在哪里?