后缀自动机如何限制串长
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了后缀自动机如何限制串长相关的知识,希望对你有一定的参考价值。
后缀自动机是一个有限状态自动机,有限状态自动机的功能是识别字符串。后缀自动机,可以识别一个字符串的所有子串。后缀自动机原理。我们考虑如果把一个字符串的后缀建立一棵字典树,那么其状态和结点都是O(N^2)级别的。因为不能充分利用字符串本身的特点。我们考虑,假设我们有一个字符串T,某个串s是其的子串,那么我们在s后面加入一些字符就有可能使其变成T的后缀,而如果串s不是T的子串,就没有必要浪费空间。所以,为了识别所有的后缀,就要尽可能的利用这些可能。后缀自动机是最简状态自动机,其状态数是线性的,可以证明。
我们按照一定的顺序来一点一点剖分自动机。
状态。我们知道,后缀自动机是可以在线构建的。每插入一个新的字符,在自动机中就会产生一个新的状态。我们用State(s)表示已经插入字符串s后所能达到的状态。每个状态里面我们有这样3个元素。 pre,其指向上一个可以接收相同后缀的结点,我们不能把这个理解成为指向当前状态的父亲,因为一个节点可能有许多个“父亲”。 len,表示从根节点(空状态,即为没有插入任何一个字符)走到当前点最多要走多少步,也可以理解为在当前状态下自动机可以识别的最长后缀的长度。next[26],记录s已经加入自动机后,再加入一个字符后,该字符在自动机中的位置。
可以识别。我们说一个状态State可以识别某个字符串x,其意思表示为,在自动机中存在从根结点到当前的状态的一条路径,使得串x是这条路径形成的字符串的一个子串。
Right集合,我们定义这样一个集合:其表示某一状态下所有能识别的后缀的右端点位置集合。
举个例子,比如说对于这样一个串T = “aabaaabaaabbab”。可以识别“ab”的的Right集合 S1 = {3,7,11,13}。
在继续向下之前,先提出一个注意的地方,就是,当我们达到State(s)时,假设是第一次达到,那么此时的自动机是串s的自动机,而不是整个文本串T(s是其的一个子串)的自动机。一定要注意语句的主语。
Parent树。我们先来对其进行一个如下的分析。
首先对于一个空串来说,其可识别的位置为所有位置(其实没有必要,这么说是为了一会的图画方便些)。
Right(空) = {1,2,3,4,5,6,7,8,9,10,11,12,13,14};
然后我们考虑长度为1的。
Right(“a”)=1,2,4,5,6,8,9,10,13,
Right("b") = 3, 7, 11, 12, 14.
接下来, Right(“aa”) = 2, 5, 6, 9, 10, Right(“ab”) = 3, 7, 11, 14, Right("bb") = 12, Right("ba") = 4,8,13
Right(“aaa”) = 6, 10 Right(“aab”) = 3, 7, 11 Right("aba") = 4, 8 Right("baa") = 5, 9 Right("bba") = 13 Right("bab") = 14 Right("abb") = 12
Right("aaab") = 7, 11 Right(“aaba”) = 4, 8 Right("abaa") = 5, 9 Right("baaa") = 6, 10 Right("aabb") = 12 Right("abba") = 13 Right("bbab") = 14
再向下由于人类智慧太不好搞了,就不画了。但是,根据现有的Right集合,或许我们可以发现些什么。给出一张图来直观地展示:

