后缀自动机超详细

后缀自动机

1.关于 e n d p o s endpos endpos

理解含义

假设字符串s是字符串S的一个子串,则 e n d p o s ( s ) endpos(s) endpos(s)表示s在S中的所有结束位置,如在字符串 a b c a b c a b abcabcab abcabcab中, e n d p o s ( a b ) = 2 , 5 , 8 endpos(ab)={2,5,8} endpos(ab)=258

如果 e n d p o s ( s 1 ) = e n d p o s ( s 2 ) endpos(s_1)=endpos(s_2) endpos(s1)=endpos(s2),则称s与s在同一等价类中。因此可以根据 e n d p o s ( s ) endpos(s) endpos(s)的值将字符串S的所有子串进行划分。

理解定理

引理 1: 字符串S的两个非空子串 s 1 s1 s1 s 2 s2 s2 e n d p o s endpos endpos相同(并且 s 1 s1 s1的长度小于等于 s 2 s2 s2的长度),当且仅当字符串 s 1 s1 s1在S中的每次出现,都是以 s 2 s2 s2后缀的形式存在的。

证明:这个我们要根据 e n d p o s endpos endpos的定义来,以 a b c a b c a b c abcabcabc abcabcabc举例, e n d p o s ( a b c ) = e n d p o s ( b c ) = 3 , 6 , 9 endpos(abc)=endpos(bc)=3,6,9 endpos(abc)=endpos(bc)=3,6,9。这里 b c = s 1 , a b c = s 2 bc=s_1,abc=s_2 bc=s1abc=s2 b c bc bc每次在字符串中出现都是以 a b c abc abc的后缀的形式。

引理 2: 考虑两个非空子串 s 1 s1 s1 s 2 s2 s2 e n d p o s endpos endpos相同(并且 s 1 s1 s1的长度小于等于 s 2 s2 s2的长度)。那么要么 e n d p o s ( s 1 ) ∩ e n d p o s ( s 2 ) = ∅ endpos(s_1)\cap{endpos(s_2)}=\emptyset endpos(s1)endpos(s2)=,要么 e n d p o s ( s 2 ) ⊂ e n d p o s ( s 1 ) endpos(s_2)\subset{endpos(s_1)} endpos(s2)endpos(s1),取决于 s 1 s1 s1是否为 s 2 s2 s2的一个后缀。

如果 s 1 s1 s1 s 2 s2 s2的一个后缀,则 e n d p o s ( s 2 ) ⊂ e n d p o s ( s 1 ) endpos(s_2)\subset{endpos(s_1)} endpos(s2)endpos(s1),否则 e n d p o s ( s 1 ) ∩ e n d p o s ( s 2 ) = ∅ endpos(s_1)\cap{endpos(s_2)}=\emptyset endpos(s1)endpos(s2)=

证明:如果 s 1 s1 s1 s 2 s2 s2的一个后缀,则 s 2 s2 s2出现的地方, s 1 s1 s1一定出现,反之不然,所以 s 2 s2 s2出现的地方一定被包含在 s 1 s1 s1出现的地方里。如果 s 1 s1 s1不是 s 2 s2 s2的一个后缀,那么 s 2 s2 s2出现的地方一定不是 s 1 s1 s1出现的地方。那你会问,如果 s 2 s2 s2里面包含了 s 1 s1 s1呢?比如 a b c a b c a b c abcabcabc abcabcabc中, a b c abc abc b b b,但是 e n d p o s ( a b c ) = 3 , 6 , 9 endpos(abc)=3,6,9 endpos(abc)=3,6,9,而 e n d p o s ( b ) = 2 , 5 , 8 endpos(b)=2,5,8 endpos(b)=2,5,8,你看,两者必然不一样。为什么我们关心的是后缀?因为 e n d p o s endpos endpos记录的是字符串的结束位置。

引理 3: 考虑字符串的 e n d p o s endpos endpos等价类,对于同一等价类的任一两子串,较短者为较长者的后缀,设最长的字符串为y,最短的字符串为x,则该等价类中的子串长度恰好覆盖整个区间 [ l e n ( x ) , l e n ( y ) ] [len(x),len(y)] [len(x),len(y)]

