https://www.cnblogs.com/--560/p/5457023.html
求两串的最长公共子串
#include<bits/stdc++.h>
using namespace std;
const int MAX=2e6;
const int INF=0x3f3f3f3f;
struct Tire
{
static const int MAXN=26;
int nxt[MAX][MAXN],f[MAX],L[MAX],last,tot;
void init()
{
last=tot=0;
memset(nxt[0],-1,sizeof(nxt[0]));
f[0]=-1;L[0]=0;
}
void add(int x)
{
int p=last,np=++tot;
L[np]=L[p]+1;
memset(nxt[np],-1,sizeof(nxt[np]));
while(~p&&nxt[p][x]==-1) nxt[p][x]=np,p=f[p];
if(p==-1) f[np]=0;
else
{
int q=nxt[p][x];
if(L[q]!=L[p]+1)
{
int nq=++tot;
L[nq]=L[p]+1;
memcpy(nxt[nq],nxt[q],sizeof(nxt[q]));
f[nq]=f[q];
f[q]=f[np]=nq;
while(~p&&nxt[p][x]==q) nxt[p][x]=nq,p=f[p];
}
else f[np]=q;
}
last=np;
}
int find(char *s)
{
int len=strlen(s);
int res=0,tmp=0,u=0;
for(int i=0;i<len;++i)
{
int x=s[i]-'a';
if(~nxt[u][x]) ++tmp,u=nxt[u][x];
else
{
while(~u&&nxt[u][x]==-1) u=f[u];
if(~u) tmp=L[u]+1,u=nxt[u][x];
else tmp=0,u=0;
}
res=max(res,tmp);
}
return res;
}
}SAM;
char s[MAX];
int main()
{
while(~scanf("%s",s))
{
SAM.init();
int len=strlen(s);
for(int i=0;i<len;++i) SAM.add(s[i]-'a');
scanf("%s",s);
printf("%d\n",SAM.find(s));
}
return 0;
}
SPOJ Longest Common Substring II
多串最长公共公共子串
#include<bits/stdc++.h>
using namespace std;
const int MAX=1e6+5;
const int INF=0x3f3f3f3f;
struct Tire
{
static const int MAXN=26;
int nxt[MAX][MAXN],f[MAX],L[MAX],last,tot;
int Min[MAX],Max[MAX];
void init()
{
last=tot=0;
memset(nxt[0],-1,sizeof(nxt[0]));
f[0]=-1;L[0]=0;
Min[0]=Max[0]=0;
}
void add(int x)
{
int p=last,np=++tot;
L[np]=L[p]+1;
memset(nxt[np],-1,sizeof(nxt[np]));
Min[np]=L[np];
while(~p&&nxt[p][x]==-1) nxt[p][x]=np,p=f[p];
if(p==-1) f[np]=0;
else
{
int q=nxt[p][x];
if(L[q]!=L[p]+1)
{
int nq=++tot;
L[nq]=L[p]+1;
memcpy(nxt[nq],nxt[q],sizeof(nxt[q]));
Min[nq]=L[nq];
f[nq]=f[q];
f[q]=f[np]=nq;
while(~p&&nxt[p][x]==q) nxt[p][x]=nq,p=f[p];
}
else f[np]=q;
}
last=np;
}
void find(char *s)
{
int len=strlen(s);
int u=0,tmp=0;
for(int i=0;i<=tot;++i) Max[i]=0;
for(int i=0;i<len;++i)
{
int x=s[i]-'a';
if(~nxt[u][x]) ++tmp,u=nxt[u][x];
else
{
while(~u&&nxt[u][x]==-1) u=f[u];
if(~u) tmp=L[u]+1,u=nxt[u][x];
else tmp=0,u=0;
}
Max[u]=max(Max[u],tmp);
}
for(int i=tot;i>=1;--i) Max[f[i]]=max(Max[f[i]],Max[i]);
for(int i=0;i<=tot;++i) Min[i]=min(Min[i],Max[i]);
}
int cal()
{
int res=0;
for(int i=0;i<=tot;++i) res=max(res,Min[i]);
return res;
}
}SAM;
char s[MAX];
int main()
{
SAM.init();
scanf("%s",s);
int len=strlen(s);
for(int i=0;i<len;++i) SAM.add(s[i]-'a');
while(~scanf("%s",s)) SAM.find(s);
printf("%d\n",SAM.cal());
return 0;
}
SPOJ Substrings
求一个串长度为1~len的子串的最大个数
#include<bits/stdc++.h>
using namespace std;
const int MAX=1e6+5;
const int INF=0x3f3f3f3f;
int ans[MAX];
struct Tire
{
static const int MAXN=26;
int nxt[MAX][MAXN],f[MAX],L[MAX],rt[MAX],in[MAX],last,tot;
void init()
{
last=tot=0;
memset(nxt[0],-1,sizeof(nxt[0]));
f[0]=-1;L[0]=0;
}
void add(int x)
{
int p=last,np=++tot;
L[np]=L[p]+1;
memset(nxt[np],-1,sizeof(nxt[np]));
rt[np]=1;
while(~p&&nxt[p][x]==-1) nxt[p][x]=np,p=f[p];
if(p==-1) f[np]=0;
else
{
int q=nxt[p][x];
if(L[q]!=L[p]+1)
{
int nq=++tot;
memcpy(nxt[nq],nxt[q],sizeof(nxt[q]));
L[nq]=L[p]+1;
f[nq]=f[q];
f[q]=f[np]=nq;
while(~p&&nxt[p][x]==q) nxt[p][x]=nq,p=f[p];
}
else f[np]=q;
}
last=np;
}
void cal()//topsort
{
memset(in,0,sizeof(in));
for(int i=1;i<=tot;++i) ++in[f[i]];
queue<int >q;
for(int i=1;i<=tot;++i) if(!in[i]) q.push(i);
while(!q.empty())
{
int u=q.front();q.pop();
if(f[u]==-1) continue;
rt[f[u]]+=rt[u];
if(--in[f[u]]==0) q.push(f[u]);
}
memset(ans,0,sizeof(ans));
for(int i=1;i<=tot;++i) ans[L[i]]=max(ans[L[i]],rt[i]);
}
}SAM;
char s[MAX];
int main()
{
while(~scanf("%s",s))
{
SAM.init();
int len=strlen(s);
for(int i=0;i<len;++i) SAM.add(s[i]-'a');
SAM.cal();
for(int i=len-1;i>=1;--i) ans[i]=max(ans[i],ans[i+1]);
for(int i=1;i<=len;++i) printf("%d\n",ans[i]);
}
return 0;
}
L[i]-L[f[i]] | ||||
在解决问题中的应用
下面看在后缀自动机的帮助下我们能做什么。
简便起见,我们假设字母表的大小k为常数。
存在性查询
问题.给定文本T,询问格式如下:给定字符串P,问P是否是T的子串。
复杂度要求.预处理O(length(T)),每次询问O(P)。
算法.我们对文本T用O(length(T))建立后缀自动机。
现在回答单次询问。假设状态——变量v,最初是初始状态T_0.我们沿字符串P给出的路径走,因此从当前状态经转移来到新的状态v
。如果在某时刻,当前状态没有要求字符的转移,那么答案就是"no"。如果我们处理了整个字符串P,答案就是"yes"。
显然这一算法将在时间O(length(P))内运行完毕。并且,该算法实际上找出了P在文本中出现过的最长前缀——如果模式串使得这些前缀
都很短,算法将比处理全部模式串要快得多。
不同的子串个数
问题.给定字符串S,问它有多少不同的子串。
复杂度要求.O(length(S))。
算法.我们将字符串S建立后缀自动机。
在后缀自动机中,S的任意子串都对应自动机中的一条路径。答案就是从初始节点t_0开始,自动机中不同的路径条数。
已知后缀自动机是一张有向无环图,我们可以考虑用动态规划计算不同的路径数量。
也就是,令d[v]为从状态v开始的不同路径条数(包括长度为零的路径),则有转移:
即d[v]是v所有后继节点的d值之和加上1.
最终答案就是d[t_0]-1(减一以忽略空串)。
不同子串的总长
问题.给定字符串S,求其所有不同子串的总长度。
复杂度要求.O(length(S)).
算法.这一问题的答案和上一题类似,但现在我们必须考虑两个状态:不同子串的个数d[v]和它们的总长ans[v].
上一题已描述了d[v]的计算方法,而ans[v]的计算方法如下:
即取所有后继节点w的ans值,并将它和d[w]相加。因为这是每个字符串的首字母。
字典序第k小子串
问题.给定字符串S,一系列询问——给出整数K_i,计算S的所有子串排序后的第K_i个。
复杂度要求.单次询问O(length(ans)*Alphabet),其中ans是该询问的答案,Alphabet是字母表大小。
算法.这一问题的基础思路和上两题类似。字典序第k小子串——自动机中字典序第k小的路径。因此,考虑从每个状态出
发的不同路径数,我们将得以轻松地确定第k小路径,从初始状态开始逐位确定答案。
最小循环移位
问题.给定字符串S,找到和它循环同构的字典序最小字符串。
复杂度要求.O(length(S)).
算法.我们将字符串S+S建立后缀自动机。该自动机将包含和S循环同构的所有字符串。
从而,问题就简化成了在自动机中找出字典序最小的,长度为length(S)的路径,这很简单:从初始状态开始,每一步都贪心地走
,经过最小的转移。
出现次数查询
问题.给定文本T,询问格式如下:给定字符串P,希望找出P作为子串在文本T中出现了多少次(出现区间可以相交)。
复杂度要求.预处理O(length(T)),单次询问O(length(P)).
算法.我们将文本T建立后缀自动机。
然后我们需要进行预处理:对自动机中的每个状态v都计算cnt[v],等于其endpos(v)集合的大小。事实上,所有在T中对应同一状态的
字符串都在T中出现了相同次数,该次数等于endpos中的位置数。
不过,我们无法对所有状态明确记录endpos集合,所以我们只计算其大小cnt.
为了实现这一点,如下处理。对每个状态,如果它不是由“拷贝”而来,最初就赋值cnt=1.然后我们按长度len降序遍历所有序列,并将
当前的cnt[v]加给后缀链接:
cnt[link(v)]+=cnt[v].
你可能会说我们并没有对每个状态计算出了正确的cnt值。
为什么这是对的?不经“拷贝”而来的状态恰好有length(S)个,而且其中的第i个是我们添加第i个字符时得到的。因此,最初这些状态的cnt=1,
其他状态的cnt=0.
然后我们对每个状态v执行如下操作:cnt[link(v)]+=cnt[v].其意义在于,如果某字符串对应状态v,曾在cnt[v]中出现过,那么它的所有后缀都
同样在其中出现。
这样,我们就掌握了如何对自动机中所有状态计算cnt值的方法。
在此之后,询问的答案就变得平凡——只需要返回cnt[t],其中t是模式串P所对应的状态。
首次出现位置查询
问题.给定文本T,询问格式如下:给定字符串P,求P在文本中第一次出现的位置。
复杂度要求.预处理O(length(T)),单次询问O(length(P)).
算法.对文本T建立后缀自动机。
为了解决这一问题,我们需要预处理firstpos,找到自动机中所有状态的出现位置,即,对每个状态v我们希望找到一个位置firstpos[v],代表
其第一次出现的位置。换句话说,我们希望预先找出每个endpos(v)中的最小元素(我们无法明确记录整个endpos集合)。
维护这些firstpos的最简单方法是在构建自动机时一并计算,当我们创建新的状态cur时,一旦进入函数sa_extend(),就确定该值:
firstpos(cur)=len(cur)-1(如果我们的下标从0开始)。
当拷贝节点q时,令:
firstpos(clone)=firstpos(q),(因为只有一个别的可能值——firstpos(cur),显然更大)。
这样就得到了查询的答案——firstpos(t)-length(P)+1,其中t是模式串P对应的状态。
所有出现位置查询
问题.给定文本T,询问格式如下:给定字符串P,要求给出P在T中的所有出现位置(出现区间可以相交)。
复杂度要求.预处理O(length(T))。单次询问O(length(P)+answer(P)),其中answer(P)是答案集合的大小,即,要求时间复杂度和输入输出同阶。
算法.对文本T建立后缀自动机,和上一个问题相似,在构建自动机的过程中对每个状态计算第一次出现的终点firstpos。
假设我们收到了一个询问——字符串P。我们找到了它对应的状态t。
显然应当返回firstpos(t)。还有哪些位置?我们考虑自动机中那些包含了字符串P的状态,即那些P是其后缀的状态。
换言之,我们需要找出所有能通过后缀链接到达状态t的状态。
因此,为了解决这一问题,我们需要对每个节点储存指向它的所有后缀链接。为了找到答案,我们需要沿着这些翻转的后缀链接进行DFS/BFS,从状
态t开始。
这一遍历将在O(answer(P))时间内结束,因为我们不会访问同一状态两次(因为每个状态的后缀链接仅指向一个点,因此不可能有两条路径通往同一
状态)。
然而,两个状态的firstpos值可能会相同,如果一个状态是由另一个拷贝而来。但这不会影响渐进复杂度,因为每个非拷贝得到的节点只会有一个拷贝。
此外,你可以轻松地除去那些重复的位置,如果我们不考虑那些拷贝得来的状态的firstpos。事实上,所有拷贝得来的状态都被其“母本”状态的后缀链接
指向。因此,我们对每个节点记录标签is_clon,我们不考虑那些is_clon=true的状态的firstpos。这样我们就得到了answer(P)个不重复地状态。
查询不在文本中出现的最短字符串
问题.给定字符串S和字母表。要求找出一个长度最短的字符串,使得它不是S的子串。
复杂度要求.O(length(S)).
算法.在字符串S的后缀自动机上进行动态规划。
令d[v]为节点v的答案,即,我们已经输入了字符串的一部分,匹配到v,我们希望找出有待添加的最少字符数量,以到达一个不存在的转移。
计算d[v]非常简单。如果在v处某个转移不存在,那么d[v]=1:用这个字符来“跳出”自动机,以得到所求字符串。
否则,一个字符串无法达到要求,因此我们必须取所有字符中的最小答案:
原问题的答案等于d[t_0],而所求字符串可以用记录转移路径的方法得到。
求两个字符串的最长公共子串
问题.给定两个字符串S和T。要求找出它们的最长公共子串,即一个字符串X,它同时是S和T的子串。
复杂度要求.O(length(S)+length(T)).
算法.我们对字符串S建立后缀自动机。
我们按照字符串T在自动机上走,查找它每个前缀在S中出现过的最长后缀。换句话说,对字符串T中的每个位置,我们都想找出S和T在该位置结束的最长公共子串。
为了实现这一点,我们定义两个变量:当前状态v和当前长度l。这两个变量描述了当前的匹配部分:其长度和状态,对应哪个字符串(如果不储存长度就无法确定这一点,因为一个状态可能匹配多个有不同长度的字符串)。
最初,p=t_0,l=0,即,匹配部分为空。
现在我们考虑字符T[i],我们希望找到这个位置的答案。
· 如果自动机中的状态v有一个符号T[i]的转移,我们可以简单地走这个转移,然后将长度l加一。
· 如果状态v没有该转移,我们应当尝试缩短当前匹配部分,为此应当沿着后缀链接走:
v=link(v).
在此情况下,当前匹配长度必须被减少,但留下的部分尽可能多。显然,应令l=len(v):
l=len(v).
若新到达的状态仍然没有我们想要的转移,那我们必须再次沿着后缀链接走,并且减少l的值,直到我们找到一个转移(那就返回第一步),或者我们最终到达了空状态-1(这意味着字符T[i]并未在S中出现,所以令v=l=0然后继续处理下一个i)。
原问题的答案就是l曾经达到的最大值。
这种遍历方法的渐进复杂度是O(length(T)),因为在一次移动中我们要么将l加一,要么沿着后缀链接走了若干次,每次都会严格减少l。因此,l总共的减少值之和不可能超过length(T),这意味着线性时间复杂度。
多个字符串的最长公共子串
问题.给出K个字符串S_1~S_K。要求找出它们的最长公共子串,即一个字符串X,它是所有S_i的子串。
复杂度要求.O(∑length(S_I)*K).
算法.将所有S_i连接在一起成为一个新的字符串T,其中每个S_i后要加上一个不同的分隔符D_i(即加上K个额外的不同特殊字符D_1~D_K):
我们对字符串T构建后缀自动机。
现在我们需要在后缀自动机找出一个字符串,它是所有字符串S_i的子串。注意到如果一个子串在某个字符串S_j中出现过,那么后缀自动机中存在一
条以这个子串为前缀的路径,包含分隔符D_j,但不包含其他分隔符D_1,...,D_j-1,D_j+1,...,D_k。
因此,我们需要计算“可达性”:对自动机中的每个状态和每个字符D_i,计算是否有一条从该状态开始的路径,包含分隔符D_i,但不包含别的分隔符。
很容易用DFS/BFS或者动态规划实现。在此之后,原问题的答案就是字符串longest(v),其中v能够到达所有的分隔符。