我们用小学生看图找特点的思维来一点点看这张图。用罗列的方法来找特点。(嘿嘿,其实 是我的思路比较乱)
1、首先要记住一件事:树的深度不等于根到这个节点组成的字符串长度。很明显的,有几个单个的叶子结点很好的说明了这个特点。
2、到某个节点组成的字符串的长度越长,其Right集合里面的元素就越少,也就是说,其在串中的可以匹配的位置就越少。
3、一个节点的Right集合大小等于以其为根的子树的叶子结点的个数。(这个好像不太明显,因为 我没有把整个树画完QAQ……不过大家可以自行向下扩展)
4、叶子结点除外,根到一个结点组成的字符串的长度等于其父亲的长度加 + 1,但是我们要清楚的知道,父亲的长度是指其Right集合所表示的所有字符串的最长长度。举个例子来说,对于这样一个串,“AABAABAAB“,Rigth("AB") = {3,6,9} Right(”ABB“) = {3,6,9},他们的Right集合是相同的。所以,用这个也可以来解释 第1 点。树的深度不等于根到这个节点组成 的字符串的长度。
5、一个非叶结点至少有两个儿子。
6、两个不同的Right集合,其关系要么是一个是另一个的真子集,要么两个完全不相交。在树上这个是很直观的吧。
我们再来理解一下pre指针的作用。
某个状态的pre指针指向的结点是当前结点(cur)Right集合所表示的字符串的最长公共后缀(不是其本身)
好,下面我们来具体说明一下构建自动机的过程:
在这个过程中我们需要两个指针,last表示上一次插入的结点位置,cur表示当前的结点位置。
1、首先是建立一个空结点,表示空串的状态,其len = 0, pre = null;这个是很好理解的吧,在Parent树中,这个结点的角色是树根。
2、然后假设我们已经构建了前i-1个字符的后缀自动机,现在我们考虑加入第i个字符 x。首先先从last 向 cur连接一条边为x的出边,然后cur.len = last.len + 1,这些都是很显然的吧。
3、好,现在我们需要确定cur的pre指针的指向。我们现在在Parent树上沿着last的pre指针向上跳。
如果不小心跳到了NULL,说明当前自动机中没有x这个字符,那么很好处理,直接把cur的pre指针指向root,注意是root,而不是NULL。
如果没有跳到NULL,那么,如果last->pre没有x这样一条出边。那么这情况好说,直接从last->pre向cur连一条为x的出边。为什么要这么连?我们考虑,我们已知last->pre指向的是当前状态Right集合所表示的所有字符串的最长公共后缀,我们在当前状态的字符串后加上了一个新的字符x,那就相当于在所有的后缀后面都加了一个x,但是为了保证不重复不遗漏的在后缀上加上这个字符x,所以我们要选择最长的那个来添加,这样自然就用到了这个pre指针, 跳到最长后缀的地方。
如果last->pre有这样一条出边。那么我们就要分两种情况来讨论:
我们用p来代表last->pre,用q来代表last->pre的x出边。
情况1. 如果q.len == p.len + 1,那么就把cur的pre设为q。
情况2. 如果q.len > p.len + 1,那么就要把拷贝一个新的结点。
情况一情况二的解释如下:(来自王梦迪(NOI2015金牌得主)的博客)
第二种情况——当我们进入一个已存在的转移(p,q)时。这意味着我们试图向字符串中添加字符x+c(其中x是字符串s的某一后缀,长度为len(p)),且该字符串先前已经被加入了自动机(即,字符串x+c已经作为子串包含在字符串s中)。因为我们假设字符串s的自动机已被正确构建,我们并不应该添加新的转移。然而,cur的后缀链接指向哪里有一定复杂性。我们需要将后缀链接指向一个长度恰好和x+c相等的状态,即,该状态的len值必须等于len(p)+1.但这样一种情况可能并不存在:在此情况下我们必须添加一个“分割的”状态。
因此,一种可能的情形是,转移(p,q)变得连续,即,len(q)=len(p)+1.在这种情况下,事情变得简单,不必再进行任何分割,我们只需要将cur的后缀链接指向q。
另一种更复杂的情况——当转移不连续时,即len(q)>len(p)+1.这意味着状态q不仅仅匹配对我们必须的,长度len(p)+1的子串w+c,它还匹配一个更长的子串。我们不得不新建一个“分割的”状态q:将子串分割成两段,第一段将恰在长度len(p)+1处结束 参考技术A 后缀自动机是一个有限状态自动机,有限状态自动机的功能是识别字符串。后缀自动机,可以识别一个字符串的所有子串。
后缀自动机原理。我们考虑如果把一个字符串的后缀建立一棵字典树,那么其状态和结点都是O(N^2)级别的。因为不能充分利用字符串本身的特点。我们考虑,假设我们有一个字符串T,某个串s是其的子串,那么我们在s后面加入一些字符就有可能使其变成T的后缀,而如果串s不是T的子串,就没有必要浪费空间。所以,为了识别所有的后缀,就要尽可能的利用这些可能。后缀自动机是最简状态自动机,其状态数是线性的,可以证明。
我们按照一定的顺序来一点一点剖分自动机。
状态。我们知道,后缀自动机是可以在线构建的。每插入一个新的字符,在自动机中就会产生一个新的状态。我们用State(s)表示已经插入字符串s后所能达到的状态。每个状态里面我们有这样3个元素。 pre,其指向上一个可以接收相同后缀的结点,我们不能把这个理解成为指向当前状态的父亲,因为一个节点可能有许多个“父亲”。 len,表示从根节点(空状态,即为没有插入任何一个字符)走到当前点最多要走多少步,也可以理解为在当前状态下自动机可以识别的最长后缀的长度。next[26],记录s已经加入自动机后,再加入一个字符后,该字符在自动机中的位置。
可以识别。我们说一个状态State可以识别某个字符串x,其意思表示为,在自动机中存在从根结点到当前的状态的一条路径,使得串x是这条路径形成的字符串的一个子串。
Right集合,我们定义这样一个集合:其表示某一状态下所有能识别的后缀的右端点位置集合。
举个例子,比如说对于这样一个串T = “aabaaabaaabbab”。可以识别“ab”的的Right集合 S1 = {3,7,11,13}。
在继续向下之前,先提出一个注意的地方,就是,当我们达到State(s)时,假设是第一次达到,那么此时的自动机是串s的自动机,而不是整个文本串T(s是其的一个子串)的自动机。一定要注意语句的主语。
Parent树。我们先来对其进行一个如下的分析。
首先对于一个空串来说,其可识别的位置为所有位置(其实没有必要,这么说是为了一会的图画方便些)。
Right(空) = {1,2,3,4,5,6,7,8,9,10,11,12,13,14};
然后我们考虑长度为1的。
Right(“a”)=1,2,4,5,6,8,9,10,13,
Right("b") = 3, 7, 11, 12, 14.
接下来, Right(“aa”) = 2, 5, 6, 9, 10, Right(“ab”) = 3, 7, 11, 14, Right("bb") = 12, Right("ba") = 4,8,13
Right(“aaa”) = 6, 10 Right(“aab”) = 3, 7, 11 Right("aba") = 4, 8 Right("baa") = 5, 9 Right("bba") = 13 Right("bab") = 14 Right("abb") = 12
Right("aaab") = 7, 11 Right(“aaba”) = 4, 8 Right("abaa") = 5, 9 Right("baaa") = 6, 10 Right("aabb") = 12 Right("abba") = 13 Right("bbab") = 14
再向下由于人类智慧太不好搞了,就不画了。但是,根据现有的Right集合,或许我们可以发现些什么。给出一张图来直观地展示:

