后缀自动机超详细

news2024/9/22 21:23:54

后缀自动机

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 stub
			sam.add(new node());
			sam.add(new node());//java的原因,因为下标从1开始,所以预存两个节点分别表示节点0和节点1
			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);}//连到旧点的边指向新点
			}
  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 stub
			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;//新点的长度等于旧点的长度
//					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 stub
	string = br.readLine().split(" ");
	int n = Integer.parseInt(string[0]);
	SAM A = new SAM(n);
}
}

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

进程的基础认识

一、进程的概念 进程是指 可执行程序 内核数据结构&#xff08;内核为了管理进程而创建的数据结构&#xff09;。 二、进程的管理 进程是靠PCB(process control block [进程控制块])管理起来的&#xff0c;在linux下PCB叫 task_struct 当一个可执行程序从磁盘加载进内存&…

分割数组的最大差值 - 华为OD统一考试

分割数组的最大差值 - 华为OD统一考试 OD统一考试 分值: 100分 题解: Java / Python / C++ 题目描述 给定一个由若干整数组成的数组nums ,可以在数组内的任意位置进行分割,将该数组分割成两个非空子数组(即左数组和右数组),分别对子数组求和得到两个值.计算这两个值的差值…

lv14 字符设备驱动基础框架解析 4

一、字符设备驱动框架解析 设备的操作函数如果比喻是桩的话&#xff08;性质类似于设备操作函数的函数&#xff0c;在一些场合被称为桩函数&#xff09;&#xff0c;则&#xff1a; 驱动实现设备操作函数 ----------- 做桩 insmod调用的init函数主要作用 --------- 钉桩 rm…

SQL窗口函数大小详解

窗口大小 OVER 子句中的 frame_clause 选项用于指定一个滑动的窗口。窗口总是位于分区范围之内&#xff0c;是分区的一个子集。指定了窗口之后&#xff0c;分析函数不再基于分区进行计算&#xff0c;而是基于窗口内的数据进行计算。 指定窗口大小的语法如下&#xff1a; ROWS…

怎么将视频转换为mp4?

怎么将视频转换为mp4&#xff1f;转换视频文件为 MP4 格式是非常普遍的需求&#xff0c;因为 MP4 是广泛支持的视频格式之一&#xff0c;能够在各种设备上流畅播放。MP4 格式具有很好的兼容性&#xff0c;基本上可以在所有的播放器中播放&#xff0c;也可以在大多数剪辑软件中打…

WEB 3D技术 three.js 补间动画(tween)

本文 我们来说 补间动画 比如说 我们有一个正方体 默认在如下图位置 然后 我们希望 一秒中之后 它到达如下图位置 那么 我们知道 终点和起点的位置 从起点到终点 一共需要一秒的时间 需要程序自己去处理这个图形 0.1 0.2 直到 1秒 它都分别要达到什么位置 通过开始和结束位…

尚硅谷大数据技术-数据湖Hudi视频教程-笔记01

大数据新风口&#xff1a;Hudi数据湖&#xff08;尚硅谷&Apache Hudi联合出品&#xff09;尚硅谷数据湖Hudi视频教程 B站直达&#xff1a;https://www.bilibili.com/video/BV1ue4y1i7na百度网盘&#xff1a;https://pan.baidu.com/s/1NkPku5Pp-l0gfgoo63hR-Q?pwdyyds阿里云…

爬虫如何使用代理IP通过HTML和CSS采集数据

目录 前言 1. 了解代理IP 2. 通过HTML和CSS采集数据 3. 使用代理IP进行数据采集 3.1 获取代理IP列表 3.2 配置代理IP 3.3 发送请求和解析网页内容 总结 前言 爬虫是一种自动化工具&#xff0c;用于从互联网上获取数据。代理IP是一种用于隐藏真实IP地址并改变网络请求的…

Python电能质量扰动信号分类(四)基于CNN-BiLSTM的一维信号分类模型

往期精彩内容&#xff1a; 引言 1 数据集制作与加载 1.1 导入数据 1.2 制作数据集 2 CNN-BiLSTM分类模型和超参数选取 2.1定义CNN-BiLSTM分类模型 2.2 设置参数&#xff0c;训练模型 3 模型评估 3.1 准确率、精确率、召回率、F1 Score 3.2 十分类混淆矩阵&#xff1a…