证明:直接用例子吧, a b c d a b c d a b c d abcdabcdabcd abcdabcdabcd里的 e n d p o s = 4 , 8 , 12 endpos=4,8,12 endpos=4,8,12的子串有 a b c d , b c d , c d , d abcd,bcd,cd,d abcdbcdcdd,最长的子串为 a b c d abcd abcd,最短的子串为 d d d,可以看见除了最短的子串 d d d,其余子串皆是最长子串的后缀,那么这些子串直接的长度一定是连续的4->3->2->1。那么其实这个等价类涵盖了 a b c d abcd abcd的所有后缀子串,但是一定会这样吗?一个字符串的后缀子串一定和它在同一个等价类里?再换一个例子 a b c d a b c d a b c d d abcdabcdabcdd abcdabcdabcdd里的 e n d p o s = 4 , 8 , 12 endpos=4,8,12 endpos=4,8,12的子串有 a b c d , b c d , c d abcd,bcd,cd abcdbcdcd,可以看到此时 d d d就不在这个等价类里,因为它在原串里不仅仅以 a b c d abcd abcd的后缀出现,也以其它方式出现,所以它的 e n d p o s endpos endpos应该比 a b c d abcd abcd多,也就是引理2。 e n d p o s ( d ) = 4 , 8 , 12 , 13 endpos(d)=4,8,12,13 endpos(d)=4,8,12,13

2.后缀自动机里的 e n d p o s endpos endpos

接下来,我要证明根据 e n d p o s endpos endpos划分的等价类可以形成一颗树。一个节点的父节点是谁呢?回顾一下刚刚举的例子, e n d p o s ( a b c d ) ⊂ e n d p o s ( d ) endpos(abcd)\subset{endpos(d)} endpos(abcd)endpos(d),那么其实 a b c d abcd abcd d d d的儿子节点,因为它的出现位置包含在 d d d中,那么同样的 e n d p o s ( a b c d ) endpos(abcd) endpos(abcd)的其它等价类也是,我们把在同一等价类中的子串放在一个节点里,那么节点2包含子串 a b c d , b c d , c d abcd,bcd,cd abcdbcdcd,它的父节点是节点1,节点1包含子串 d d d

现在给一个字符串 a a b a b a aababa aababa,按照 e n d p o s endpos endpos划分的等价类构成一颗树,如下图所示,可以先自己试着画一画哦。

可以看见,其实 e n d p o s endpos endpos的作用就是对字符串的子串进行压缩,比如8号节点,我通过1个节点存了3个节点的信息。那么除了树必须要记录的父节点信息,我还需要记录什么信息呢?还需要记录每个节点的最长串的长度,那么最为什么不用记录最短串的长度呢?因为我们可以直接用最长串的长度表示。

fa[x]:存节点x的父节点

len[x]:存节点x的最长串的长度

节点x最短串的长度= l e n [ x ] − l e n [ f a [ x ] ] + 1 len[x]-len[fa[x]]+1 len[x]len[fa[x]]+1,如 l e n [ 8 ] = 6 , f a [ 8 ] = 9 , l e n [ 9 ] = 3 , l e n [ 8 ] − l e n [ 9 ] + 1 = 6 − 3 + 1 = 4 len[8]=6,fa[8]=9,len[9]=3,len[8]-len[9]+1=6-3+1=4 len[8]=6,fa[8]=9,len[9]=3,len[8]len[9]+1=63+1=4,刚好是节点8的最短串的长度

节点的子串数量= l e n [ x ] − l e n [ f a [ x ] ] len[x]-len[fa[x]] len[x]len[fa[x]],如 l e n [ 6 ] − l e n [ 7 ] = 3 , l e n [ 7 ] − l e n [ 1 ] = 2 len[6]-len[7]=3,len[7]-len[1]=2 len[6]len[7]=3len[7]len[1]=2

我们是不是还差一点信息呢?一个子串可能不仅仅在原串中出现一次,是的,我们还需要记录每个子串的出现次数。在这之前,我们需要达成一个共识,就是属于同一个等价类的节点出现次数是相同的。这个其实也是显而易见的,因为他们的出现位置相同必然出现次数也相同。

我们还需要再达成一个共识,这个比较重要,我们另起一段,那就是一个节点的出现次数是它单独出现的次数+它作为后缀在其它字符串里出现的次数。什么时候它会单独出现而不是作为其它字符的后缀出现?先自己想哈,它不是其它子串的后缀,那么它必然是以原字符串前缀的形式出现。如 a b c a b c a b c abcabcabc abcabcabc,这里面a本身自己出现的次数是1,作为后缀在其它字符串里出现的次数是2, a b c a abca abca本身自己出现的次数是1。

