字符串小结(持续更新)

只是给忘记模板时的我看的

AC自动机

大概流程

首先对所有模式串建出 T r i e Trie Trie 树,并标记。

f a i l fail fail 的定义:设 i i i 节点所代表的字符串为 S S S,则 f a i l i fail_i faili 表示 S S S 的所有后缀里面,在 T r i e Trie Trie 树中出现过的最长的那个串所代表的节点。

然后通过 bfs \texttt{bfs} bfs 求出 f a i l fail fail,代码如下:

void getfail()
{
	queue<int>q;
	for(int i=0;i<26;i++)
		if(t[0].ch[i])
			q.push(t[0].ch[i]);
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		for(int i=0;i<26;i++)
		{
			if(t[u].ch[i])
			{
				t[t[u].ch[i]].fail=t[t[u].fail].ch[i];
				q.push(t[u].ch[i]);
			}
			else t[u].ch[i]=t[t[u].fail].ch[i];//tag1
		}
	}
}

其中为什么 t a g 1 tag_1 tag1 处可以将 u u u 的儿子直接指向 f a i l u fail_u failu 的儿子 v v v

首先实际的操作应该是新建一个虚拟节点 n e w new new,使 n e w new new u u u 的儿子,且 f a i l n e w = v fail_{new}=v failnew=v

又由于 n e w new new 本身是新建的节点,没有任何儿子,所以 n e w new new 的儿子全都是要靠新建虚拟节点构成。

所以 n e w new new 的子树其实和 v v v 的子树是一模一样的。

那我们不妨用同一棵子树表示他们,也就是说让 u u u 的儿子指向 v v v 而不是新建节点。

然后由于 n e w new new 树的 f a i l fail fail 全部都是指向 v v v 树的,所以合并到一起不会对 f a i l fail fail 产生影响。

那么 getfail ⁡ ( ) \operatorname{getfail}() getfail() 之后原来的 T r i e Trie Trie 树就会变成一个 DAG 了。

实际应用

一、模式串与文本串匹配上的应用

原理

首先通过递归 f a i l fail fail,就可以遍历某个串的所有在模式串中出现过的后缀。

同样,如果建立 f a i l fail fail 树( f a i l i → i fail_i\to i failii,就可以通过遍历某一个点 u u u 的子树(设 u u u 所代表的串为 s s s),遍历所有以 s s s 为后缀的串。(也就是往 s s s 的前面加字符)

其次,对于原 T r i e Trie Trie 树中的某一个节点 u u u(设其代表的串为 s s s),可以遍历统计 u u u 子树内的所有点,遍历所有以 s s s 为前缀的串。(也就是往 u u u 后面加字符)

那么综合上面两个操作,对于某个串 t t t,我们可以求出所有满足 t t t s s s 的子串的 s s s 串的信息。

时间复杂度为 O ( n ) O(n) O(n)(遍历一遍 T r i e Trie Trie 树+一遍 f a i l fail fail 树)。

所以对于解决模式串类的问题,AC 自动机的本质就是对于每一种字符串,除了记录在它后面加字符能到达的出现过的串( T r i e Trie Trie 树),还记录了在它前面加字符能到达的出现过的串( f a i l fail fail 树)。

那么对于 s s s 串的子串信息,我们可以对 s s s 的前缀跳 f a i l fail fail 链。而对于 t t t 串的扩展串信息( t t t 是某个串的子串),我们可以在 f a i l fail fail 树中遍历 t t t 树的子树,再在 T r i e Trie Trie 树中遍历 遍历到的点 的子树。

例题

1.请你分别求出每个模式串 T i T_i Ti 在文本串 S S S 中出现的次数。

可以直接按我们刚刚的做法来做(跳 S S S 前缀的 f a i l fail fail 链),但是会 T 飞。

考虑优化,把根到 S S S 路径上的节点都标记(设为 s i z e = 1 size=1 size=1),然后建立 f a i l fail fail 树( f a i l i → i fail_i \to i failii),设 s i z e i size_i sizei i i i 这个节点所代表的字符串在 S S S 中出现的次数。

那么在 f a i l fail fail 树中, i i i 的子树中的所有有效节点都能为 s i z e i size_i sizei 贡献 1 1 1。所以把每一个有效节点 s i z e size size 的初始值都设为 1 1 1 然后在 f a i l fail fail 树上从下往上统计 s i z e size size

#include<bits/stdc++.h>

#define N 200010
#define ll long long

using namespace std;

struct Trie
{
	int ch[26],fail;
	ll size;
}t[N];

int n,node,id[N];
int cnt,head[N],nxt[N],to[N];

void adde(int u,int v)
{
	to[++cnt]=v;
	nxt[cnt]=head[u];
	head[u]=cnt;
}

int insert(string s)
{
	int u=0,len=s.size();
	for(int i=0;i<len;i++)
	{
		int v=s[i]-'a';
		if(!t[u].ch[v]) t[u].ch[v]=++node;
		u=t[u].ch[v];
	}
	return u;
}

void dfsTrie(string s)
{
	int u=0,len=s.size();
	for(int i=0;i<len;i++)
	{
		int v=s[i]-'a';
		u=t[u].ch[v];//这里可能没有u->v这个转移然后回到根,但也是对的。因为这代表在Trie树中没有出现任何一个s[1...i]的后缀(注意这里的转移时geifail后的)
		t[u].size++;
	}
}

void getfail()
{
	queue<int>q;
	for(int i=0;i<26;i++)
		if(t[0].ch[i])
			q.push(t[0].ch[i]);
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		for(int i=0;i<26;i++)
		{
			if(t[u].ch[i])
			{
				t[t[u].ch[i]].fail=t[t[u].fail].ch[i];
				q.push(t[u].ch[i]);
			}
			else t[u].ch[i]=t[t[u].fail].ch[i];
		}
	}
	for(int i=1;i<=node;i++)
		adde(t[i].fail,i);
}