我们用小学生看图找特点的思维来一点点看这张图。用罗列的方法来找特点。(嘿嘿,其实 是我的思路比较乱)
1、首先要记住一件事:树的深度不等于根到这个节点组成的字符串长度。很明显的,有几个单个的叶子结点很好的说明了这个特点。
2、到某个节点组成的字符串的长度越长,其Right集合里面的元素就越少,也就是说,其在串中的可以匹配的位置就越少。
3、一个节点的Right集合大小等于以其为根的子树的叶子结点的个数。(这个好像不太明显,因为 我没有把整个树画完QAQ……不过大家可以自行向下扩展)
4、叶子结点除外,根到一个结点组成的字符串的长度等于其父亲的长度加 + 1,但是我们要清楚的知道,父亲的长度是指其Right集合所表示的所有字符串的最长长度。举个例子来说,对于这样一个串,“AABAABAAB“,Rigth("AB") = {3,6,9} Right(”ABB“) = {3,6,9},他们的Right集合是相同的。所以,用这个也可以来解释 第1 点。树的深度不等于根到这个节点组成 的字符串的长度。
5、一个非叶结点至少有两个儿子。
6、两个不同的Right集合,其关系要么是一个是另一个的真子集,要么两个完全不相交。在树上这个是很直观的吧。
我们再来理解一下pre指针的作用。
某个状态的pre指针指向的结点是当前结点(cur)Right集合所表示的字符串的最长公共后缀(不是其本身)
好,下面我们来具体说明一下构建自动机的过程:
在这个过程中我们需要两个指针,last表示上一次插入的结点位置,cur表示当前的结点位置。
1、首先是建立一个空结点,表示空串的状态,其len = 0, pre = null;这个是很好理解的吧,在Parent树中,这个结点的角色是树根。
2、然后假设我们已经构建了前i-1个字符的后缀自动机,现在我们考虑加入第i个字符 x。首先先从last 向 cur连接一条边为x的出边,然后cur.len = last.len + 1,这些都是很显然的吧。
3、好,现在我们需要确定cur的pre指针的指向。我们现在在Parent树上沿着last的pre指针向上跳。
如果不小心跳到了NULL,说明当前自动机中没有x这个字符,那么很好处理,直接把cur的pre指针指向root,注意是root,而不是NULL。
如果没有跳到NULL,那么,如果last->pre没有x这样一条出边。那么这情况好说,直接从last->pre向cur连一条为x的出边。为什么要这么连?我们考虑,我们已知last->pre指向的是当前状态Right集合所表示的所有字符串的最长公共后缀,我们在当前状态的字符串后加上了一个新的字符x,那就相当于在所有的后缀后面都加了一个x,但是为了保证不重复不遗漏的在后缀上加上这个字符x,所以我们要选择最长的那个来添加,这样自然就用到了这个pre指针, 跳到最长后缀的地方。
如果last->pre有这样一条出边。那么我们就要分两种情况来讨论:
我们用p来代表last->pre,用q来代表last->pre的x出边。
情况1. 如果q.len == p.len + 1,那么就把cur的pre设为q。
情况2. 如果q.len > p.len + 1,那么就要把拷贝一个新的结点。
情况一情况二的解释如下:(来自王梦迪(NOI2015金牌得主)的博客)
第二种情况——当我们进入一个已存在的转移(p,q)时。这意味着我们试图向字符串中添加字符x+c(其中x是字符串s的某一后缀,长度为len(p)),且该字符串先前已经被加入了自动机(即,字符串x+c已经作为子串包含在字符串s中)。因为我们假设字符串s的自动机已被正确构建,我们并不应该添加新的转移。然而,cur的后缀链接指向哪里有一定复杂性。我们需要将后缀链接指向一个长度恰好和x+c相等的状态,即,该状态的len值必须等于len(p)+1.但这样一种情况可能并不存在:在此情况下我们必须添加一个“分割的”状态。
因此,一种可能的情形是,转移(p,q)变得连续,即,len(q)=len(p)+1.在这种情况下,事情变得简单,不必再进行任何分割,我们只需要将cur的后缀链接指向q。
另一种更复杂的情况——当转移不连续时,即len(q)>len(p)+1.这意味着状态q不仅仅匹配对我们必须的,长度len(p)+1的子串w+c,它还匹配一个更长的子串。我们不得不新建一个“分割的”状态q:将子串分割成两段,第一段将恰在长度len(p)+1处结束
回答于 8 小时前
赞同1
家用空气净化器,潮流新品,好货热卖,更多优惠尽在淘宝!
跨境代发家用桌面空气净化器 办公室小型负离子杀菌除烟尘净化机
¥117 元
空气净化器家用室内客厅除烟尘除甲醛紫外线UV消毒机负离子净化器
¥98 元
除烟味净化空气抗菌除雾霾空气清新器一体机净化机家用空气净化器
¥338 元
simba.taobao.com广告
买十大品牌奶粉是哪些上万能的淘宝!优享品质,惊喜价格!
全脂奶粉800g罐装
¥105.8 元
奶粉奶粉400克*1袋装奶粉成人儿童老人奶粉
¥93.6 元
包邮雀巢甜奶粉800g克*3袋装成人学生甜奶粉全家营养甜奶粉
¥120 元
simba.taobao.com广告
什么牌子的奶粉好,淘宝热卖产品推荐
比较推荐的奶粉品牌-购物上淘宝,品类集结,热卖好物!海量优质商品,轻松畅购!尽享优惠,买东西上淘宝,一站轻松购!
广告
怎么利用自动机求多个串的最长公共子串
求多串最长公共子串可以用后缀自动机解决问题。(我就当你已经会写后缀自动机了。不会也可以去学学。)先建立一个串的后缀自动机,然后再去拿其他串往里面跑一遍。只不过在维护节点的信息时,要多设两个int:minf,maxg。minf表示该节点在全局中的最小匹配长度(多串的公共子串当然是取交集)。maxg表示该节点在当前串种的最大匹配长度(对于单串,当然是取最大值)。即:struct SAM int step, pre, minf, maxg, son[26];h[N << 1]; //N为串的长度。然后跑自动机的代码(我是自己写的,不能保证正确性,不过应该差不多了):void Travel(char *s) int slen = strlen(s), u = 0, len = 0; for (int i = 0; i < slen; i++) int j = s[i] - 'A'; if (h[u].son[j]) len++, u = h[u].son[j]; else while (u && !h[u].son[j]) u = h[u].pre; if (!u && !h[u].son[j]) len = 0; else len = h[u].step + 1; u = h[u].son[j]; h[u].maxg = max(h[u].maxg, len); for (int i = 1; i <= tot; i++) h[i].minf = min(h[i].minf, h[i].maxg); h[i].maxg = 0; 最长长度就是: for (int i = 1; i <= tot; i++) ans = max(ans, h[i].minf);如果要求出具体是哪个串的话,就可以在求ans时记录一个tmpstep,储存子串的最末位。就这么多了。如果我有哪些地方弄错了,还请指正。 参考技术B 后缀自动机是一个有限状态自动机,有限状态自动机的功能是识别字符串。后缀自动机,可以识别一个字符串的所有子串。
后缀自动机原理。我们考虑如果把一个字符串的后缀建立一棵字典树,那么其状态和结点都是O(N^2)级别的。因为不能充分利用字符串本身的特点。我们考虑,假设我们有一个字符串T,某个串s是其的子串,那么我们在s后面加入一些字符就有可能使其变成T的后缀,而如果串s不是T的子串,就没有必要浪费空间。所以,为了识别所有的后缀,就要尽可能的利用这些可能。后缀自动机是最简状态自动机,其状态数是线性的,可以证明。
我们按照一定的顺序来一点一点剖分自动机。
状态。我们知道,后缀自动机是可以在线构建的。每插入一个新的字符,在自动机中就会产生一个新的状态。我们用State(s)表示已经插入字符串s后所能达到的状态。每个状态里面我们有这样3个元素。 pre,其指向上一个可以接收相同后缀的结点,我们不能把这个理解成为指向当前状态的父亲,因为一个节点可能有许多个“父亲”。 len,表示从根节点(空状态,即为没有插入任何一个字符)走到当前点最多要走多少步,也可以理解为在当前状态下自动机可以识别的最长后缀的长度。next[26],记录s已经加入自动机后,再加入一个字符后,该字符在自动机中的位置。
可以识别。我们说一个状态State可以识别某个字符串x,其意思表示为,在自动机中存在从根结点到当前的状态的一条路径,使得串x是这条路径形成的字符串的一个子串。
Right集合,我们定义这样一个集合:其表示某一状态下所有能识别的后缀的右端点位置集合。
举个例子,比如说对于这样一个串T = “aabaaabaaabbab”。可以识别“ab”的的Right集合 S1 = {3,7,11,13}。
在继续向下之前,先提出一个注意的地方,就是,当我们达到State(s)时,假设是第一次达到,那么此时的自动机是串s的自动机,而不是整个文本串T(s是其的一个子串)的自动机。一定要注意语句的主语。
Parent树。我们先来对其进行一个如下的分析。
首先对于一个空串来说,其可识别的位置为所有位置(其实没有必要,这么说是为了一会的图画方便些)。
Right(空) = {1,2,3,4,5,6,7,8,9,10,11,12,13,14};
然后我们考虑长度为1的。
Right(“a”)=1,2,4,5,6,8,9,10,13,
Right("b") = 3, 7, 11, 12, 14.
接下来, Right(“aa”) = 2, 5, 6, 9, 10, Right(“ab”) = 3, 7, 11, 14, Right("bb") = 12, Right("ba") = 4,8,13
Right(“aaa”) = 6, 10 Right(“aab”) = 3, 7, 11 Right("aba") = 4, 8 Right("baa") = 5, 9 Right("bba") = 13 Right("bab") = 14 Right("abb") = 12
Right("aaab") = 7, 11 Right(“aaba”) = 4, 8 Right("abaa") = 5, 9 Right("baaa") = 6, 10 Right("aabb") = 12 Right("abba") = 13 Right("bbab") = 14
再向下由于人类智慧太不好搞了,就不画了。但是,根据现有的Right集合,或许我们可以发现些什么。给出一张图来直观地展示:

我们用小学生看图找特点的思维来一点点看这张图。用罗列的方法来找特点。(嘿嘿,其实 是我的思路比较乱)
1、首先要记住一件事:树的深度不等于根到这个节点组成的字符串长度。很明显的,有几个单个的叶子结点很好的说明了这个特点。
2、到某个节点组成的字符串的长度越长,其Right集合里面的元素就越少,也就是说,其在串中的可以匹配的位置就越少。
3、一个节点的Right集合大小等于以其为根的子树的叶子结点的个数。(这个好像不太明显,因为 我没有把整个树画完QAQ……不过大家可以自行向下扩展)
4、叶子结点除外,根到一个结点组成的字符串的长度等于其父亲的长度加 + 1,但是我们要清楚的知道,父亲的长度是指其Right集合所表示的所有字符串的最长长度。举个例子来说,对于这样一个串,“AABAABAAB“,Rigth("AB") = {3,6,9} Right(”ABB“) = {3,6,9},他们的Right集合是相同的。所以,用这个也可以来解释 第1 点。树的深度不等于根到这个节点组成 的字符串的长度。
5、一个非叶结点至少有两个儿子。
6、两个不同的Right集合,其关系要么是一个是另一个的真子集,要么两个完全不相交。在树上这个是很直观的吧。
我们再来理解一下pre指针的作用。
某个状态的pre指针指向的结点是当前结点(cur)Right集合所表示的字符串的最长公共后缀(不是其本身)
好,下面我们来具体说明一下构建自动机的过程:
在这个过程中我们需要两个指针,last表示上一次插入的结点位置,cur表示当前的结点位置。
1、首先是建立一个空结点,表示空串的状态,其len = 0, pre = null;这个是很好理解的吧,在Parent树中,这个结点的角色是树根。
2、然后假设我们已经构建了前i-1个字符的后缀自动机,现在我们考虑加入第i个字符 x。首先先从last 向 cur连接一条边为x的出边,然后cur.len = last.len + 1,这些都是很显然的吧。
3、好,现在我们需要确定cur的pre指针的指向。我们现在在Parent树上沿着last的pre指针向上跳。
如果不小心跳到了NULL,说明当前自动机中没有x这个字符,那么很好处理,直接把cur的pre指针指向root,注意是root,而不是NULL。
如果没有跳到NULL,那么,如果last->pre没有x这样一条出边。那么这情况好说,直接从last->pre向cur连一条为x的出边。为什么要这么连?我们考虑,我们已知last->pre指向的是当前状态Right集合所表示的所有字符串的最长公共后缀,我们在当前状态的字符串后加上了一个新的字符x,那就相当于在所有的后缀后面都加了一个x,但是为了保证不重复不遗漏的在后缀上加上这个字符x,所以我们要选择最长的那个来添加,这样自然就用到了这个pre指针, 跳到最长后缀的地方。
如果last->pre有这样一条出边。那么我们就要分两种情况来讨论:
我们用p来代表last->pre,用q来代表last->pre的x出边。
情况1. 如果q.len == p.len + 1,那么就把cur的pre设为q。
情况2. 如果q.len > p.len + 1,那么就要把拷贝一个新的结点。
情况一情况二的解释如下:(来自王梦迪(NOI2015金牌得主)的博客)
第二种情况——当我们进入一个已存在的转移(p,q)时。这意味着我们试图向字符串中添加字符x+c(其中x是字符串s的某一后缀,长度为len(p)),且该字符串先前已经被加入了自动机(即,字符串x+c已经作为子串包含在字符串s中)。因为我们假设字符串s的自动机已被正确构建,我们并不应该添加新的转移。然而,cur的后缀链接指向哪里有一定复杂性。我们需要将后缀链接指向一个长度恰好和x+c相等的状态,即,该状态的len值必须等于len(p)+1.但这样一种情况可能并不存在:在此情况下我们必须添加一个“分割的”状态。
因此,一种可能的情形是,转移(p,q)变得连续,即,len(q)=len(p)+1.在这种情况下,事情变得简单,不必再进行任何分割,我们只需要将cur的后缀链接指向q。
另一种更复杂的情况——当转移不连续时,即len(q)>len(p)+1.这意味着状态q不仅仅匹配对我们必须的,长度len(p)+1的子串w+c,它还匹配一个更长的子串。我们不得不新建一个“分割的”状态q:将子串分割成两段,第一段将恰在长度len(p)+1处结束 参考技术C 后缀自动机是一个有限状态自动机,有限状态自动机的功能是识别字符串。后缀自动机,可以识别一个字符串的所有子串。
后缀自动机原理。我们考虑如果把一个字符串的后缀建立一棵字典树,那么其状态和结点都是O(N^2)级别的。因为不能充分利用字符串本身的特点。我们考虑,假设我们有一个字符串T,某个串s是其的子串,那么我们在s后面加入一些字符就有可能使其变成T的后缀,而如果串s不是T的子串,就没有必要浪费空间。所以,为了识别所有的后缀,就要尽可能的利用这些可能。后缀自动机是最简状态自动机,其状态数是线性的,可以证明。
我们按照一定的顺序来一点一点剖分自动机。
状态。我们知道,后缀自动机是可以在线构建的。每插入一个新的字符,在自动机中就会产生一个新的状态。我们用State(s)表示已经插入字符串s后所能达到的状态。每个状态里面我们有这样3个元素。 pre,其指向上一个可以接收相同后缀的结点,我们不能把这个理解成为指向当前状态的父亲,因为一个节点可能有许多个“父亲”。 len,表示从根节点(空状态,即为没有插入任何一个字符)走到当前点最多要走多少步,也可以理解为在当前状态下自动机可以识别的最长后缀的长度。next[26],记录s已经加入自动机后,再加入一个字符后,该字符在自动机中的位置。
可以识别。我们说一个状态State可以识别某个字符串x,其意思表示为,在自动机中存在从根结点到当前的状态的一条路径,使得串x是这条路径形成的字符串的一个子串。
Right集合,我们定义这样一个集合:其表示某一状态下所有能识别的后缀的右端点位置集合。
举个例子,比如说对于这样一个串T = “aabaaabaaabbab”。可以识别“ab”的的Right集合 S1 = {3,7,11,13}。
在继续向下之前,先提出一个注意的地方,就是,当我们达到State(s)时,假设是第一次达到,那么此时的自动机是串s的自动机,而不是整个文本串T(s是其的一个子串)的自动机。一定要注意语句的主语。
Parent树。我们先来对其进行一个如下的分析。
首先对于一个空串来说,其可识别的位置为所有位置(其实没有必要,这么说是为了一会的图画方便些)。
Right(空) = {1,2,3,4,5,6,7,8,9,10,11,12,13,14};
然后我们考虑长度为1的。
Right(“a”)=1,2,4,5,6,8,9,10,13,
Right("b") = 3, 7, 11, 12, 14.
接下来, Right(“aa”) = 2, 5, 6, 9, 10, Right(“ab”) = 3, 7, 11, 14, Right("bb") = 12, Right("ba") = 4,8,13
Right(“aaa”) = 6, 10 Right(“aab”) = 3, 7, 11 Right("aba") = 4, 8 Right("baa") = 5, 9 Right("bba") = 13 Right("bab") = 14 Right("abb") = 12
Right("aaab") = 7, 11 Right(“aaba”) = 4, 8 Right("abaa") = 5, 9 Right("baaa") = 6, 10 Right("aabb") = 12 Right("abba") = 13 Right("bbab") = 14
再向下由于人类智慧太不好搞了,就不画了。但是,根据现有的Right集合,或许我们可以发现些什么。给出一张图来直观地展示:

