因为明天要讲解后缀自动机了,所以只能抱抱佛脚,临时做做题目。其实很久以前看过,但是不太懂,看的是clj的原文,不太懂。现在只能临时看看是怎么弄的,应付下。
------------------------------------------------------------------------------------------------------------------------------
1、自动机A为后缀自动机,A(sub) = true当且仅当sub是str的后缀。
2、一个较差的和后缀自动机有相同功能的东西是trie,把所有后缀都insert进去trie里面,但是空间&&时间复杂度O(N^2),不行。
3、有一个空间和时间复杂度都是O(N)的,就是:以ACADD为例子
也就是向每个字符都添加一条边,然后顺着这条边遍历,就能找到所有后缀。也能保存所有子串。
但是查找复杂度O(N^2)
注意到连接去字符'A'的边是有两条的,也就是这个图只能用邻接表来存,不能像trie这样用pNext[26]这样存,所以查找是O(N^2)
SAM就是为了解决了这样的问题而诞生的,也就是每一种字母边,只能存在一条。
后缀自动机是一个基于trie的字符串有向图,时间和空间复杂度均为O(N)
构造过程:
假设现在已经建立好前t个字符的后缀自动机,现在要增加一个字符x,使得其变成tx的后缀自动机。
每个节点保存的信息如下:
int cnt; // cnt表示后缀自动机中从root走到它最多需要多少步
int id; //表示它是第几个后缀自动机节点,指向了它,但是不知道是第几个,用id判断
int pos; //pos表示它在原串中的位置。
上面保存的信息都比较易懂,关键看看pNext[N]和fa,pNext[N]其实就是和字典树的26枚指针一样,只是为了O(1)判断是否存在这种字母的边。fa,指向的是一个能接受后缀的节点。什么叫能接受后缀的节点?比如我现在要建立"sazaa"的sam,一开始建立了's'的,如下图
红色的是fa边,然后现在新来一个字符'a',要使得其变成"sa"的sam,sam有什么特征呢,就是对于每一个"sa"的后缀,他都要能够识别。
那么,现在的末尾节点s,是肯定能够接受后缀的,然后爬fa边,去到root,root也和'a'连一条边,因为我也需要识别'a'这个后缀。
所以总的来说,fa边是为了寻找可以添加后缀节点的节点,使得其能识别当前建立的前缀串的所有后缀的。
在建立的时候,会遇到一种情况就是重边,我们用下标来判断是否能识别这个字符,那么很明显不能连接两条边。
但是建立的过程中,我们确实需要两条边。
比如建立到第四个"a"的时候,由于root-->a(2)已经有边,所以就不能连接root--->a(4)
那么怎么办呢,看cnt。
分两种情况,p和q的定义看代码
1、p->cnt + 1 = q->cnt
2、p->cnt + 1 != q->cnt
第一种情况,p->cnt + 1 = q->cnt说明p和q中间不包含任何其他字符,所以直接把q当作np接受新的后缀即可。
第二种情况可以通过虚拟一个节点,使得变成第一种情况
struct Node { int cnt; // cnt表示后缀自动机中从root走到它最多需要多少步 int id; //表示它是第几个后缀自动机节点,指向了它,但是不知道是第几个,用id判断 int pos; //pos表示它在原串中的位置。 struct Node *pNext[N], *fa; }suffixAutomaton[maxn * 2], *root, *last; //大小需要开2倍,因为有一些虚拟节点 int t; //用到第几个节点 struct Node *create(int cnt = -1, struct Node *node = NULL) { //新的节点 if (cnt != -1) { suffixAutomaton[t].cnt = cnt, suffixAutomaton[t].fa = NULL; suffixAutomaton[t].id = t; //必须要有的,不然id错误 for (int i = 0; i < N; ++i) suffixAutomaton[t].pNext[i] = NULL; } else { suffixAutomaton[t] = *node; //保留了node节点所有的指向信息 suffixAutomaton[t].id = t; //必须要有的,不然id错误 //可能需要注意下pos,在原串中的位置。现在pos等于原来node的pos } return &suffixAutomaton[t++]; } void addChar(int x, int pos) { //pos表示在原串的位置 struct Node *p = last, *np = create(p->cnt + 1, NULL); np->pos = pos, last = np; //last是最尾那个可接收后缀字符的点。 for (; p != NULL && p->pNext[x] == NULL; p = p->fa) p->pNext[x] = np; if (p == NULL) { np->fa = root; return; } struct Node *q = p->pNext[x]; if (q->cnt == p->cnt + 1) { //中间没有任何字符 np->fa = q; return; } // p: 当前往上爬到的可以接受后缀的节点 // np:当前插入字符x的新节点 // q: q = p->pNext[x],q就是p中指向的x字符的节点 // nq:因为q->cnt != p->cnt + 1而新建出来的模拟q的节点 struct Node *nq = create(-1, q); // 新的q节点,用来代替q,帮助np接收后缀字符 nq->cnt = p->cnt + 1; //就是需要这样,这样中间不包含任何字符 q->fa = nq, np->fa = nq; //现在nq是包含了本来q的所有指向信息 for (; p && p->pNext[x] == q; p = p->fa) { p->pNext[x] = nq; } } void init() { t = 0; root = last = create(0, NULL); } void build(char str[], int lenstr) { init(); for (int i = 1; i <= lenstr; ++i) addChar(str[i] - 'a', i); }
例子"sazaa"
ask && question
①、识别了一个子串后,若想得到它在原串中的开始位置,假设子串长度是lensub,识别到最后一个字符在原串中是第pos个位置,那么开始位置 beginPos = = pos – lensub + 1
②、后缀自动机也能识别所有的子串,按序dfs后,当前拾得的所以字符串都是主串的一个子串,不重不漏。
例如上面的"sazaa",dfs后
有:
s
sa
saz
saza
sazaa
a
az
aza
azaa
aa
z
za
zaa
不重不漏。
③、你说sam仅能识别后缀,那么现在为什么又能识别任何一个子串?
是这样的,sam为了不浪费空间,一个节点可能有多重身份,比如sazaa里面,最后面那个a(5),是被虚拟出来的那个节点(最下面那个a代替的),使得最下面那个a有三个身份,一是代替a(5)和a(4)接受后缀字符,三是自己作为一个后缀'a',其实可以在trie中很简单地用一个DFN标志当前这个节点是否能成为现在sam的后缀节点。是成为现在sam的后缀节点,因为sam的建立是在线的,是sam1的后缀节点,不一定是sam2的后缀节点。
④、一个很好的SAM教程。
http://hihocoder.com/problemset/problem/1441
#1441 : 后缀自动机一·基本概念
描述
小Hi:今天我们来学习一个强大的字符串处理工具:后缀自动机(Suffix Automaton,简称SAM)。对于一个字符串S,它对应的后缀自动机是一个最小的确定有限状态自动机(DFA),接受且只接受S的后缀。
小Hi:比如对于字符串S="aabbabd",它的后缀自动机是:
其中红色状态是终结状态。你可以发现对于S的后缀,我们都可以从S出发沿着字符标示的路径(蓝色实线)转移,最终到达终结状态。例如"bd"对应的路径是S59,"abd"对应的路径是S189,"abbabd"对应的路径是S184679。而对于不是S后缀的字符串,你会发现从S出发,最后会到达非终结状态或者“无路可走”。特别的,对于S的子串,最终会到达一个合法状态。例如"abba"路径是S1846,"bbab"路径是S5467。而对于其他不是S子串的字符串,最终会“无路可走”。 例如"aba"对应S18X,"aaba"对应S123X。(X表示没有转移匹配该字符)
小Ho:好像很厉害的样子!对于任意字符串都能构造出一个SAM吗?另外图中那些绿色虚线是什么?
小Hi:是的,任意字符串都能构造出一个SAM。我们知道SAM本质上是一个DFA,DFA可以用一个五元组 <字符集,状态集,转移函数、起始状态、终结状态集>来表示。下面我们将依次介绍对于一个给定的字符串S如何确定它对应的 状态集 和 转移函数 。至于那些绿色虚线虽然不是DFA的一部分,却是SAM的重要部分,有了这些链接SAM是如虎添翼,我们后面再细讲。
SAM的States
小Hi:这一节我们将介绍给定一个字符串S,如何确定S对应的SAM有哪些状态。首先我们先介绍一个概念 子串的结束位置集合 endpos。对于S的一个子串s,endpos(s) = s在S中所有出现的结束位置集合。还是以S="aabbabd"为例,endpos("ab") = {3, 6},因为"ab"一共出现了2次,结束位置分别是3和6。同理endpos("a") = {1, 2, 5}, endpos("abba") = {5}。
小Hi:我们把S的所有子串的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} |
小Ho:这些状态恰好就是上面SAM图中的状态。
小Hi:没错。此外,这些状态还有一些美妙的性质,且等我一一道来。首先对于S的两个子串s1和s2,不妨设length(s1) <= length(s2),那么 s1是s2的后缀当且仅当endpos(s1) ⊇ endpos(s2),s1不是s2的后缀当且仅当endpos(s1) ∩ endpos(s2) = ∅。
小Ho:我验证一下啊... 比如"ab"是"aabbab"的后缀,而endpos("ab")={3,6},endpos("aabbab")={6},是成立的。"b"是"ab"的后缀,endpos("b")={3,4,6}, endpos("ab")={3,6}也是成立的。"ab"不是"abb"的后缀,endpos("ab")={3,6},endpos("abb")={4},两者没有交集也是成立的。怎么证明呢?
小Hi:证明还是比较直观的。首先证明s1是s2的后缀=>endpos(s1) ⊇ endpos(s2):既然s1是s2后缀,所以每次s2出现时s1以必然伴随出现,所以有endpos(s1) ⊇ endpos(s2)。再证明endpos(s1) ⊇ endpos(s2)=>s1是s2的后缀:我们知道对于S的子串s2,endpos(s2)不会是空集,所以endpos(s1) ⊇ endpos(s2)=>存在结束位置x使得s1结束于x,并且s2也结束于x,又length(s1) <= length(s2),所以s1是s2的后缀。综上我们可知s1是s2的后缀当且仅当endpos(s1) ⊇ endpos(s2)。s1不是s2的后缀当且仅当endpos(s1) ∩ endpos(s2) = ∅是一个简单的推论,不再赘述。
小Ho:我好像对SAM的状态有一些认识了!我刚才看上面的表格就觉得SAM的一个状态里包含的子串好像有规律。考虑到SAM中的一个状态包含的子串都具有相同的endpos,那它们应该都互为后缀?
小Hi:你观察力还挺敏锐的。下面我们就来讲讲一个状态包含的子串究竟有什么关系。上文提到我们把S的所有子串按endpos分类,每一类就代表一个状态,所以我们可以认为一个状态包含了若干个子串。我们用substrings(st)表示状态st中包含的所有子串的集合,longest(st)表示st包含的最长的子串,shortest(st)表示st包含的最短的子串。例如对于状态7,substring(7)={aabbab,abbab,bbab,bab},longest(7)=aabbab,shortest(7)=bab。
小Hi:对于一个状态st,以及任意s∈substrings(st),都有s是longest(st)的后缀。证明比较容易,因为endpos(s)=endpos(longest(st)),所以endpos(s) ⊇ endpos(longest(st)),根据我们刚才证明的结论有s是longest(st)的后缀。
小Hi:此外,对于一个状态st,以及任意的longest(st)的后缀s,如果s的长度满足:length(shortest(st)) <= length(s) <= length(longsest(st)),那么s∈substrings(st)。 证明也是比较容易,因为:length(shortest(st)) <= length(s) <= length(longsest(st)),所以endpos(shortest(st)) ⊇ endpos(s) ⊇ endpos(longest(st)), 又endpos(shortest(st)) = endpos(longest(st)),所以endpos(shortest(st)) = endpos(s) = endpos(longest(st)),所以s∈substrings(st)。
小Ho:这么说来,substrings(st)包含的是longest(st)的一系列连续后缀?
小Hi:没错。比如你看状态7中包含的就是aabbab的长度分别是6,5,4,3的后缀;状态6包含的是aabba的长度分别是5,4,3,2的后缀。
SAM的Suffix Links
小Hi:前面我们讲到substrings(st)包含的是longest(st)的一系列连续后缀。这连续的后缀在某个地方会“断掉”。比如状态7,包含的子串依次是aabbab,abbab,bbab,bab。按照连续的规律下一个子串应该是"ab",但是"ab"没在状态7里,你能想到这是为什么么?
小Ho:aabbab,abbab,bbab,bab的endpos都是{6},下一个"ab"当然也在结束位置6出现过,但是"ab"还在结束位置3出现过,所以"ab"比aabbab,abbab,bbab,bab出现次数更多,于是就被分配到一个新的状态中了。
小Hi:没错,当longest(st)的某个后缀s在新的位置出现时,就会“断掉”,s会属于新的状态。比如上例中"ab"就属于状态8,endpos("ab"}={3,6}。当我们进一步考虑"ab"的下一个后缀"b"时,也会遇到相同的情况:"b"还在新的位置4出现过,所以endpos("b")={3,4,6},b属于状态5。在接下去处理"b"的后缀我们会遇到空串,endpos("")={0,1,2,3,4,5,6},状态是起始状态S。
小Hi:于是我们可以发现一条状态序列:7->8->5->S。这个序列的意义是longest(7)即aabbab的后缀依次在状态7、8、5、S中。我们用Suffix Link这一串状态链接起来,这条link就是上图中的绿色虚线。
小Ho:原来如此。
小Hi:Suffix Links后面会有妙用,我们暂且按下不表。
SAM的Transition Function
小Hi:最后我们来介绍SAM的转移函数。对于一个状态st,我们首先找到从它开始下一个遇到的字符可能是哪些。我们将st遇到的下一个字符集合记作next(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}。
小Hi:对于一个状态st来说和一个next(st)中的字符c,你会发现substrings(st)中的所有子串后面接上一个字符c之后,新的子串仍然都属于同一个状态。比如对于状态4,next(4)={a},aabb,abb,bb后面接上字符a得到aabba,abba,bba,这些子串都属于状态6。
小Hi:所以我们对于一个状态st和一个字符c∈next(st),可以定义转移函数trans(st, c) = x | longest(st) + c ∈ substrings(x) 。换句话说,我们在longest(st)(随便哪个子串都会得到相同的结果)后面接上一个字符c得到一个新的子串s,找到包含s的状态x,那么trans(st, c)就等于x。
小Ho:吼~ 终于把SAM中各个部分搞明白了。
小Hi:SAM的构造有时空复杂度均为O(length(S))的算法,我们将在后面介绍。这一期你可以先用暴力算法依照定义构造SAM,先对SAM有个直观认识再说。
小Ho:没问题,暴力算法我最拿手了。我先写程序去了。
习题:
字符串的最小表示法
https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=660
因为sam能识别所有的子串,那么把整个串复制一遍去后面,那么所有情况将会都考虑到。
贪心从root出发,找放在第一个地方的字母,有'a'就选'a',没'a'就选b等等等等。
选够lenstr个,就会知道那个最小表示法的那个串是什么。然后去原串找那个位置出现了这个串即可。
我用了hash来找。给个数据:sazaa
#include <bits/stdc++.h> #define IOS ios::sync_with_stdio(false) using namespace std; #define inf (0x3f3f3f3f) typedef long long int LL; const int maxn = 4e5 + 2; const int N = 26; struct Node { int cnt, id, pos; // cnt表示在后缀自动机中从root走到它最多需要多少步 //id表示它是第几个后缀自动机节点,指向了它,但是不知道是第几个,需要id判断 //pos表示它在原串中的位置。 struct Node *pNext[N], *fa; }suffixAutomaon[maxn * 2], *root, *last; //大小需要开2倍,因为有一些虚拟节点 int t; // 用到第几个节点 struct Node *create(int cnt = -1, struct Node *node = NULL) { //新的节点 if (cnt != -1) { suffixAutomaon[t].cnt = cnt, suffixAutomaon[t].fa = NULL; suffixAutomaon[t].id = t; for (int i = 0; i < N; ++i) suffixAutomaon[t].pNext[i] = NULL; } else { suffixAutomaon[t] = *node; //保留了node节点指向的信息 suffixAutomaon[t].id = t; //必须要有的,不然id错误 } return &suffixAutomaon[t++]; } void init() { t = 0; root = last = create(0, NULL); } void addChar(int x, int pos) { //pos表示在原串的位置 struct Node *p = last, *np = create(p->cnt + 1, NULL); np->pos = pos, last = np; //最后一个可接收后缀字符的点。 for (; p != NULL && p->pNext[x] == NULL; p = p->fa) p->pNext[x] = np; if (p == NULL) { np->fa = root; return; } struct Node *q = p->pNext[x]; if (q->cnt == p->cnt + 1) { //中间没有任何字符 np->fa = q; return; } struct Node *nq = create(-1, q); // 新的q节点,用来代替q,帮助np接收后缀字符 nq->cnt = p->cnt + 1; //就是需要这样,这样中间不包含任何字符 q->fa = nq, np->fa = nq; //现在nq是包含了本来q的所有指向信息 for (; p && p->pNext[x] == q; p = p->fa) { p->pNext[x] = nq; } } void build(char str[], int lenstr) { init(); for (int i = 1; i <= lenstr; ++i) addChar(str[i] - 'a', i); } char str[maxn]; void dfs(int id, string s) { cout << s << endl; for (int i = 0; i < N; ++i) { if (suffixAutomaon[id].pNext[i]) { char fuck = i + 'a'; dfs(suffixAutomaon[id].pNext[i]->id, s + fuck); } } } int lenstr; vector<int> pos; bool tofind(int id) { if (pos.size() == lenstr) return true; for (int i = 0; i < N; ++i) { if (suffixAutomaon[id].pNext[i] == NULL) continue; pos.push_back(suffixAutomaon[id].pNext[i]->pos); if (tofind(suffixAutomaon[id].pNext[i]->id)) return true; pos.pop_back(); } return false; } LL f[maxn]; LL poseed[maxn]; void work() { scanf("%s", str + 1); lenstr = strlen(str + 1); strncpy(str + lenstr + 1, str + 1, lenstr); str[2 * lenstr + 1] = '