【算法】字符串
把字符串原样复制一遍放在后面是惯用套路,此时字符串数组开两倍!
★字符串算法的核心是构造失配指针!
【字符串哈希】
双蛤习取模保险,毕竟连自然溢出都是能卡的……
例题:【CodeForces】961 F. k-substrings 字符串哈希+二分
用于O(1)判断两个字符串是否相等:对于s[i~j],哈希值为h[j]-h[i]*p[j-i+1]。
用于O(log n)判断两个字符串大小(字典序),方法是二分求LCP,比较下一位。
用于O(n log2n )求后缀数组。
for(int i=1;i<=n;i++)h[i]=(h[i-1]*base+s[i])%p; for(int i=1;i<=n;i++)H[i]=(H[i-1]*Base+s[i])%P; if(h[y]-h[x-1]==h[b]-h[a-1]&&H[y]-H[x-1]==H[b]-H[a-1]);
【KMP】
KMP解决的是线性时间在模式串A中找到匹配串B的问题。
对于匹配串B的前i个字符构成的子串,既是它的后缀又是它的前缀的字符串中(它本身除外),最长的长度记作fail[i]。
比较时,如果A[i]=B[j+1],则j++,否则j=fail[j]。
fail[i]的实际含义就是此处匹配而下处失配时往前跳到一样的位置(即前缀=后缀),显然fail[1]=0(1处匹配2处失配,只能跳到0处),fail[0]没有意义。
预处理fail数组也可以视为匹配,如果B[i]=B[j+1],则fail[i]=++j,否则j=fail[j],继续比较。
(换一种角度看,当前需要计算fail[i],已知fail[i-1],判断fail[i]=fail[i-1]+1是否成立,否则判断fail[i]=fail[fail[i-1]]+1……)
记得kmp的预匹配必须从2开始循环,这样2可以和1比较,避免追上(比较到自身)。j=0就无处可跳了,不必再跳。
blog:http://www.matrix67.com/blog/archives/115
【BZOJ】1355 [Baltic2009]Radio Transmission 循环节
★upd:KMP的核心是强大的fail数组,表示的是后缀等于前缀的最大长度,这个性质非常强,这里只用于匹配时的快速失配跳后重新匹配。
#include<cstdio> #include<algorithm> #include<cstring> using namespace std; const int maxn=1000010,maxm=1010; char A[maxn],B[maxm]; int p[maxm],n,m; int main() { scanf("%s%s",A+1,B+1); n=strlen(A+1);m=strlen(B+1); p[1]=0; int j=0; for(int i=2;i<=m;i++) { while(j>0&&B[j+1]!=B[i])j=p[j]; if(B[j+1]==B[i])j++; p[i]=j; } j=0; for(int i=1;i<=n;i++) { while(j>0&&B[j+1]!=A[i])j=p[j]; if(B[j+1]==A[i])j++; if(j==m) { printf("%d\n",i-j+1); j=p[j]; } } for(int i=1;i<m;i++)printf("%d ",p[i]); printf("%d",p[m]); return 0; }
【AC自动机】识别字符串的自动机
AC自动机是对若干模式串O(m*26)建立trie和fail边,从而实现O(n)从新串中匹配到所有存在的模式串。
AC自动机中,不存在的节点直接指向fail节点处,存在的节点fail[ch[u][c]]=ch[fail[u]][c]。
询问的时候按位直接转移,失配会自动跳跃不用写出来,复杂度O(n)。
如果每次都要询问到0点的所有fail,须标记访问过的点不再访问,否则复杂度不对,参考aaaaa。
#include<cstdio> #include<cstring> #include<queue> using namespace std; const int maxn=1000010; int ch[maxn][26],val[maxn],fail[maxn],sz,ans; queue<int>Q; char s[maxn]; void insert(char *s){ int u=0,n=strlen(s); for(int i=0;i<n;i++){ int c=s[i]-'a'; if(!ch[u][c])ch[u][c]=++sz; u=ch[u][c]; } val[u]++; } void AC_build(){ for(int c=0;c<26;c++)if(ch[0][c])Q.push(ch[0][c]); while(!Q.empty()){ int u=Q.front();Q.pop(); for(int c=0;c<26;c++){ if(!ch[u][c])ch[u][c]=ch[fail[u]][c];else{ Q.push(ch[u][c]); fail[ch[u][c]]=ch[fail[u]][c]; //last[ch[u][c]]=val[fail[ch[u][c]]]?fail[ch[u][c]]:last[fail[ch[u][c]]]; } } } } void work(int u){if(fail[u]&&~val[fail[u]])work(fail[u]);ans+=val[u],val[u]=-1;} void find(char *s){ int n=strlen(s); int u=0; for(int i=0;i<n;i++){ u=ch[u][s[i]-'a']; if(~val[u])work(u); } } int main(){ int n; scanf("%d",&n); for(int i=1;i<=n;i++){ scanf("%s",s); insert(s); } AC_build(); scanf("%s",s); find(s); printf("%d",ans); return 0; }
trie的初始化可以用即化即用的方法,即访问到才初始化其子节点,保持旧版本和新版本有一层空白间隔。
last只能优化常数。
★upd:任何字符串数据结构都依赖于强大的fail机制。AC自动机的fail会带到最近的满足后缀=前缀的节点处,同时一个点在fail树上到根的路径就是匹配了这个点代表串的所有在AC-aho上的后缀。
【回文自动机】识别回文子串的自动机(PAM),又称”回文树“。
初始节点:ch[1]表示len=-1下接奇数串(为了方便,这里用1存节点-1),ch[0]表示len=0下接偶数串,0点指向-1点(之后拓展的所有节点都会先fail到0点再到1点)。
节点:每个点表示一个本质不同的回文串(从根到点组成的字符串是回文串中从中间到右端的串)
fail指针:每个点fail到相同后缀的次短回文串节点(显然最短到点0,然后才到-1),由于回文的性质次短回文子串代表节点一定已经出现过。
线性构造:(计算len)新加入一个字符a时,若a-1的最长回文串往前到b,则a的最长回文串至多到b-1,而能否到b-1取决于(s[a]==s[b-1])的真假。
所以从a-1代表的节点y开始不断fail直至满足s[a]==s[b-1]为止,就计算出了a的最长回文串(ch[x].len=ch[y].len+2),如果没有对应节点就新建(ch[y].t[x])。
(计算fail)若新建节点,构造x的fail指针只需从ch[y].fail开始再次找到满足s[a]==s[b-1]的为止。
复杂度O(n)。
注意:记得sz=1。
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn=100010; int fail[maxn],len[maxn],ch[maxn][300],n,length=0,nownode,sz; char s[maxn]; int getfail(int x){while(s[length-len[x]-1]!=s[length])x=fail[x];return x;} void insert(){ int y=s[++length]-'a'; int x=getfail(nownode); while(!ch[x][y]){ len[++sz]=len[x]+2; fail[sz]=ch[getfail(fail[x])][y]; ch[x][y]=sz; } nownode=ch[x][y]; } int main(){ scanf("%s",s+1); n=strlen(s+1); len[0]=0;fail[0]=1; len[1]=-1;fail[1]=1; sz=1;//!!! for(int i=1;i<=n;i++)insert(); int ans=0; for(int i=1;i<=sz;i++)ans=max(ans,len[i]); printf("%d",ans); return 0; }
应用:
1.每个点的访问次数是该回文串作为最长回文串的次数,由fail边反向建新树就可以由子树得到该回文串所有信息。
一点心得:其实字符串自动机写多了就会发现,都是一样的。
回文自动机和SAM是一样……一样是记录本质不同的回文串(子串),一样是n个点n条边构成n^2个串。
节点的本质一样是Right集合,不过这里的不同在于回文自动机的Right集合是所有以该串为最长回文串的右端点,所以一个串的出现次数是子树的和。
fail边一样是前面删字符直至能继续下去。
【后缀数组】SA
n 字符串长度 m字符值为1~m
x 字符值数组/名次数组(x[i]表示后缀i的对应名次)
y 第二关键字排名对应后缀
sa 第一关键字排名对应后缀(总排名)
base 基排数组 base[x[.]] 取排名 sa[base[x[.]]--]=. 排名赋值
因为后缀一定不可能相同,所以暂时相同时的排名先后没有影响。
每次基排赋给SA对于同组都是先赋值名次越低,这就是再根据第二关键字排名的本质。
x数组本质上是sa对应的rank数组,主要目的是判重和记录最新排名以备下一次基排。
x数组可以把最新排名SA中相同的挑出来赋给同一个排名值。
基排赋值给SA时记得自减!
过程:
初始基排得到SA
倍增
根据SA排出第二关键字y
根据y的倒序再次排序SA
根据原x和新的sa更新x
END
最后的x数组就是rank数组
计算LCP:h[i]表示SA中后缀i和后缀i-1的最长公共前缀。
按照h[i]≥h[i-1]-1,到SA[1]时自然会是0,不用担心。
void build_sa(int m) { //初始基排-4步 for(int i=1;i<=m;i++)base[i]=0;//初始化 for(int i=1;i<=n;i++)base[x[i]=s[i]+1]++;//累积 for(int i=2;i<=m;i++)base[i]+=base[i-1];//叠加排名 for(int i=n;i>=1;i--)sa[base[x[i]]--]=i;//排名赋值(愈前愈前,但无所谓) for(int k=1;k<=n;k<<=1)//倍增 { int p=0; //排序第二关键字 for(int i=n-k+1;i<=n;i++)y[++p]=i;//没有第二关键字默认为$ for(int i=1;i<=n;i++)if(sa[i]>k)y[++p]=sa[i]-k;//根据sa决定第二关键字排名,注意k即以后才能作为第二关键字 sa[i]-k取对应第一关键字(后缀) //排序第一关键字 for(int i=1;i<=m;i++)base[i]=0; for(int i=1;i<=n;i++)base[x[i]]++; for(int i=2;i<=m;i++)base[i]+=base[i-1]; for(int i=n;i>=1;i--)sa[base[x[y[i]]]--]=y[i];//根据y顺序(倒)赋值SA //把x放进y,然后更新x swap(x,y); p=1;x[sa[1]]=1; for(int i=2;i<=n;i++) x[sa[i]]=y[sa[i-1]]==y[sa[i]]&&y[sa[i-1]+k]==y[sa[i]+k]?p:++p;//判重 if(p>=n)break;//排名各不相同即退出 m=p; } int k=0; for(int i=1;i<=n;i++) { if(k)k--; int j=sa[x[i]-1];//j是i在SA中的上一个后缀 while(s[i+k]==s[j+k])k++; h[x[i]]=k; } }
【后缀自动机】识别子串的自动机。
【序列自动机】识别子序列的自动机。
对于字符串,f[x][c]表示第x位后的第一个字符c的位置。|a|为字符集大小。
O(n|a|)构造:对于当前x位的字符c,上一个字符c的位置pre[c],使ch[y][c]=x,y=pre[c]~x-1。
for(int i=1;i<=m;i++){ int c=s[i]-'a'; for(int j=i-1;j>=pre[c];j--)ch[j][c]=i; pre[c]=i; }
O(n log |a|)构造:从后往前扫,那么每次的操作就是复制数组后修改一个字符的数值,用可持久化线段树维护。
【trie】字典树
结构:数组存储式(空间大,时间小),链表存储式(空间小,时间大)
功能:
1.串的快速检索
2.串排序
3.从串中快速匹配单词
4.最长公共前缀(LCP)=两点的LCA
未完待续……
【自动机的本质】
字符串自动机的本质:所有字符串自动机的本质都是【节点】【Trans边】【Fail边】,自动机是一个或几个字符串建出来的,一个字符串可以在自动机上匹配到一些节点,结合自动机建串产生一些特殊的性质。
节点是匹配的对象。
Trans边是在匹配字符串后面加字母。
Fail边是在前面减字母,使得到达一个新状态。
一、KMP:单字符串匹配自动机
用模板串A建自动机,串B匹配。
①节点是串A的前缀。
②Trans边接串A下一个字符,不能接则失配。
③Fail边是在前面减字符,会发现转移到的点恰好就叫【最长的满足前缀=后缀的前缀右端点】。
于是KMP的fail数组就出现了所谓最长的“前缀=后缀”的长度这种含义。
再考虑KMP如何建自动机,只需要建Fail边。依赖于上一个节点的Fail,如果加一个字符还可以就继承,否则继续fail(继续减字符)直到满足。
二、AC-Aho:多字符串匹配自动机
用模板串集合建AC自动机,串B匹配。
①节点是本质不同的串前缀。
②Trans接26个字符转移到新的状态。
构造:直接依赖于Trie即可。
③Fail边是在前面减字符。
构造:将Trie从根开始用队列BFS,每个点的Fail依赖于上一个点。
这里有一个很厉害的优化,就是不匹配时直接用Trans边转移到(原来需要不断Fail到的)位置。
这样,如果匹配就Fail到上一个点的Fail位置+c处。如果不匹配就直接指向上一个点的Fail位置+c处,根据传递性能最直接到匹配的位置。
SAM和PAM都是同理咯。