后缀自动机(Suffix Automaton,简称SAM)。
SAM的States
字串结束集合(endpos):对于S的一个子串s,endpos(s) = s在S中所有出现的结束位置集合。还是以S="aabbabd"为例,endpos("ab") = {3, 6}(这里说的是位置,不要和下面那个图所说的状态1、2、3混淆,这里说的“ab”位置是“aabbabd”,这两个位置为3,6),因为"ab"一共出现了2次,结束位置分别是3和6。同理endpos("a") = {1, 2, 5}, endpos("abba") = {5}。
将所有子串的endpos都求出来。如果两个子串的endpos相等,就把这两个子串归为一类。最终这些endpos的等价类就构成的SAM的状态集合。例如对于S="aabbabd":
状态 |
字符串 |
endpos |
S |
空串 |
{0,1,2,3,4,5,6} |
1 |
a |
{1,2,5} |
2 |
aa |
{2} |
3 |
aab |
{3} |
4 |
aabb,abb,bb |
{4} |
5 |
b |
{3,4,6} |
6 |
aabba,abba,bba,ba |
{5} |
7 |
aabbab,abbab,bbab,bab |
{6} |
8 |
ab |
{3,6} |
9 |
aabbabd,abbabd,bbabd,babd,abd,bd,d |
{7} |
substrings(st)表示状态st中包含的所有子串的集合,
longest(st)表示st包含的最长的子串,
shortest(st)表示st包含的最短的子串。
有几点性质:
- 对于S的两个子串s1和s2,不妨设length(s1) <= length(s2),那么 s1是s2的后缀当且仅当endpos(s1) ⊇ endpos(s2),s1不是s2的后缀当且仅当endpos(s1) ∩ endpos(s2) = ∅。
- 对于一个状态st,以及任意s∈substrings(st),都有s是longest(st)的后缀。
- 对于一个状态st,以及任意的longest(st)的后缀s,如果s的长度满足:length(shortest(st)) <= length(s) <= length(longsest(st)),那么s∈substrings(st)
后缀自动机
对于一个字符串S,它对应的后缀自动机是一个最小的确定有限状态自动机(DFA),接受且只接受S的后缀。
对于字符串S="aabbabd",它的后缀自动机是:
S是开始状态,9是结束状态。通过这个图可以看出状态S到状态9的蓝线所有经过的路线的字符都是aabbabd的一个后缀,例如:S-1-8-9经过的路线是abd正好是aabbabd的一个后缀数组。再比如例如字符串aabb的所有后缀数组即为从状态S到状态4经过的所有路线的字符组成的字符串,例如S-1-8-4经过的路线为abb。
这里有一个问题就是b是aabb的一个后缀数组,但是从状态S到状态4经过的路线中没有b的字符串 ,这是因为b在状态3(字符串aab)中也是它的后缀数组。所以引入了一个中间状态5,这样你会发现对于整个字符串aabbabd来说它的自没有串并没有减少,而且不会出现重复(这里从状态S出发到任意一个状态的蓝线经过的字符串都是aabbabd的一个字串)。
SAM的Suffix Links
Suffix Links实际就是上图的绿线,通过刚才的介绍发现,当前状态引入了中间状态,绿线就指向中间状态,例如上一段说的状态4的路线就指向状态5,否则指向状态S。我们可以发现一条状态序列:7->8->5->S。这个序列的意义是longest(7)即aabbab的后缀依次在状态7、8、5、S中。这个绿线在我们接下来的使用中有很大作用。
SAM的Transition Function
next(st):st遇到的下一个字符集,有next(st) = {S[i+1] | i ∈ endpos(st)}。例如next(S)={S[1], S[2], S[3], S[4], S[5], S[6], S[7]}={a, b, d},next(8)={S[4], S[7]}={b, d}。这里可以看成就是从状态st节点发出的线的符号。
对于一个状态st和一个字符c∈next(st),可以定义转移函数trans(st, c) = x | longest(st) + c ∈ substrings(x) ,具体怎么转移在下一个笔记里。
性质:对于一个状态st来说和一个next(st)中的字符c,你会发现substrings(st)中的所有子串后面接上一个字符c之后,新的子串仍然都属于同一个状态。比如对于状态4,next(4)={a},aabb,abb,bb后面接上字符a得到aabba,abba,bba,这些子串都属于状态6
实例
(这里用的爆搜,我的代码写的不好,但是也实现了,后面会有进步的):
问题:
输入
第一行包含一个字符串S,S长度不超过50。
第二行包含一个整数N,表示询问的数目。(1 <= N <= 10)
以下N行每行包括一个S的子串s,s不为空串。
输出
对于每一个询问s,求出包含s的状态st,输出一行依次包含shortest(st)、longest(st)和endpos(st)。其中endpos(st)由小到大输出,之间用一个空格分割。
样例输入
aabbabd
5
b
abbab
aa
aabbab
bb
样例输出
b b 3 4 6
bab aabbab 6
aa aa 2
bab aabbab 6
bb aabb 4
代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace hiho127
{
class Program
{
static bool equipment(List<int> l1, List<int> l2)
{
for (int i = 0; i < l1.Count; i++)
{
if (l1[i]!=l2[i])
{
return false;
}
}
return true;
}
static void Main(string[] args)
{
Dictionary<string, List<int>> myDic = new Dictionary<string, List<int>>();
string s = Console.ReadLine();
for (int i = 0; i < s.Length; i++)
{
for (int j = i; j >= 0; j--)
{
string temp = s.Substring(j, i - j + 1);
if (myDic.ContainsKey(temp))
{
myDic[temp].Add(i + 1);
}
else
{
List<int> lint = new List<int>();
lint.Add(i + 1);
myDic.Add(temp, lint);
}
}
}
int n = int.Parse(Console.ReadLine());
for (int i = 0; i < n; i++)
{
string temp = Console.ReadLine();
List<int> ltem = myDic[temp];
for (int j = 0; j < myDic.Count; j++)
{
List<int> lint = myDic.ElementAt(j).Value;
if (lint.Count == ltem.Count && equipment(lint,ltem))
{
Console.Write(myDic.ElementAt(j).Key);
break;
}
}
for (int j = myDic.Count - 1; j >= 0; j--)
{
List<int> lint = myDic.ElementAt(j).Value;
if (lint.Count == ltem.Count && equipment(lint, ltem))
{
Console.Write(" " + myDic.ElementAt(j).Key);
break;
}
}
foreach (int item in ltem)
{
Console.Write(" " + item.ToString());
}
Console.WriteLine();
}
}
}
}
注意:一开始一直是过90%点,注意试试这组数据
dbddba
3
db
dba
b