再来看这棵树,节点对于字符串 a a b a b a aababa aababa,我们用数组 c n t cnt cnt存储遍历字符串的过程中字符串的前缀子串出现的次数,其实也就是1。那么我们来看, c n t [ 2 ] = 1 , c n t [ 6 ] = 1 , c n t [ 9 ] = 0 cnt[2]=1,cnt[6]=1,cnt[9]=0 cnt[2]=1,cnt[6]=1,cnt[9]=0,为什么 c n t [ 9 ] = 0 cnt[9]=0 cnt[9]=0?你可以看见节点9是 a b a aba aba b a ba ba,他们都不是 a a b a b a aababa aababa的前缀,而节点2的字符串是 a a aa aa,节点6的字符串 a a b a b aabab aabab都是前缀。但是你还会问,那节点6里面还存了 a b a b abab abab,他不是前缀呀?因为i前缀 a a b a b aabab aabab里面已经包含了 a b a b abab abab了呀。而 a a b a b aabab aabab又不是 a b a b abab abab的父节点,如果不在这里给他记录,那么这一次的出现就丢失了,或者另一种解释,同一个等价类里的子串出现次数是相同的,所以我可以用一个 c n t [ i ] cnt[i] cnt[i]数组表示该节点里所有子串的出现次数。现在 c n t cnt cnt的值只是初始建树的过程中赋予的,那么后面我们再求才能知道到底出现了几次,怎么求?累加上子节点的出现次数!因为子节点的字符串是包含当前节点的字符串的。

c n t [ 2 ] = c n t [ 2 ] + c n t [ 3 ] + c n t [ 9 ] = c n t [ 2 ] + c n t [ 3 ] + c n t [ 5 ] + c n t [ 8 ] = 1 + 1 + 2 = 1 + 1 + 1 + 1 = 4 cnt[2]=cnt[2]+cnt[3]+cnt[9]=cnt[2]+cnt[3]+cnt[5]+cnt[8]=1+1+2=1+1+1+1=4 cnt[2]=cnt[2]+cnt[3]+cnt[9]=cnt[2]+cnt[3]+cnt[5]+cnt[8]=1+1+2=1+1+1+1=4