void dfsFail(int u)
{
	for(int i=head[u];i;i=nxt[i])
	{
		int v=to[i];
		dfsFail(v);
		t[u].size+=t[v].size;
	}
}

int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		string str;
		cin>>str;
		id[i]=insert(str);
	}
	getfail();
	string s;
	cin>>s;
	dfsTrie(s);
	dfsFail(0);
	for(int i=1;i<=n;i++)
		printf("%lld\n",t[id[i]].size);
	return 0;
}
/*
3
abc
cde
de
abcde
*/

2.https://blog.csdn.net/ez_lcw/article/details/99613063

后缀自动机(SAM)

大概流程

(以下的 “节点” 均表示后缀自动机中的节点)

(定义对于两个字符串 A , B A,B A,B 的运算 A + B A+B A+B 表示 A A A B B B 顺次拼接起来的串)

(下面请注意 S ( i ) S(i) S(i) S [ i ] S[i] S[i] 的区别,其中后者表示字符串 S S S 的第 i i i 位,而前者在下文中会有定义)

Endpos ⁡ \operatorname{Endpos} Endpos 集合

我们把 S S S 的一个子串在 S S S 中每一次出现的结束位置的集合定义为 Endpos ⁡ \operatorname{Endpos} Endpos 集合。

然后我们考虑我们要构建的后缀自动机长什么样:我们将 Endpos ⁡ \operatorname{Endpos} Endpos 集合完全相同的子串合并到同一个节点。

我们发现,对于越短的子串,其 Endpos ⁡ \operatorname{Endpos} Endpos 集合往往越大。更具体地,如果 t t t 是某一个子串 T T T 的后缀,则 ∣ Endpos ⁡ ( t ) ∣ ≥ ∣ Endpos ⁡ ( T ) ∣ |\operatorname{Endpos}(t)|\geq |\operatorname{Endpos}(T)| Endpos(t)Endpos(T)。当且仅当取等号时, t t t T T T 会被压缩到同一个节点中。

而对于某一个子串 T T T 来说,肯定有一个分界长度 l e n len len,使得每一个长度 ≥ l e n \geq len len T T T 的后缀的 Endpos ⁡ \operatorname{Endpos} Endpos 都和 Endpos ⁡ ( T ) \operatorname{Endpos}(T) Endpos(T) 相同(所以这些后缀和 T T T 在同一个节点),且每一个长度 < l e n <len <len T T T 的后缀的 Endpos ⁡ \operatorname{Endpos} Endpos 大小都比 Endpos ⁡ ( T ) \operatorname{Endpos}(T) Endpos(T) 大(所以这些后缀和 T T T 不在同一个节点,而且这些后缀可能在不同的节点)。

