@xiaoziyao
2021-01-31T13:48:41.000000Z
字数 7111
阅读 3098
字符串
学习笔记
上一篇:后缀自动机(SAM)学习笔记
做了一些SAM和广义SAM的题,总结出一些套路,把它们记下来以供参考。
SAM板子:
int extend(int last,int x){
int now=++tot,pos=last,tmp,cln;
len[now]=len[pos]+1;
tag[now]=1;
while(pos!=0&&nxt[pos][x]==0)
nxt[pos][x]=now,pos=link[pos];
if(pos==0){
link[now]=1;
return now;
}
tmp=nxt[pos][x];
if(len[tmp]==len[pos]+1){
link[now]=tmp;
return now;
}
cln=++tot;
len[cln]=len[pos]+1;
for(int i=1;i<=26;i++)
nxt[cln][i]=nxt[tmp][i];
link[cln]=link[tmp],link[tmp]=link[now]=cln;
while(pos!=0&&nxt[pos][x]==tmp)
nxt[pos][x]=cln,pos=link[pos];
return now;
}
cin>>s;
n=s.size(),s=" "+s;
last=++tot;
for(i=1;i<=n;i++)
last=extend(s[i]-96);
广义SAM板子:
int extend(int last,int x){
if(nxt[last][x]){
int pos=last,tmp=nxt[pos][x],cln;
if(len[tmp]==len[pos]+1)
return tmp;
cln=++tot;
for(int i=1;i<=26;i++)
nxt[cln][i]=nxt[tmp][i];
len[cln]=len[pos]+1;
link[cln]=link[tmp],link[tmp]=cln;
while(pos!=0&&nxt[pos][x]==tmp)
nxt[pos][x]=cln,pos=link[pos];
return cln;
}
int now=++tot,pos=last,tmp,cln;
len[now]=len[pos]+1;
while(pos!=0&&nxt[pos][x]==0)
nxt[pos][x]=now,pos=link[pos];
if(pos==0){
link[now]=1;
return now;
}
tmp=nxt[pos][x];
if(len[tmp]==len[pos]+1){
link[now]=tmp;
return now;
}
cln=++tot;
len[cln]=len[pos]+1;
for(int i=1;i<=26;i++)
nxt[cln][i]=nxt[tmp][i];
link[cln]=link[tmp],link[tmp]=link[now]=cln;
while(pos!=0&&nxt[pos][x]==tmp)
nxt[pos][x]=cln,pos=link[pos];
return now;
}
SAM可以很简单地处理各种关于子串长度,子串出现次数等信息。
子串出现次数:对于某个状态,它的出现次数可以在Parent Tree上记一下数(具体地,对于每一次的,记录,然后在Parent Tree上求这个子树的之和)
void dfs(int x){
size[x]=tag[x];
for(int i=start[x];i;i=then[i]){
int y=to[i];
dfs(y);
size[x]+=size[y];
}
}
(其中就是出现次数)
子串长度:某个状态的包含的所有子串的长度恰好为区间,且不重不漏。
似乎没有什么要放的代码,凑个数QAQ
子串长度出现次数:对于某个状态,区间修改一下子串长度区间,一般可以差分做。
cnt[len[link[x]]+1]++,cnt[len[x]+1]--;
for(i=1;i<=n;i++)
cnt[i]+=cnt[i-1];
P3804 【模板】后缀自动机 (SAM):求所有出现次数不为的子串出现次数乘该子串长度的最大值,直接统计就可以了。
P5341 [TJOI2019]甲苯先生和大中锋的字符串:求恰好出现次的子串中出现次数最多的长度,在Parent Tree上求一下各种信息,恰好出现次的状态都区间修改一下长度的出现次数,可以差分一下,然后前缀和统计答案。
CF802I Fake News (hard):求所有子串的出现次数平方和,直接统计就可以了。
CF123D String:求所有子串的出现次数乘(出现次数)之和,直接统计就可以了。
SP8222 NSUBSTR - Substrings:给定长度为的串,求长度为的子串在中最大出现次数。
CF316G3 Good Substrings:直接建广义SAM暴力算就好了
方法:
void dfs(int x){
size[x]=len[x]-len[link[x]];
for(int i=start[x];i;i=then[i]){
int y=to[i];
dfs(y);
size[x]+=size[y];
}
}
方法:
void dfs(int x){
if(ans[x])
return ;
ans[x]=1;
for(int i=1;i<=26;i++)
if(nxt[x][i]){
dfs(nxt[x][i]);
ans[x]+=ans[nxt[x][i]];
}
}
方法:
for(i=1;i<=n;i++){
last=extend(last,s[i]);
ans+=(len[last]-len[link[last]]);
}
SP694 DISUBSTR - Distinct Substrings求本质不同子串个数。
SP705 SUBST1 - New Distinct :求本质不同子串个数,是上一题加强版。
P2408 不同子串个数:求本质不同子串个数。
P4070 [SDOI2016]生成魔咒:求本质不同子串个数,离散化+map优化。
SP32951 ADASTRNG - Ada and Substring:求某个字母开头本质不同子串个数。
#6071. 「2017 山东一轮集训 Day5」字符串:求多个串可空子串拼接后不同子串个数,对每个串建SAM,然后用子序列自动机把DAWG拼接起来,形成的新DAG有个美妙的性质:它上面的路径对应着所有新字符串的子串。这样,我们就只需要在上面计算路径数量就可以了。
P3181 [HAOI2016]找相同字符:求两个字符串中各取出一个子串使得这两个子串相同的方案数。直接建出一个串的SAM,跑dfs求出出现次数,然后跑个匹配就好了。
P6139 【模板】广义后缀自动机(广义 SAM):求多串本质不同子串,建广义SAM后直接算
CF653F Paper task:求本质不同括号子串数量,用一个数据结构随便维护一下,用SAM判断本质不同就好了。
我们要在上匹配一个串,可以怎么做呢?
可以考虑不断让这个串进行两个操作:向前增加字符来进行匹配,将无法继续匹配的字符丢弃(以匹配后面的字符)。
我们在学习笔记辨析过Parent Tree和DAWG的差异与联系,在这里派上了用场:
如何在后面增加字符:跳一个为这个字符的DAWG边,一定让你恰好增加了一个字符。
如何在前面丢弃字符:每一次丢弃字符都暴力跳后缀link,直到到根结点或者存在当前匹配字符的DAWG边。由后缀link的定义可以知道跳后缀link是让你的发生改变的最小代价(即存在机会让你继续匹配),因此如果可以继续匹配,暴力跳后缀link一定可以跳到能匹配的状态。
因为每个状态最多遍历一遍,所以复杂度是的。
int now=1,l=0;
for(i=1;i<=n;i++){
while(now!=0&&nxt[now][s[i]]==0)
now=link[now],l=len[now];
if(nxt[now][s[i]])
now=nxt[now][s[i]],l++;
......
}
循环同构:的循环同构包括,,,,(比如说,的循环同构有,,)。
循环同构的匹配和普通串的匹配差不多,你只需要破环为链就可以了。(不过有一点要注意,如果匹配的长度大于该字符串的长度,那么需要强制失配)
破环为链就不需要代码了吧QAQ
CF235C Cyclical Quest:给定和若干个询问串,求每个的所有本质不同循环同构在的出现次数之和,循环同构破环为链就好了,到了长度需要强制失配,本质不同只需要打个标记就可以了。
P3763 [TJOI2017]DNA:直接在DAWG上跑dfs就好了。
最小表示法:求字符串字典序最小的循环同构
很简单,只需要倍长一下(因为要破环为链),建好SAM,然后在DAWG上贪心走字典序更小的边就可以了(也可以用记,然后用跳,这样快一些,而且在字符集过大之时有很好的优化作用)。
放的是第二种做法。
int now=1;
for(int i=1;i<=n;i++){
int x=nxt[now].begin()->first;
printf("%d ",x);
i=nxt[i][x];
}
本质不同小子串:
void getcnt(int x){
if(cnt[x])
return ;
cnt[x]=x==1? 0:1;
for(int i=1;i<=26;i++){
int y=nxt[x][i];
if(y==0)
continue;
getcnt(y);
cnt[x]+=cnt[y];
}
}
void query(int x,int k){
if(k<=0)
return ;
for(int i=1;i<=26;i++){
int y=nxt[x][i];
if(y==0)
continue;
if(k>cnt[y]){
k-=cnt[y];
continue;
}
printf("%c",i+96);
query(y,k-1);
return ;
}
}
位置不同小子串:
void getsize(int x){
size[x]=tag[x];
for(int i=start[x];i;i=then[i]){
int y=to[i];
getsize(y);
size[x]+=size[y];
}
}
void getcnt(int x){
if(cnt[x])
return ;
cnt[x]=x==1? 0:size[x];
for(int i=1;i<=26;i++){
int y=nxt[x][i];
if(y==0)
continue;
getcnt(y);
cnt[x]+=cnt[y];
}
}
void query(int x,int k){
if(k<=0)
return ;
for(int i=1;i<=26;i++){
int y=nxt[x][i];
if(y==0)
continue;
if(k>cnt[y]){
k-=cnt[y];
continue;
}
printf("%c",i+96);
query(y,k-size[y]);
return ;
}
}
P3975 [TJOI2015]弦论:求本质不同/位置不同的小子串
SP7258 SUBLEX - Lexicographical Substring Search:求本质不同小子串
我们定义代表状态是否在串中出现,这样我们每一次插入可以记录数组。然后在Parent Tree上合并一下,可以用优化到(为SAM状态数,为串数),实际上也可以用线段树合并(见下面的"维护")。
bitset<maxn>end[maxn];
int extend(int last,int x){
......
end[now][t]=1;
......
}
void dfs(int x){
for(int i=start[x];i;i=then[i]){
int y=to[i];
dfs(y);
end[x]|=end[y];
}
for(int i=1;i<=n;i++)
if(end[x][i]==0)
return ;
ans=max(ans,len[x]);
}
SP1811 LCS - Longest Common Substring:求两串公共串,也可以用第二个串在第一个串上跑匹配。
SP1812 LCS2 - Longest Common Substring II:求多串公共串
SP10570 LONGCS - Longest Common Substring:多组数据,求多串公共串
P5546 [POI2000]公共串:求多串公共串
P2463 [SDOI2008]Sandy的卡片:求所有串的相同段(其中相同段的定义为这一段统一加上某个数就会变成另一端),差分一下就是求公共串的裸题
,即,最长公共前缀;,即,最长公共后缀。
有一个性质,反串SAM的Parent Tree是正串的后缀树,然后我们就可以利用SAM建后缀树了。
把串反过来建SAM不需要看了吧QAQ
然后还有一个性质,两个串的是它们在后缀树上对应的结点的(很显然,感性理解一下就好了),这样我们就可以用SAM求一些神奇的操作了。
LCA难道要看?
有时候会和虚树dp搭配在一起,SAM(细节少,不直观)+虚树dp(细节多,直观)=SAM上虚树dp(细节多,不直观)QAQ。
的有时候会被卡,需要用ST表优化到。
P4248 [AHOI2013]差异:求后缀的lcp,SAM辅助建后缀树,也可以考虑式子的意义,然后用点分治的思想给路径计数。
P1117 [NOI2016]优秀的拆分:SAM+部分结论+插点统计贡献(调和级数)
CF1073G Yet Another LCP Problem:SAM上虚树dp求。
SP687 REPEATS - Repeats:论文题。
有些题目需要求SAM中,我们可以用几种方法来维护。(推荐使用方法,偷懒可以用方法,但不保证不会TLE)
注:SAM上线段树合并,经常和树上倍增搭配在一起,就是屑上加屑。
CF1037H Security:给定,组询问,每组询问给定,求字典序最小的满足是的子串且的字典序大于(即求内的前缀),我们可以用线段树合并维护出等价类,然后寻找替代每个字符的最小代价。具体地,我们边执行子串匹配(注意,这里的匹配不可以丢弃前面的字符,如果无法匹配下去就直接退出匹配),边从后面的字符到字符不断找,如果后面接上这个字符还在存在在限定的区间内(其中为已匹配的长度),那么这个字符是最小的能代替当前字符的字符。直到我们匹配完整个串,我们就贪心从后往前扫,扫到第一个可以替代的字符进行替代,然后输出。(有一个细节,匹配的长度需要到的长度,比如说,此时答案为)
P4094 [HEOI2016/TJOI2016]字符串:SAM+线段树合并+二分答案
CF666E Forensic Examination,广义SAM+线段树合并+子串匹配+树上倍增
P4770 [NOI2018] 你的名字:SAM+线段树合并+子串匹配
P5161 WD与数列:SAM+线段树合并+分类讨论