我们用小学生看图找特点的思维来一点点看这张图。用罗列的方法来找特点。(嘿嘿,其实 是我的思路比较乱)
1、首先要记住一件事:树的深度不等于根到这个节点组成的字符串长度。很明显的,有几个单个的叶子结点很好的说明了这个特点。
2、到某个节点组成的字符串的长度越长,其Right集合里面的元素就越少,也就是说,其在串中的可以匹配的位置就越少。
3、一个节点的Right集合大小等于以其为根的子树的叶子结点的个数。(这个好像不太明显,因为 我没有把整个树画完QAQ……不过大家可以自行向下扩展)
4、叶子结点除外,根到一个结点组成的字符串的长度等于其父亲的长度加 + 1,但是我们要清楚的知道,父亲的长度是指其Right集合所表示的所有字符串的最长长度。举个例子来说,对于这样一个串,“AABAABAAB“,Rigth("AB") = {3,6,9} Right(”ABB“) = {3,6,9},他们的Right集合是相同的。所以,用这个也可以来解释 第1 点。树的深度不等于根到这个节点组成 的字符串的长度。
5、一个非叶结点至少有两个儿子。
6、两个不同的Right集合,其关系要么是一个是另一个的真子集,要么两个完全不相交。在树上这个是很直观的吧。
我们再来理解一下pre指针的作用。
某个状态的pre指针指向的结点是当前结点(cur)Right集合所表示的字符串的最长公共后缀(不是其本身)
好,下面我们来具体说明一下构建自动机的过程:
在这个过程中我们需要两个指针,last表示上一次插入的结点位置,cur表示当前的结点位置。
1、首先是建立一个空结点,表示空串的状态,其len = 0, pre = null;这个是很好理解的吧,在Parent树中,这个结点的角色是树根。
2、然后假设我们已经构建了前i-1个字符的后缀自动机,现在我们考虑加入第i个字符 x。首先先从last 向 cur连接一条边为x的出边,然后cur.len = last.len + 1,这些都是很显然的吧。
3、好,现在我们需要确定cur的pre指针的指向。我们现在在Parent树上沿着last的pre指针向上跳。
如果不小心跳到了NULL,说明当前自动机中没有x这个字符,那么很好处理,直接把cur的pre指针指向root,注意是root,而不是NULL。
如果没有跳到NULL,那么,如果last->pre没有x这样一条出边。那么这情况好说,直接从last->pre向cur连一条为x的出边。为什么要这么连?我们考虑,我们已知last->pre指向的是当前状态Right集合所表示的所有字符串的最长公共后缀,我们在当前状态的字符串后加上了一个新的字符x,那就相当于在所有的后缀后面都加了一个x,但是为了保证不重复不遗漏的在后缀上加上这个字符x,所以我们要选择最长的那个来添加,这样自然就用到了这个pre指针, 跳到最长后缀的地方。
如果last->pre有这样一条出边。那么我们就要分两种情况来讨论:
我们用p来代表last->pre,用q来代表last->pre的x出边。
情况1. 如果q.len == p.len + 1,那么就把cur的pre设为q。
情况2. 如果q.len > p.len + 1,那么就要把拷贝一个新的结点。
情况一情况二的解释如下:(来自王梦迪(NOI2015金牌得主)的博客)
第二种情况——当我们进入一个已存在的转移(p,q)时。这意味着我们试图向字符串中添加字符x+c(其中x是字符串s的某一后缀,长度为len(p)),且该字符串先前已经被加入了自动机(即,字符串x+c已经作为子串包含在字符串s中)。因为我们假设字符串s的自动机已被正确构建,我们并不应该添加新的转移。然而,cur的后缀链接指向哪里有一定复杂性。我们需要将后缀链接指向一个长度恰好和x+c相等的状态,即,该状态的len值必须等于len(p)+1.但这样一种情况可能并不存在:在此情况下我们必须添加一个“分割的”状态。
因此,一种可能的情形是,转移(p,q)变得连续,即,len(q)=len(p)+1.在这种情况下,事情变得简单,不必再进行任何分割,我们只需要将cur的后缀链接指向q。
另一种更复杂的情况——当转移不连续时,即len(q)>len(p)+1.这意味着状态q不仅仅匹配对我们必须的,长度len(p)+1的子串w+c,它还匹配一个更长的子串。我们不得不新建一个“分割的”状态q:将子串分割成两段,第一段将恰在长度len(p)+1处结束 参考技术D 后缀自动机是一个有限状态自动机,有限状态自动机的功能是识别字符串。后缀自动机,可以识别一个字符串的所有子串。
后缀自动机原理。我们考虑如果把一个字符串的后缀建立一棵字典树,那么其状态和结点都是O(N^2)级别的。因为不能充分利用字符串本身的特点。我们考虑,假设我们有一个字符串T,某个串s是其的子串,那么我们在s后面加入一些字符就有可能使其变成T的后缀,而如果串s不是T的子串,就没有必要浪费空间。所以,为了识别所有的后缀,就要尽可能的利用这些可能。后缀自动机是最简状态自动机,其状态数是线性的,可以证明。
我们按照一定的顺序来一点一点剖分自动机。
状态。我们知道,后缀自动机是可以在线构建的。每插入一个新的字符,在自动机中就会产生一个新的状态。我们用State(s)表示已经插入字符串s后所能达到的状态。每个状态里面我们有这样3个元素。 pre,其指向上一个可以接收相同后缀的结点,我们不能把这个理解成为指向当前状态的父亲,因为一个节点可能有许多个“父亲”。 len,表示从根节点(空状态,即为没有插入任何一个字符)走到当前点最多要走多少步,也可以理解为在当前状态下自动机可以识别的最长后缀的长度。next[26],记录s已经加入自动机后,再加入一个字符后,该字符在自动机中的位置。
可以识别。我们说一个状态State可以识别某个字符串x,其意思表示为,在自动机中存在从根结点到当前的状态的一条路径,使得串x是这条路径形成的字符串的一个子串。
Right集合,我们定义这样一个集合:其表示某一状态下所有能识别的后缀的右端点位置集合。
举个例子,比如说对于这样一个串T = “aabaaabaaabbab”。可以识别“ab”的的Right集合 S1 = {3,7,11,13}。
在继续向下之前,先提出一个注意的地方,就是,当我们达到State(s)时,假设是第一次达到,那么此时的自动机是串s的自动机,而不是整个文本串T(s是其的一个子串)的自动机。一定要注意语句的主语。
Parent树。我们先来对其进行一个如下的分析。
首先对于一个空串来说,其可识别的位置为所有位置(其实没有必要,这么说是为了一会的图画方便些)。
Right(空) = {1,2,3,4,5,6,7,8,9,10,11,12,13,14};
然后我们考虑长度为1的。
Right(“a”)=1,2,4,5,6,8,9,10,13,
Right("b") = 3, 7, 11, 12, 14.
接下来, Right(“aa”) = 2, 5, 6, 9, 10, Right(“ab”) = 3, 7, 11, 14, Right("bb") = 12, Right("ba") = 4,8,13
Right(“aaa”) = 6, 10 Right(“aab”) = 3, 7, 11 Right("aba") = 4, 8 Right("baa") = 5, 9 Right("bba") = 13 Right("bab") = 14 Right("abb") = 12
Right("aaab") = 7, 11 Right(“aaba”) = 4, 8 Right("abaa") = 5, 9 Right("baaa") = 6, 10 Right("aabb") = 12 Right("abba") = 13 Right("bbab") = 14
再向下由于人类智慧太不好搞了,就不画了。但是,根据现有的Right集合,或许我们可以发现些什么。给出一张图来直观地展示:

我们用小学生看图找特点的思维来一点点看这张图。用罗列的方法来找特点。(嘿嘿,其实 是我的思路比较乱)
1、首先要记住一件事:树的深度不等于根到这个节点组成的字符串长度。很明显的,有几个单个的叶子结点很好的说明了这个特点。
2、到某个节点组成的字符串的长度越长,其Right集合里面的元素就越少,也就是说,其在串中的可以匹配的位置就越少。
3、一个节点的Right集合大小等于以其为根的子树的叶子结点的个数。(这个好像不太明显,因为 我没有把整个树画完QAQ……不过大家可以自行向下扩展)
4、叶子结点除外,根到一个结点组成的字符串的长度等于其父亲的长度加 + 1,但是我们要清楚的知道,父亲的长度是指其Right集合所表示的所有字符串的最长长度。举个例子来说,对于这样一个串,“AABAABAAB“,Rigth("AB") = {3,6,9} Right(”ABB“) = {3,6,9},他们的Right集合是相同的。所以,用这个也可以来解释 第1 点。树的深度不等于根到这个节点组成 的字符串的长度。
5、一个非叶结点至少有两个儿子。
6、两个不同的Right集合,其关系要么是一个是另一个的真子集,要么两个完全不相交。在树上这个是很直观的吧。
我们再来理解一下pre指针的作用。
某个状态的pre指针指向的结点是当前结点(cur)Right集合所表示的字符串的最长公共后缀(不是其本身)
好,下面我们来具体说明一下构建自动机的过程:
在这个过程中我们需要两个指针,last表示上一次插入的结点位置,cur表示当前的结点位置。
1、首先是建立一个空结点,表示空串的状态,其len = 0, pre = null;这个是很好理解的吧,在Parent树中,这个结点的角色是树根。
2、然后假设我们已经构建了前i-1个字符的后缀自动机,现在我们考虑加入第i个字符 x。首先先从last 向 cur连接一条边为x的出边,然后cur.len = last.len + 1,这些都是很显然的吧。
3、好,现在我们需要确定cur的pre指针的指向。我们现在在Parent树上沿着last的pre指针向上跳。
如果不小心跳到了NULL,说明当前自动机中没有x这个字符,那么很好处理,直接把cur的pre指针指向root,注意是root,而不是NULL。
如果没有跳到NULL,那么,如果last->pre没有x这样一条出边。那么这情况好说,直接从last->pre向cur连一条为x的出边。为什么要这么连?我们考虑,我们已知last->pre指向的是当前状态Right集合所表示的所有字符串的最长公共后缀,我们在当前状态的字符串后加上了一个新的字符x,那就相当于在所有的后缀后面都加了一个x,但是为了保证不重复不遗漏的在后缀上加上这个字符x,所以我们要选择最长的那个来添加,这样自然就用到了这个pre指针, 跳到最长后缀的地方。
如果last->pre有这样一条出边。那么我们就要分两种情况来讨论:
我们用p来代表last->pre,用q来代表last->pre的x出边。
情况1. 如果q.len == p.len + 1,那么就把cur的pre设为q。
情况2. 如果q.len > p.len + 1,那么就要把拷贝一个新的结点。
情况一情况二的解释如下:(来自王梦迪(NOI2015金牌得主)的博客)
第二种情况——当我们进入一个已存在的转移(p,q)时。这意味着我们试图向字符串中添加字符x+c(其中x是字符串s的某一后缀,长度为len(p)),且该字符串先前已经被加入了自动机(即,字符串x+c已经作为子串包含在字符串s中)。因为我们假设字符串s的自动机已被正确构建,我们并不应该添加新的转移。然而,cur的后缀链接指向哪里有一定复杂性。我们需要将后缀链接指向一个长度恰好和x+c相等的状态,即,该状态的len值必须等于len(p)+1.但这样一种情况可能并不存在:在此情况下我们必须添加一个“分割的”状态。
因此,一种可能的情形是,转移(p,q)变得连续,即,len(q)=len(p)+1.在这种情况下,事情变得简单,不必再进行任何分割,我们只需要将cur的后缀链接指向q。
另一种更复杂的情况——当转移不连续时,即len(q)>len(p)+1.这意味着状态q不仅仅匹配对我们必须的,长度len(p)+1的子串w+c,它还匹配一个更长的子串。我们不得不新建一个“分割的”状态q:将子串分割成两段,第一段将恰在长度len(p)+1处结束
hihocoder #1465 : 后缀自动机五·重复旋律8
时间限制:10000ms
单点时限:1000ms
内存限制:256MB
描述
小Hi平时的一大兴趣爱好就是演奏钢琴。我们知道一段音乐旋律可以被表示为一段数构成的数列。
小Hi发现旋律可以循环,每次把一段旋律里面最前面一个音换到最后面就成为了原旋律的“循环相似旋律”,还可以对“循环相似旋律”进行相同的变换能继续得到原串的“循环相似旋律”。
小Hi对此产生了浓厚的兴趣,他有若干段旋律,和一部音乐作品。对于每一段旋律,他想知道有多少在音乐作品中的子串(重复便多次计)和该旋律是“循环相似旋律”。
解题方法提示
输入
第一行,一个由小写字母构成的字符串S,表示一部音乐作品。字符串S长度不超过100000。
第二行,一个整数N,表示有N段旋律。接下来N行,每行包含一个由小写字母构成的字符串str,表示一段旋律。所有旋律的长度和不超过 100000。
输出
输出共N行,每行一个整数,表示答案。
样例输入
abac
3
a
ab
ca
样例输出
2
2
1
思路: 这个题目,由于要考虑循环相似,所以我们对所有的模式串进行加倍处理,即添加相同的串到原串的最后。
先对主串建立sam,然后模式串去主串上匹配,匹配的过程类似于spoj里面lcs和lcs2的做法,如果有这个字符的儿子,就沿着这个边走下去,否则就跳转到他的父亲,直到某个点拥有当前这个字符作为儿子,然后我们就走到这个儿子上去。
每次统计当前匹配的一个最大长度,如果这个最大长度超过或者等原来模式串的长度,那么我们就不断向上沿着fa找到第一个a[i].len>=模式串长度,这样保证模式串一定在这个节点所代表的集合里面出现,然后答案累积上这个sz。
注意,由于会找到相同的节点,而每个节点在一个模式串匹配中也只能被用1次,所以我们需要标记已经用过的节点,用vis数组标记。
1 #include<bits/stdc++.h> 2 using namespace std; 3 int const N=100000+3; 4 struct node 5 int len,fa,ch[26]; 6 a[N<<2]; 7 int n,tot,ls,sz[N<<1],num[N],sa[N<<1],vis[N<<1]; 8 vector<int> d; 9 char s[N]; 10 void add(int c,int id) 11 int p=ls; 12 int np=ls=++tot; 13 a[np].len=a[p].len+1; 14 sz[np]=1; 15 for(;p&&!a[p].ch[c];p=a[p].fa) a[p].ch[c]=np; 16 if(!p) a[np].fa=1; 17 else 18 int q=a[p].ch[c]; 19 if(a[q].len==a[p].len+1) a[np].fa=q; 20 else 21 int nq=++tot;a[nq]=a[q]; 22 a[nq].len=a[p].len+1; 23 a[q].fa=a[np].fa=nq; 24 for(;p&& a[p].ch[c]==q;p=a[p].fa) 25 a[p].ch[c]=nq; 26 27 28 29 void solve(char *s) 30 int len=strlen(s),ans=0,p=1,tmp=0; 31 d.clear(); 32 for(int i=0;i<2*len;i++) 33 int c=s[i%len]-‘a‘; 34 if(a[p].ch[c]) tmp++,p=a[p].ch[c]; 35 else 36 while (p && !a[p].ch[c]) p=a[p].fa; 37 if(!p) tmp=0,p=1; 38 else 39 tmp=a[p].len+1; 40 p=a[p].ch[c]; 41 42 43 if(tmp>=len) 44 int x=p,t=a[p].fa; 45 while (t && a[t].len>=len) 46 x=t,t=a[t].fa; 47 if(!vis[x]) ans+=sz[x],d.push_back(x); 48 vis[x]=1; 49 50 51 printf("%d\n",ans); 52 for(int i=0;i<d.size();i++) vis[d[i]]=0; 53 54 int main() 55 tot=ls=1; 56 scanf("%s",s); 57 int len=strlen(s); 58 for(int i=0;s[i];i++) 59 add(s[i]-‘a‘,i+1); 60 for(int i=1;i<=tot;i++) num[a[i].len]++; 61 for(int i=1;i<=len;i++) num[i]+=num[i-1]; 62 for(int i=1;i<=tot;i++) sa[num[a[i].len]--]=i; 63 for(int i=tot;i>=1;i--) 64 int x=sa[i]; 65 int f=a[x].fa; 66 sz[f]+=sz[x]; 67 68 scanf("%d",&n); 69 while (n--) 70 scanf("%s",s); 71 solve(s); 72 73 return 0; 74
以上是关于后缀自动机如何限制串长的主要内容,如果未能解决你的问题,请参考以下文章
hihocoder #1465 : 后缀自动机五·重复旋律8