所以每个节点 u u u 中存储的一定是一堆长度连续的子串,且短的串是长的串的后缀。不妨把这些串的集合称为 S ( u ) S(u) S(u),设其中最长的串为 longest ⁡ ( u ) \operatorname{longest}(u) longest(u),最短的串为 shortest ⁡ ( u ) \operatorname{shortest}(u) shortest(u)

我们在具体实现时会用一个 l e n len len 数组记录每个节点中最长的子串的长度(即 longest ⁡ ( u ) \operatorname{longest}(u) longest(u) 的长度),为什么不用记最短的长度,下文会讲。

Parent Tree

如上文所述,对于每一个子串都会有唯一一个 ”分界长度“,而且每一个节点中所有子串的 “分界长度” 都相同,为这个节点中最短的子串的长度。

而如果 t t t T T T 的一个后缀且没有和 T T T 分在一个节点中,那么 t t t 肯定也是别的子串的后缀,例如 ab \texttt{ab} ab 在串 cabzab \texttt{cabzab} cabzab 中既可以是 cab \texttt{cab} cab 的后缀,也可以是 zab \texttt{zab} zab 的后缀。这样我们看到:长的串 T T T 只能 “对应” 唯一的一个短的串 t t t,而短的串可以 “对应” 多个长的串,如果将 “短的串” 视为 “长的串” 的父亲,这就构成了一棵严格的树形结构。我们称为Parent Tree。

形式化地说,对于一个节点 u u u,我们找到 S ( u ) S(u) S(u) 中某一个子串 T T T 的后缀 t t t,使得 t t t 不在 S ⁡ ( u ) \operatorname{S}(u) S(u) 中且满足 ∣ t ∣ |t| t 最大(显然 t t t S ( u ) S(u) S(u) 中任何一个串的后缀且 ∣ t ∣ |t| t 等于 S ( u ) S(u) S(u) 中任何一个串的 “分界长度“ 减 1 1 1),记 u u u 的后缀链接 link ⁡ ( u ) \operatorname{link}(u) link(u) t t t 所属的节点。那么 link ⁡ \operatorname{link} link 所构成的就是这个 Parent Tree。

这时你会发现 shortest ⁡ ( u ) \operatorname{shortest}(u) shortest(u) 的长度其实就是 longest ⁡ ( link ⁡ ( u ) ) \operatorname{longest}(\operatorname{link}(u)) longest(link(u)) 的长度加 1 1 1,即 l e n ( link ⁡ ( u ) ) + 1 len(\operatorname{link}(u))+1 len(link(u))+1,所以我们无需记录 shortest ⁡ ( u ) \operatorname{shortest}(u) shortest(u) 的长度。

SAM 的转移

对于一个节点 u u u,在 S ( u ) S(u) S(u) 中的某一个串后面添加一个字符 c c c 变成一个新的串,如果这个新的串仍是 S S S 的子串(那么由于 S ( u ) S(u) S(u) 中的任意一个串在 S S S 的某个位置出现, S ( u ) S(u) S(u) 中的其他串肯定也会在同样位置出现,所以此时 S ( u ) S(u) S(u) 中的所有串添加这个字符 c c c 所形成的的新串也都仍是 S S S 的子串   ( 1 ) {\,}^{(1)} (1)),设这个新串所属的节点为 p p p,那么我们记录转移 c h [ u ] [ c ] ← p ch[u][c]\gets p ch[u][c]p

注意对于添加字符 c c c 而言,添加 c c c 后的新串可能不同,但它们的 Endpos ⁡ \operatorname{Endpos} Endpos 都是相同的,因为新串中的某一个串在某个位置出现,那么将它末尾的 c c c 删除后, S ( u ) S(u) S(u) 中的其他串也肯定会在同样位置出现,然后再加上末尾的 c c c,于是所有的新串也都会在同样的位置出现。这同时说明了 c h [ u ] [ c ] ch[u][c] ch[u][c] 是唯一的   ( 2 ) {\,}^{(2)} (2)

但注意这些新串所属的等价类 S ( c h [ u ] [ c ] ) S(ch[u][c]) S(ch[u][c]) 不一定只包含这些新串   ( 3 ) {\,}^{(3)} (3)。同时也说明了有可能有多个 c h [ u ] [ c ] ch[u][c] ch[u][c] 指向同一个点,于是 SAM 实际上是一个 DAG。

