制胡窜考前总结
制胡窜考前总结
manacher算法
利用了回文串的对称性质,\(O(n)\)的时间复杂度就可以求出以每个点作为回文中心时最长回文串的长度
个人习惯\(mc[]\)的值为回文串长度,而不是回文串长度+1
代码:
int n,mc[maxn<<1],N;
char s[maxn],d[maxn<<1];
inline void manacher(){
d[1]='$',d[N=n*2+2]='#';
for(int i=2;i<N;i+=2)
d[i]='#',d[i+1]=s[i/2];
int mr=0,mid=0;
for(int i=1;i<=N;i++){
if(i<=mr) mc[i]=min(mr-i,mc[(mid<<1)-i]);
while(d[i-mc[i]-1]==d[i+mc[i]+1]) mc[i]++;
if(i+mc[i]>mr) mr=i+mc[i],mid=i;
}
}
常见用法是先求出\(mc[]\),然后根据题目要求和其他数据结构和算法配合使用,也有直接用\(mc[]\)的
还有些题目要在\(manacher\)算法的过程中进行操作
后缀数组
\(sa[i]\):排名为\(i\)的后缀在原串的位置
\(rk[i]\):在原串位置为\(i\)的后缀的排名
\(h[i]\):排名为\(i\)的后缀和排名为\(i-1\)的后缀最长的前缀长度
感觉没什么好说的,板子背熟,熟悉各个数组的意义,其他的就没什么了
代码:
inline void Qsort(){
for(int i=0;i<=m;i++) tax[i]=0;
for(int i=1;i<=n;i++) tax[rk[i]]++;
for(int i=1;i<=m;i++) tax[i]+=tax[i-1];
for(int i=n;i>=1;i--) sa[tax[rk[nx[i]]]--]=nx[i];
}
inline void getsa(){
for(int i=1;i<=n;i++) rk[i]=s[i],nx[i]=i;
Qsort();
for(int p=1,w=1;p<n;m=p,w<<=1){
p=0;
for(int i=1;i<=w;i++) nx[++p]=n+i-w;
for(int i=1;i<=n;i++) if(sa[i]>w) nx[++p]=sa[i]-w;
Qsort(),swap(rk,nx),rk[sa[1]]=p=1;
for(int i=2;i<=n;i++)
rk[sa[i]]=nx[sa[i]]==nx[sa[i-1]]&&nx[sa[i]+w]==nx[sa[i-1]+w]?p:++p;
}
}
inline void geth(){
for(int i=1,j,k=0;i<=n;h[rk[i++]]=k)
for(k?k--:k,j=sa[rk[i]-1];s[i+k]==s[j+k];k++);
}
一部分题目:
其实直接点这个就行了:
KMP算法
贴一下板子吧,有个常数优化,写在注释里了
inline void getnex(){
for(int i=2,j=0;i<=m;i++){
while(j&&p[i]!=p[j+1]) j=nex[j];
if(p[i]==p[j+1]) j++;
if(p[i+1]==p[j+1]) nex[i]=nex[j];
//如果p[i+1]已经和p[j+1]相等了,
//如果出现p[i+1]和s[k]不匹配的状况
//那么p[j+1]页一定与s[k]不匹配
//这一次跳nex就是没有意义的,所以nex[i]应为nex[j]
else nex[i]=j;
}
}
注意我这个版本的匹配方式是:字符串下标从1开始,每次尝试\(s[i+1]\)与\(p[j+1]\)匹配。
for(int i=1,j=0;i<=n;i++){
while(j&&s[i]!=p[j+1]) j=nex[j];
if(s[i]==p[j+1]) j++;
if(j==m){
printf("%d\n",i-m+1);
j=nex[j];
}
}
后缀自动机
学习资料的话,感觉OI Wiki上的俄文讲解翻译比陈立杰的PPT讲的更容易让人理解,当然CLJ的PPT是在冬令营讲课时候用的,没有真人讲解难以理解也很正常。。。
这里记一下一些比较重要的概念:
\(endpos(x)\):子串\(x\)的结束位置集合
后缀自动机上的每一个状态都代表着一个\(endpos\)等价类,即结束位置集合相同的子串们
设\(len[x]\)表示属于等价类\(x\)的子串中,最长的子串的长度,同理,\(minlen[x]\)代表其中最短的子串的长度
可以发现,对于一个子串的所有后缀,后缀长度越短,其\(endpos\)大小单调不减。也就是说,根据后缀自动机状态的定义,每个状态所表示的子串一定是长度连续的、某个子串的一些后缀
每个状态\(x\)都有一个后缀链接\(link[x]\),其指向的节点\(y\)代表的子串是\(x\)所代表子串的后缀,由于\(y\)的\(endpos\)要比\(x\)的\(endpos\)大,使得他们分成了两个不同的状态,并由\(link[x]\)链接。根据上面的那条性质,满足\(len[y]+1==minlen[x]\)
在原串末尾新插入一个字符\(c\)的时候,要新建一个节点\(cur\),其代表的就是新串这个子串和他的所有满足\(endpos\)大小等于\(1\)的后缀们。设前一个状态为\(las\),对于\(las\)和他的每个后缀,都要新建一个向\(cur\)的转移
但是,如果\(las\)的某个后缀\(p\)已经有字符\(c\)的转移了,设转移到\(q\),这个时候要分两种情况考虑:
- \(len[q]==len[p]+1\):由于我们新加的那个\(cur\)也是\(len[cur]=len[p]+1\),所以就相当于把\(q\)的\(endpos\)集合加入一个新的位置,直接让\(st[cur].link=q\)就好了
- \(len[q]>len[p]+1\):也就是说,在我们加入\(cur\)之前,原本的长为\(len[p]+1\)的子串和一些长度大于\(len[p]+1\)的子串有同样的\(endpos\)集合,非常的和谐。但是在我们加入\(cur\)之后,长为\(len[p]+1\)的子串的\(endpos\)集合多了1,和原来不一样了。我们就要强制把\(len[p]+1\)的和\(>len[p]+1\)的分成两个点,也就是要克隆出一个新的点\(clo\)。这个点的\(len[clo]=len[p]+1\),\(las[clo]=las[q]\),原来\(q\)的转移\(clo\)也全部满足,最后\(q\)和\(cur\)的\(link\)指向\(clo\)
贴一下\(extend\)的代码
inline void extend(char c){
int cur=++tot,z=c-'a';
st[cur].len=st[las].len+1;
st[cur].siz++;
int p=las;
while(~p&&!st[p].son[z])
st[p].son[z]=cur,p=st[p].link;.//las和所有后缀都要转移
if(p==-1) st[cur].link=0;//不存在有c转移的后缀
else{
int q=st[p].son[z];
if(st[q].len==st[p].len+1)
st[cur].link=q;//直接加入,不影响q
else{
int clo=++tot;
st[clo].len=st[p].len+1; //克隆新的点
st[clo].link=st[q].link;
for(int i=0;i<26;i++)
st[clo].son[i]=st[q].son[i];
while(~p&&st[p].son[z]==q)
st[p].son[z]=clo,p=st[p].link;//更新las的后缀所转移的点
st[q].link=st[cur].link=clo; //更新q和cur的link
}
}
las=cur; // 不要忘记
}
一些常见的用法
① 以每个状态为前缀的子串数量(也就是不同子串数量)
对于状态\(x\)来说,设他作为前缀的子串数量为\(d[x]\),枚举他的所有转移,得
②以每个状态为前缀的子串总长度(也就是不同子串总长度)
对于状态\(x\)来说,设要求的为\(ans[x]\),转移一次就相当于他对所有转移的串都产生了长度1的贡献,总共产生了\(d[x]\)的贡献,在累加上之后的贡献,得
③每个状态所代表的\(endpos\)集合的大小(也就是子串的出现次数)
注意不要把这个问题和问题①搞混了
要解决这个问题,首先我们要在每次新建一个节点的时候判断,如果它不是克隆出来的,就让他的\(siz\)(用来记录答案)\(++\)
根据后缀链接\(link\)的定义,一个点的\(endpos\)集合是其\(link\)所指向节点的\(endpos\)集合的子集,也就是说\(siz[st[x].link]+=siz[x]\)
注意以上问题对于点的dp顺序是要按照长度从长到短进行的,所以要先排一下序
for(int i=1;i<=tot;i++) tax[st[i].len]++;
for(int i=1;i<=n;i++) tax[i]+=tax[i-1];
for(int i=1;i<=tot;i++) rk[tax[st[i].len]--]=i;