Stata各版本安装指南

Stata下载链接 https://pan.baidu.com/s/1ECc2mPsfNOUUwOQC9hCcYg?pwd0531 1.鼠标右击【Stata18(64bit)】压缩包&#xff08;win11及以上系统需先点击“显示更多选项”&#xff09;【解压到 Stata18(64bit)】。 2.打开解压后的文件夹&#xff0c;鼠标右击【Setup】选择【以管…

修复移动硬盘显示盘符但打不开问题

问题&#xff1a; 移动硬盘显示盘符&#xff0c;但无法打开。点击属性不显示磁盘使用信息。 分析解决&#xff1a; 这是由于硬盘存在损坏导致的&#xff0c;可以通过系统自带的磁盘检查修复解决&#xff0c;而无需额外工具。 假设损坏的盘符是E&#xff0c;在命令行运行以下命令…

【日积月累】Java中 正则表达式

目录 日积月累】Java中 正则表达式 1.前言2.基本语法3.Pattern和Matcher类4.校验的表达式大全5.参考文章所属专区 日积月累 1.前言 正则表达式是一种用于匹配文本模式的语法,它通常与编程语言一起使用。在Java中,正则表达式用于匹配字符串,可以使用Pattern和Matcher类来实…

性能测评高效云盘、ESSD Entry云盘、SSD云盘、ESSD云盘、ESSD PL-X云盘及ESSD AutoPL云盘

阿里云服务器系统盘或数据盘支持多种云盘类型&#xff0c;如高效云盘、ESSD Entry云盘、SSD云盘、ESSD云盘、ESSD PL-X云盘及ESSD AutoPL云盘等&#xff0c;阿里云百科aliyunbaike.com详细介绍不同云盘说明及单盘容量、最大/最小IOPS、最大/最小吞吐量、单路随机写平均时延等性…

Vue 中的 ref 与 reactive:让你的应用更具响应性(上)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

uniapp中uview组件库丰富LoadingPage 加载页

目录 基本使用 #显示或隐藏 #文字内容 #动画模式 #动画图片 #文字颜色 #文字大小 #图标大小 2.0.32 #背景颜色 #图标颜色 API #Props 基本使用 <template><view><u-loading-page></u-loading-page></view> </template>#显示或…

Flink版本更新汇总(1.14-1.18)

0、汇总 1.14.0 1.有界流支持 Checkpoint&#xff1b; 2.批执行模式支持 DataStream 和 Table/SQL 混合应用&#xff1b; 3.新增 Hybrid Source 功能&#xff1b; 4.新增 缓冲区去膨胀 功能&#xff1b; 5.新增 细粒度资源管理 功能&#xff1b; 6.新增 DataStream 的 Pulsar …

.mallox勒索病毒数据恢复|金蝶、用友、管家婆、OA、速达、ERP等软件数据库恢复

引言&#xff1a; 随着技术的不断发展&#xff0c;网络空间也不可避免地面临着各种威胁&#xff0c;其中之一就是勒索病毒&#xff0c;而.mallox是近期引起关注的一种恶意软件。本文将介绍.mallox勒索病毒&#xff0c;以及如何有效地恢复被其加密的数据文件&#xff0c;并提供…

高德地图经纬度坐标导出工具

https://tool.xuexiareas.com/map/amap 可以导出单个点&#xff0c;也可以导出多个&#xff0c;多个点可以连成线&#xff0c;可用于前端开发时自己模拟“线“数据

修复键盘问题的十种方法,总有一种可以帮到你

坏了的键盘可不是闹着玩的。这就是为什么苹果公司向人们支付395美元,以解决其蝴蝶键盘故障的集体诉讼。但这个问题并不总是那么普遍,所以这通常意味着如果出现问题,你只能靠自己了。 重新启动电脑 你有没有试过反复打开电脑?在你尝试任何随机修复之前,一个简单的重新启动…

基于SSM的滁艺咖啡在线销售系统设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…