算法(实现)

考虑从前往后加入 S S S 的每一个字符,假设当前加入的是 c = S [ x ] c=S[x] c=S[x]

加入字符 c c c 的实际操作是把 S [ 1.. x ] S[1..x] S[1..x] 的所有后缀的 Endpos ⁡ \operatorname{Endpos} Endpos 集合都改变了(新增加了元素 x x x),考虑这将如何影响后缀树的形态,那么我们先要找到 S [ 1.. x ] S[1..x] S[1..x] 的所有后缀所在的节点。

那我们肯定要先新建一个节点 n o w now now 表示 S [ 1.. x ] S[1..x] S[1..x] Endpos ⁡ \operatorname{Endpos} Endpos 等价类,因为这个等价类之前一直没有出现过。

我们上一次插入 S [ x − 1 ] S[x-1] S[x1] 的时候肯定也新建了一个节点表示 S [ 1.. x − 1 ] S[1..x-1] S[1..x1] Endpos ⁡ \operatorname{Endpos} Endpos 等价类,记这个节点为 l a s t last last

根据 ( 1 ) (1) (1),由于 S [ 1.. x ] S[1..x] S[1..x] S [ 1.. x − 1 ] S[1..x-1] S[1..x1] 末尾添加字符 c c c 后得到的串,那么 S ( l a s t ) + c S(last)+c S(last)+c 的所有串都应该属于同一个 Endpos ⁡ \operatorname{Endpos} Endpos 等价类,于是直接 c h [ l a s t ] [ c ] ← n o w ch[last][c]\gets now ch[last][c]now

接着,令 p = l a s t p=last p=last,然后让 p p p 沿着 link ⁡ \operatorname{link} link 往上跳,并且一直记录 c h [ p ] [ c ] ← n o w ch[p][c]\gets now ch[p][c]now,直到满足已经存在转移 c h [ p ] [ c ] ch[p][c] ch[p][c] 了(此时证明 S [ 1.. x − 1 ] S[1..x-1] S[1..x1] 中出现过 S [ 1.. x ] S[1..x] S[1..x] 的后缀)。

p p p 一直往上跳的过程实际上相当于从长到短枚举 S [ 1.. x − 1 ] S[1..x-1] S[1..x1] 后缀中的每一种 Endpos ⁡ \operatorname{Endpos} Endpos 定价类,也就相当于把 S [ 1.. x − 1 ] S[1..x-1] S[1..x1] 的所有后缀都枚举一遍,而判断是否已经存在转移 c h [ p ] [ c ] ch[p][c] ch[p][c] 也就相当于把 S [ 1.. x ] S[1..x] S[1..x] 的每一个后缀都枚举了一遍(因为满足一个串 T T T S [ 1.. x ] S[1..x] S[1..x] 的后缀的必要条件是 T T T 去掉最后一位后是 S [ 1.. x − 1 ] S[1..x-1] S[1..x1] 的后缀),并判断它们是否在 S [ 1.. x − 1 ] S[1..x-1] S[1..x1] 中出现过。

所以如果跳到某个 p p p 仍然不存在转移 c h [ p ] [ c ] ch[p][c] ch[p][c],即 S ( p ) + c S(p)+c S(p)+c(显然这是 S [ 1.. x ] S[1..x] S[1..x] 的一段长度连续的后缀)没有在 S [ 1.. x − 1 ] S[1..x-1] S[1..x1] 中出现过,那么 S ( p ) + c S(p)+c S(p)+c Endpos ⁡ \operatorname{Endpos} Endpos 集合和 S [ 1.. x ] S[1..x] S[1..x] 的是一样的,即 S ( p ) + c S(p)+c S(p)+c 包含于 S ( n o w ) S(now) S(now),于是我们直接令 c h [ p ] [ c ] ← n o w ch[p][c]\gets now ch[p][c]now,再继续往上跳即可。