可以用递归去求,代码如下,代码里的size和 c n t cnt cnt数组的含义一样

		private void dfs(int u) {// TODO Auto-generated method stub
//			sam.get(u).size = 1;if(tree.get(u) != null) {for(int e:tree.get(u)) {dfs(e);sam.get(u).size += sam.get(e).size;}}}

分析到这里,这颗树还差最后一个点了,那就是图里的灰色节点是什么情况呢?这里我们就要真正讨论后缀自动机的构建了,关键是构建这棵树,这棵树上的边在后缀自动机里称为链接边。

后缀自动机存两类边,链接边和转移边。链接边就是来建树的,沿着转移边走可以形成一个子串。

3.后缀自动机代码

话痨版证明

我们来模拟一下自动机形成的过程吧。

private void extend(int x) {//要往后缀自动机里插入的字符// TODO Auto-generated method stubsam.add(new node());sam.add(new node());//java的原因,因为下标从1开始,所以预存两个节点分别表示节点0和节点1int p = las;//记录上一个节点int np = ++tot;//np表示要插入节点的编号las = np;sam.add(new node());//新建一个节点sam.get(np).len = sam.get(p).len+1;//p沿链接边回跳,从旧点向新点建转移边 ch[p][x]for(;p!=0&&sam.get(p).map.get(x)==null;p=sam.get(p).par) sam.get(p).map.put(x, np);if(p==0) {//插入的点是一个新点,回跳边直接连到根节点上sam.get(np).par=rt;}else {int q = sam.get(p).map.get(x);//p沿x可以到达的点if(sam.get(q).len == sam.get(p).len+1) {//弱q和p相邻sam.get(np).par = q;//是合法点,回跳边连接到q上}else {int nq = ++tot;//不是合法点,要裂点新建sam.add(new node());sam.get(nq).len = sam.get(p).len+1;//新点的长度等于旧点的长度
//					HashMap<Integer, Integer> tempmap = new HashMap<Integer, Integer>();
//					tempmap.putAll(tempmap);sam.get(nq).map.putAll(sam.get(q).map);//这里要深拷贝,把旧点指出去的边复制给新节点
//					sam.get(nq).map = sam.get(q).map;//新点连出去的边等于旧点连出去的边sam.get(nq).par = sam.get(q).par;//新点的回跳边等于旧点的回跳边sam.get(q).par = nq;//旧点的回跳边等于新点sam.get(np).par = nq;//插入点的回跳边等于新点for (;p!=0&& sam.get(p).map.get(x) == q; p = sam.get(p).par) sam.get(p).map.put(x, nq);}//连到旧点的边指向新点}
  1. 初始时,只有一个节点1(用p表示旧节点,也就是存前一个字符的节点),这时我插入一个字符a,那么用节点2(用 n p np np表示新节点)表示该字符的状态,节点2自然要连到节点1的后面,并且节点2记录的字符串的长度要比节点1记录的字符串长度大1(sam.get(np).len = sam.get(p).len+1;),因为在节点1的基础上多了节点2所代表的这个字符嘛。

  2. 当新插入一个节点时,我要建转移边。建转移边的代码为for(;p!=0&&sam.get(p).map.get(x)==null;p=sam.get(p).par) sam.get(p).map.put(x, np);我有两个问题,

    问题1:转移边的作用是什么?

    问题2:为什么是沿着转移边回跳?

​ 最好是自己先想哈!我们在讲树的时候,每个节点表示了多个子串,其实后缀自动机里的节点和 e n d p o s endpos endpos树里的节点是同一个,那么想一想字典树,从根节点沿着边走,走过的所有边表示的字符串起来形成一个字符串,这里也是同理。

如图,这里的节点6表示了哪些字符串呢? a a b a b a , b a b , a b a b aababa,bab,abab aababa,bab,abab,就是从节点1开始沿着转移边走走到节点6形成的字符串,那么它们其实也是以节点字符b结尾的子串。这里的思想有点类似AC自动机了。其实我要求以字符b结尾的子串,首先字符b节点(节点6)的前一个节点(节点5),它一定有条转移边指向b。然后我需要找到节点5的所有后缀,让他们也有一个转移边指向b。那么其实就是不断沿着节点的链接边回跳。因为链接边的两端是父节点和子节点的关系,同时节点的链接边指向的就是该节点的后缀,这是我们在讲树的时候讲到的。回跳到什么情况我就停止呢?想一想,停止是因为我把以b结尾的子串都知道了,如果我们的字符b是第一次出现,那么必然要一直跳到节点1,如果字符b之前出现过呢?我只需要跳到他出现过的地方就可以停了。

​ 好,回到我们刚刚的构建过程,下面我们要插入字符a,节点2向节点3连转移边,然后把p指向节点2的链接边指向的节点1,此时p指向节点1,但是节点1已经有沿a走的转移边了,那我们就不需要给它建变了,我们的转移边停止建边,退出while循环,同时也说明字符a之前出现过。节点3的链接边连向节点1沿a指向的节点,也就是节点2。我们在抽象版里解释这里为什么可以停止建转移边。

下面我们要插入字符b,节点3向节点4连转移边,然后把p指向节点3的链接边指向的节点2,此时p指向节点2,节点2没有沿b走的转移边了,那么我们就给他建沿b走的转移边且指向节点4;然后把p指向节点2的链接边指向的节点1,此时p指向节点1,节点1没有沿b走的转移边了,那么我们就给他建沿b走的转移边且指向节点4,节点1的链接边指向节点0,此时退出while循环,也说明了b字符之前没有出现过,那么节点4的链接边指向节点1。之前没有出现过,那也就不存在后缀,所以指向节点1没有问题。

下面我们要插入字符a,节点4向节点5连转移边,然后把p指向节点4的链接边指向的节点1,此时p指向节点1,节点1有沿a走的转移边了,停止建转移边,并且节点5的链接边指向节点1有沿a走的转移边指向的节点,即节点2。

下面我们要插入字符b,节点5向节点6连转移边,然后把p指向节点5的链接边指向的节点2,此时p指向节点2,节点2有沿b走的转移边了,停止建转移边,并且节点6的链接边指向节点2有沿b走的转移边指向的节点,即节点4,也就是红色的线段。我们来看一下这样建边有什么不妥之处。节点6表示的字符串有 a a b a b , a b a b , b a b aabab,abab,bab aabab,abab,bab,节点4表示的字符串有 a a b , a b , b aab,ab,b aab,ab,b,在加入字符b之前, a a b , a b , b aab,ab,b aab,ab,b的出现位置都是3,但是加入了字符b后, b b b a b ab ab的出现位置就变成了3,5。所以此时 b , a b b,ab b,ab a a b aab aab分开。那我们要新建一个节点7,这个节点7要存什么呢?这个节点7的边的关系又如何去表示呢?节点7存的是 b , a b b,ab b,ab,为啥?

之前分析的时候,正常的点应该存了字符串的前缀,而这里 a a b aab aab是字符串的前缀,应该让他留在节点4里面。接下来看裂点后链接边怎么修改,链接边指向的是当前节点的后缀,那么此时节点4应该指向节点7,因为他们的后缀是 a b ab ab b b b,那么节点4链接边原本指向的节点由节点7去指。不要忘了裂点的目的,让节点6链接边指向节点7。转移边又该如何去变呢?原本从节点4指出去的转移边也要从节点7指出去,原本沿b指向节点4的转移边现在改成指向节点7。

实例就讲到这里,还差最后一个字符,大家可以自己试试怎么插,那么完整的后缀自动机如下图,

抽象版证明

private void extend(int x) {// TODO Auto-generated method stubsam.add(new node());sam.add(new node());int p = las;//记录上一个节点int np = ++tot;//np表示要插入节点的编号las = np;sam.add(new node());sam.get(np).len = sam.get(p).len+1;//p沿链接边回跳,从旧点向新点建转移边 ch[p][x]for(;p!=0&&sam.get(p).map.get(x)==null;p=sam.get(p).par) sam.get(p).map.put(x, np);if(p==0) {//插入的点是一个新点,回跳边直接连到根节点上sam.get(np).par=rt;}else {int q = sam.get(p).map.get(x);//p沿x可以到达的点if(sam.get(q).len == sam.get(p).len+1) {//弱q和p相邻sam.get(np).par = q;//是合法点,回跳边连接到q上}else {int nq = ++tot;//不是合法点,要裂点新建sam.add(new node());sam.get(nq).len = sam.get(p).len+1;//新点的长度等于旧点的长度
//					HashMap<Integer, Integer> tempmap = new HashMap<Integer, Integer>();
//					tempmap.putAll(tempmap);sam.get(nq).map.putAll(sam.get(q).map);//这里要深拷贝,把旧点指出去的边复制给新节点
//					sam.get(nq).map = sam.get(q).map;//新点连出去的边等于旧点连出去的边sam.get(nq).par = sam.get(q).par;//新点的回跳边等于旧点的回跳边sam.get(q).par = nq;//旧点的回跳边等于新点sam.get(np).par = nq;//插入点的回跳边等于新点for (;p!=0&& sam.get(p).map.get(x) == q; p = sam.get(p).par) sam.get(p).map.put(x, nq);}//连到旧点的边指向新点}}

sam.get(np).len = sam.get(p).len+1;假设新插入的节点是c,新插入节点所表示字符串的长度是前一个节点的长度+1,因为比前一个节点多了一个字符c。

for(;p!=0&&sam.get(p).map.get(x)==null;p=sam.get(p).par) sam.get(p).map.put(x, np);这个代码的含义其实就是在前一个节点的所有后缀上加一个字符c,当然,如果本来就有字符c,也就是本来就有沿c走的转移边我就不加了。如前一个字符串是 a b a d abad abad,我要加一个新字符c,那么实际会产生的新字符串应该是 a b a d + c , b a d + c , a d + c , d + c abad+c,bad+c,ad+c,d+c abad+c,bad+c,ad+c,d+c,也就是在 a b a d abad abad的所有后缀里面添加一个字符c。如果没有走到底是什么情况呢?比如 a a b a aaba aaba里面添加一个字符b,产生的新字符串是 a a b a + b , a b a + b , b a + b , a + b aaba+b,aba+b,ba+b,a+b aaba+b,aba+b,ba+b,a+b,但是实际产生的新字符串应该是 a a b a + b , a b a + b , b a + b aaba+b,aba+b,ba+b aaba+b,aba+b,ba+b,ab在插入b之前就已经存在了,那么在建转移边的过程中,因为表示字符串a的节点此时已经有沿b的转移边了,所以为了不重复,就不再往下走了,当然当前这个节点也不再建边。

if(p==0)如果p=0,说明当前字符是新插入的字符,自然没有后缀,直接连在根节点上。

if(p==0) {//插入的点是一个新点,回跳边直接连到根节点上sam.get(np).par=rt;}

如果p停在了半路,说明字符是旧字符,如果p沿字符x走到的节点与p长度相差1,则链接边直接连到q。链接边指向后缀,该后缀的出现位置比当前字符串多。p沿字符x走到的节点所代表的字符串一定是当前字符串的后缀,且它在当前字符串出现之前就已经存在了,必然其出现位置比当前字符串多,且包含当前字符串出现的位置。

int q = sam.get(p).map.get(x);//p沿x可以到达的点
if(sam.get(q).len == sam.get(p).len+1) {//弱q和p相邻sam.get(np).par = q;//是合法点,回跳边连接到q上
}

但是为什么要判断sam.get(q).len == sam.get(p).len+1呢?我们之前在建树时提到节点内子串的长度是连续的,父子节点之间也是连续的,如果出现了不连续的情况,说明当前节点记录的子串因为后续字符的加入而不再属于同一等价类,也就是不合法。为什么不合法,因为受插入字符串的影响,某些字符串的出现次数增多。我把出现次数增多的字符串分出来,新建一个节点存它,新节点的长度此时一定是合法的,据此更新一下它的长度

int nq = ++tot;//不是合法点,要裂点新建
sam.add(new node());
sam.get(nq).len = sam.get(p).len+1;//新点的长度等于旧点的长度

出现次数多的子串一定是出现次数少的子串的后缀,所以原节点和新插入节点的链接边都指向它,原节点的链接边给它,

sam.get(nq).par = sam.get(q).par;//新点的回跳边等于旧点的回跳边
sam.get(q).par = nq;//旧点的回跳边等于新点
sam.get(np).par = nq;//插入点的回跳边等于新点

原本从旧点连出去的转移边也要从新点转移出去

sam.get(nq).map.putAll(sam.get(q).map);//这里要深拷贝,把旧点指出去的边复制给新节点

在一开始我们满足sam.get(p).map.get(x)!=null就暂停了,因为再向后遍历必然也存在沿x走的转移边,得到的后缀串也是已经出现过的,也说明从这里向后的字符串的出现位置比原来多,所以他们应该被放在新建的节点中,所以原本连向旧点的转移边改成连向新点。

for (;p!=0&& sam.get(p).map.get(x) == q; p = sam.get(p).par) sam.get(p).map.put(x, nq);//连到旧点的边指向新点

至此后缀自动机的模板代码就解读完毕了,不仅仅要知道应该这样写,也要知道为什么这样写,最顶端的思考应该是知道如何想到的这样写,但是我不会,就不写了。

4.后缀自动机完整代码


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;public class ok后缀自动机2 {static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));static String[] string; static class SAM{class node{int par,len;Map<Integer, Integer> map = new HashMap<Integer, Integer>();}List<node> sam = new ArrayList<node>();int las,rt,tot;		SAM(int n) throws IOException{las = 1;rt = 1;tot = 1;long ans = 0;string = br.readLine().split(" ");for(int i = 0;i < n;i++) {int x = Integer.parseInt(string[i]);extend(x);ans += sam.get(las).len - sam.get(sam.get(las).par).len;//回顾后缀自动机的规律System.out.println(ans + " " +(sam.get(las).len - sam.get(sam.get(las).par).len)+" "+las+" "+sam.get(las).par);}System.out.println(4+" "+sam.get(4).par);			}private void extend(int x) {sam.add(new node());sam.add(new node());int p = las;//记录上一个节点int np = ++tot;//np表示要插入节点的编号las = np;sam.add(new node());sam.get(np).len = sam.get(p).len+1;//p沿链接边回跳,从旧点向新点建转移边 ch[p][x]for(;p!=0&&sam.get(p).map.get(x)==null;p=sam.get(p).par) sam.get(p).map.put(x, np);if(p==0) {//插入的点是一个新点,回跳边直接连到根节点上sam.get(np).par=rt;}else {int q = sam.get(p).map.get(x);//p沿x可以到达的点if(sam.get(q).len == sam.get(p).len+1) {//弱q和p相邻sam.get(np).par = q;//是合法点,回跳边连接到q上}else {int nq = ++tot;//不是合法点,要裂点新建sam.add(new node());sam.get(nq).len = sam.get(p).len+1;//新点的长度等于旧点的长度sam.get(nq).map.putAll(sam.get(q).map);//这里要深拷贝,把旧点指出去的边复制给新节点sam.get(nq).par = sam.get(q).par;//新点的回跳边等于旧点的回跳边sam.get(q).par = nq;//旧点的回跳边等于新点sam.get(np).par = nq;//插入点的回跳边等于新点for (;p!=0&& sam.get(p).map.get(x) == q; p = sam.get(p).par) sam.get(p).map.put(x, nq);}//连到旧点的边指向新点}}}
public static void main(String[] args) throws IOException {solve();
}
private static void solve() throws IOException {// TODO Auto-generated method stubstring = br.readLine().split(" ");int n = Integer.parseInt(string[0]);SAM A = new SAM(n);
}
}

SAM构造函数是根据题目来的,可以不做思考,关键关注extend函数。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/229969.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

文件操作安全之-目录穿越流量告警运营分析篇

本文从目录穿越的定义,目录穿越的多种编码流量数据包示例,目录穿越的suricata规则,目录穿越的告警分析研判,目录穿越的处置建议等几个方面阐述如何通过IDS/NDR,态势感知等流量平台的目录穿越类型的告警的线索,开展日常安全运营工作,从而挖掘有意义的安全事件。 目录穿越…

go 源码解读 sync.RWMutex

sync.RWMutex 简介源码结构RLockRUnlockUnlockgo 运行时方法 简介 简述sync包中读写锁的源码。 &#xff08;go -version 1.21&#xff09; 读写锁&#xff08;RWMutex&#xff09;是一种并发控制机制&#xff0c;用于在多个 goroutine 之间对共享资源进行读写操作。它提供了…

flutter学习-day21-使用permission_handler进行系统权限的申请和操作

文章目录 1. 介绍2. 环境准备2-1. Android2-2. iOS 3. 使用 1. 介绍 在大多数操作系统上&#xff0c;权限不是在安装时才授予应用程序的。相反&#xff0c;开发人员必须在应用程序运行时请求用户的许可。在 flutter 开发中&#xff0c;则需要一个跨平台(iOS, Android)的 API 来…

python爬虫实现获取招聘信息

使用的python版本&#xff1a; 3.12.1 selenium版本&#xff1a;4.8.0 urllib版本&#xff1a;1.26.18 from selenium import webdriver from selenium.webdriver import ActionChains import timeimport re import xlwt import urllib.parsedef get_html(url):chrome_drive…

5大自动化测试的Python框架,看完就能涨薪5k 【实用干货】

目前&#xff0c;它在Tiobe指数中排名第三个&#xff0c;仅次于Java和C。随着该编程语言的广泛使用&#xff0c;基于Python的自动化测试框架也应运而生&#xff0c;且不断发展与丰富。 因此&#xff0c;开发与测试人员在为手头的项目选择测试框架时&#xff0c;需要考虑许多方…

嵌入式系统(二)单片机基础 | 单片机特点 内部结构 最小系统 电源 晶振 复位

上一篇文章我们介绍了嵌入式系统 嵌入式系统&#xff08;Embedded System&#xff09;是一种特定用途的计算机系统&#xff0c;它通常嵌入在更大的产品或系统中&#xff0c;用于控制、监测或执行特定的任务。这些系统通常由硬件和软件组成&#xff0c;旨在满足特定的需求&…

【Leetcode】466. 统计重复个数

文章目录 题目思路代码 题目 466. 统计重复个数 思路 题目要求找出一个最大整数 m&#xff0c;使得经过 n2 个字符串 s2 组成的字符串能够被经过 n1 个字符串 s1 组成的字符串完全包含的次数。使用动态规划来记录每个位置匹配的情况&#xff0c;并通过循环节的分析来计算最…

2_并发编程同步锁(synchronized)

并发编程带来的安全性同步锁(synchronized) 1.他的背景 当多个线程同时访问&#xff0c;公共共享资源的时候&#xff0c;这时候就会出现线程安全&#xff0c;代码如&#xff1a; public class AtomicDemo {int i0;//排他锁、互斥锁public void incr(){ //synchronizedi; …

uni-app App.vue生命周期全局样式全局存储globalData

锋哥原创的uni-app视频教程&#xff1a; 2023版uniapp从入门到上天视频教程(Java后端无废话版)&#xff0c;火爆更新中..._哔哩哔哩_bilibili2023版uniapp从入门到上天视频教程(Java后端无废话版)&#xff0c;火爆更新中...共计23条视频&#xff0c;包括&#xff1a;第1讲 uni…

ansible管理windows测试

一、环境介绍 Ansible管理主机&#xff1a; 系统: redhat7.6 Linux管理服务器需安装pywinrm插件 Windows客户端主机&#xff1a; 系统: Server2012R2 Windows机器需要安装或升级powershell4.0以上版本&#xff0c;Server2008R2默认的版本是2.0&#xff0c;因此必须升…

App.vue中引入自定义组件

components目录中定义组件&#xff1a;Person.vue 目录截图&#xff1a; Person.vue文件中内容&#xff1a; <template><div class"person"><h2>姓名&#xff1a;{{name}}</h2><h2>年龄&#xff1a;{{age}}</h2><!--定义了…

工作流入门这篇就够了!

总概 定义&#xff1a;工作流是在计算机支持下业务流程的自动或半自动化&#xff0c;其通过对流程进行描述以及按一定规则执行以完成相应工作。 应用&#xff1a;随着计算机技术的发展以及工业生产、办公自动化等领域的需求不断提升&#xff0c;面向事务审批、材料提交、业务…

【hyperledger-fabric】将智能合约部署到通道

简介 本文主要来自于B站视频教学视频&#xff0c;也主要参看了官方文档中下图这一章节。针对的是测试网络中go语言的部分&#xff0c;部署测试网络。附上B站的教学视频 B站教学视频第一小节&#xff0c;附上 官方文档 1.启动网络 # 跳转到指定的目录 cd /root/fabric/fabri…

WSL使用VsCode运行cpp文件

文章目录 缘起主要步骤参考 缘起 今天在阅读《C20设计模式-可复用的面向对象设计方法&#xff08;原书第2版&#xff09;》的时候&#xff0c;遇到代码想要运行一下&#xff0c;于是决定使用wsl下的vscode配置cpp的环境。 主要步骤 1.安装gcc和g编译器 打开命令行输入wsl&am…

栈实现前缀表达式的计算

前缀表达式计算 过程分析 中缀表达式&#xff1a;&#xff08;1 5&#xff09;*3 > 前缀表达式&#xff1a;*153 &#xff08;可参考这篇文章&#xff1a;中缀转前缀&#xff09; 第一步&#xff1a;从右至左扫描前缀表达式&#xff08;已存放在字符数组中&#xff09;&a…

摆烂式学习ssh

摆烂式学习ssh ssh工作原理ssh基本使用sshd配置文件密钥登录1.客户端2.服务器3.注意事项4.使用密钥登录测试 ssh高级使用技巧1.在非正规端口启动2.rsync 命令3.透过 ssh 通道加密原本无加密的服务4.以ssh信道配合x server 传递图形接口5.ssh配合virtualbox虚拟机使用技巧 ssh工…

【linux】ufw 的基本使用

碎碎念 所有的云平台的网络流量的进出基本上有三层&#xff0c;首先是虚拟网的流量控制&#xff0c;一般是通过子网访问控制列表来控制vpc也好子网也好的流量出入&#xff0c;其次是安全组控制一层&#xff0c;通过安全组规则控制一类/一组主机&#xff08;指EC2/ECS/VM/CE这些…

深度学习在语义分割中的进展与应用

埃弗顿戈梅德&#xff08;Everton Gomede&#xff09; 一、说明 语义分割是计算机视觉领域的一项关键任务&#xff0c;涉及将图像中的每个像素分类为预定义的类。这项任务在从自动驾驶汽车到医学成像的各种应用中都具有深远的影响。深度学习的出现显著提高了语义分割模型的功能…

LeetCode 每日一题 Day 28293031 ||三则模拟||找循环节(hard)

1185. 一周中的第几天 给你一个日期&#xff0c;请你设计一个算法来判断它是对应一周中的哪一天。 输入为三个整数&#xff1a;day、month 和 year&#xff0c;分别表示日、月、年。 您返回的结果必须是这几个值中的一个 {“Sunday”, “Monday”, “Tuesday”, “Wednesday…

MT8766安卓核心板规格参数_MTK8766核心板模块方案定制

MT8766安卓核心板&#xff1a;高性能、稳定可靠、集成度高的一体化解决方案 MT8766安卓核心板采用联发科MTK8766四核4G模块方案&#xff0c;是一款高度集成的安卓一体板。四核芯片架构&#xff0c;主频可达到2.0GHz&#xff0c;支持国内4G全网通。12nm制程工艺&#xff0c;支持…