字符串小结(持续更新)

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

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 exttt{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 o 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 o 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];
		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
",t[id[i]].size);
	return 0;
}
/*
3
abc
cde
de
abcde
*/

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

原文地址:https://www.cnblogs.com/ez-lcw/p/14448619.html