接下来我们分情况讨论:

  • 如果就这么顺着 Parent Tree 跳一直跳到了根节点还要往上,此时证明 S [ 1.. x ] S[1..x] S[1..x] 的任何一个后缀都没有在 S [ 1.. x − 1 ] S[1..x-1] S[1..x1] 中出现过,那么我们直接让 link ⁡ ( n o w ) = r t \operatorname{link}(now)=rt link(now)=rt 即可。

  • 否则,如果我们在跳的过程中找到了一个 p p p 使得已经存在转移 c h [ p ] [ c ] ch[p][c] ch[p][c] 了,我们就先设 q = c h [ p ] [ c ] q=ch[p][c] q=ch[p][c]

    但注意此时仅满足 S ( p ) + c S(p)+c S(p)+c 包含于 S ( q ) S(q) S(q),所以并不一定是 S ( q ) S(q) S(q) 中所有串的 Endpos ⁡ \operatorname{Endpos} Endpos 集合都改变了,即 S ( q ) S(q) S(q) 里面不一定全是 S [ 1.. x ] S[1..x] S[1..x] 的后缀。

    可以发现 S ( q ) S(q) S(q) 中所有 longest ⁡ ( p ) + c \operatorname{longest}(p)+c longest(p)+c 的后缀(即 S ( q ) S(q) S(q) 中所有长度小于等于 l e n ( p ) + 1 len(p)+1 len(p)+1 的串)都是 S [ 1.. x ] S[1..x] S[1..x] 的后缀(尽管这些串中可能有长度短的一部分并不属于 S ( p ) + c S(p)+c S(p)+c,但他们仍然是 S [ 1.. x ] S[1..x] S[1..x] 的后缀,我们一起考虑),它们的 Endpos ⁡ \operatorname{Endpos} Endpos 集合都改变了。

    同时 S ( q ) S(q) S(q) 中所有长度大于 l e n ( p ) + 1 len(p)+1 len(p)+1 的串都一定不是 S [ 1.. x ] S[1..x] S[1..x] 的后缀(因为这个 p p p 使我们最先找到的,即 longest ⁡ ( p ) + c \operatorname{longest}(p)+c longest(p)+c 应该是 S [ 1.. x ] S[1..x] S[1..x] S [ 1.. x − 1 ] S[1..x-1] S[1..x1] 中出现的最长的后缀),它们的 Endpos ⁡ \operatorname{Endpos} Endpos 集合都没有改变。

    然后我们再分情况讨论:

    • l e n ( q ) = l e n ( p ) + 1 len(q)=len(p)+1 len(q)=len(p)+1,我们直接令 link ⁡ ( n o w ) ← q \operatorname{link}(now)\gets q link(now)q 即可,上面已经证明了这样的 q q q 一定是最长的。

    • l e n ( q ) ≠ l e n ( p ) + 1 len(q)\neq len(p)+1 len(q)=len(p)+1,此时 longest ⁡ ( q ) \operatorname{longest}(q) longest(q) 不是 S [ 1.. x ] S[1..x] S[1..x] 的后缀,而且 longest ⁡ ( q ) \operatorname{longest}(q) longest(q) 会比 longest ⁡ ( p ) \operatorname{longest}(p) longest(p) 长一截。

      那么此时 S ( q ) S(q) S(q) 中长度大于 l e n ( p ) + 1 len(p)+1 len(p)+1 和长度小于等于 l e n ( p ) + 1 len(p)+1 len(p)+1 的两部分串的 Endpos ⁡ \operatorname{Endpos} Endpos 集合已经不同了,需要分离。

      于是我们新建一个点 n q nq nq,表示 S ( q ) S(q) S(q) 中长度小于等于 l e n ( p ) + 1 len(p)+1 len(p)+1 的那一部分串的 Endpos ⁡ \operatorname{Endpos} Endpos 等价类。这样就在 q q q f = link(q) ⁡ f=\operatorname{link(q)} f=link(q) 之间新插入了一个点,所以 link ⁡ ( q ) ← n q \operatorname{link}(q)\gets nq link(q)nq link ⁡ ( n q ) ← f \operatorname{link}(nq)\gets f link(nq)f。同时更新 l e n ( n q ) ← l e n ( p ) + 1 len(nq)\gets len(p)+1 len(nq)len(p)+1。也要更新 c h [ n q ] ← c h [ q ] ch[nq]\gets ch[q] ch[nq]ch[q](更新 c h [ n q ] ← c h [ q ] ch[nq]\gets ch[q] ch[nq]ch[q] 的原因上面 ( 1 ) (1) (1) 处有提到)。

      同时要让 link ⁡ ( n o w ) ← n q \operatorname{link}(now)\gets nq link(now)nq,上面同样也已经证明了这样找到的 n q nq nq 一定是最长的。

      最后,我们就要更新我们还要继续让 p p p 沿着 link ⁡ \operatorname{link} link 往上跳,如果 c h [ p ] [ c ] = q ch[p][c]=q ch[p][c]=q,那么更新 c h [ p ] [ c ] ← n q ch[p][c]\gets nq ch[p][c]nq(这里这么更新的证明比较显然,略去),否则停止上跳退出。

    然后就结束了吗? q q q n q nq nq)在 Parent Tree 上的祖先(即 p p p 在 Parent Tree 上的祖先往 c c c 的转移)的 Endpos ⁡ \operatorname{Endpos} Endpos 集合都有改变,它们不需要更新吗?事实上由于这些点所包含的所有串的 Endpos ⁡ \operatorname{Endpos} Endpos 集合都同样增加了一个元素 x x x(而且由于增加的元素为 x x x,所以这些点的转移不可能有更新),于是经过若干推导可知 SAM 的结构并没有改变,所以我们无需更新。

这样 SAM 就建好了。

实际应用

咕咕咕……

后缀树

定义

后缀树定义比 SAM 简单很多。对于串 S S S 的后缀树,我们先把串 S S S 的所有后缀各加入一个终止符后都插入到一棵 Trie 树中,比如对于串 banana \texttt{banana} banana,将得到下面这么一棵 Trie 树:(图来自于 EA’s blog

在这里插入图片描述

但这样节点数是 O ( n 2 ) O(n^2) O(n2) 的,但我们发现这棵 Trie 树上有很多节点只有一个儿子,这样构成了若干条单链,我们可以把这些链进行压缩,变成这样:

在这里插入图片描述

这样压缩后的字典树我们就把它称为后缀树。

这样的后缀树的节点数量是 O ( n ) O(n) O(n) 级别的,因为它只有 n n n 个叶子(终止符),而且每个点的儿子个数都大于 1 1 1,于是就能用类似虚树的方式证明出这棵树的节点至多只有 2 n − 1 2n-1 2n1 个。

再根据等一下会说的结论,这也侧面证明了 SAM 的节点个数至多为 2 n − 1 2n-1 2n1 个。

构建

直接构建后缀树有 Ukkonen 算法,但是实际上我们可以用 SAM 来构建。

结论:串 S S S 在 SAM 上的 Parent Tree 为串 S S S 的反串的后缀树。

假设现在有某个串 S ′ S' S,我们先定义 S ′ S' S 的某个子串在 S ′ S' S 中出现的所有位置的左端点集合为 leftpos ⁡ \operatorname{leftpos} leftpos 集合。这个定义和 Endpos ⁡ \operatorname{Endpos} Endpos 类似。

然后你发现后缀树上的一条边就代表着一个 leftpos ⁡ \operatorname{leftpos} leftpos 等价类,因为这条边上的所有点都没有分支,意味着对于这条边上的任意两个长度相差 1 1 1 A , A + c A,A+c A,A+c A A A 只会出现在 A + c A+c A+c 中,否则若 A A A 还出现在 A + c ′ A+c' A+c 中那么就会有 c ′ c' c 这个分支,就矛盾了。

于是后缀树上的一个点 u u u 就能代表它往父亲的那条边的 leftpos ⁡ \operatorname{leftpos} leftpos 等价类,于是可以类似地定义 longest ⁡ ′ ( u ) \operatorname{longest}'(u) longest(u) 表示 u u u 所代表的 leftpos ⁡ \operatorname{leftpos} leftpos 等价类中的所有串中最长的那个,显然 u u u 中的其他串都是 longest ⁡ ′ ( u ) \operatorname{longest}'(u) longest(u) 的前缀。

而且对于后缀树上点 u u u 的父亲 f f f,肯定有 longest ⁡ ′ ( f ) \operatorname{longest}'(f) longest(f) longest ⁡ ′ ( u ) \operatorname{longest}'(u) longest(u) 的所有前缀中和 longest ⁡ ′ ( u ) \operatorname{longest}'(u) longest(u) 不属于同一个 leftpos ⁡ \operatorname{leftpos} leftpos 集合的最长的前缀。

发现这和 SAM 的 Parent Tree 类似,于是把 S S S 反串, leftpos ⁡ \operatorname{leftpos} leftpos 变为 Endpos ⁡ \operatorname{Endpos} Endpos,就可以得到上面的结论了。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值