AC自动机算法概述及习题

A C AC 自动机概述

一、适用问题

A C AC 自动机主要适用于多个模式串下的匹配问题,常嵌套 d p dp 进行考察或者利用其 f a i l fail 树的性质进行出题。但是这些问题都有一个相同的特点,就是一定会题目给出或者人为建出多个字符串进行匹配,此点可以用于辨别 A C AC 自动机问题。

二、 A C AC 自动机算法解析

基本步骤

  1. 构造一颗 T r i e Trie 树,作为 AC自动机的搜索数据结构。
  2. 构造 f a i l fail 指针,使当前字符失配时跳转到具有最长公共前后缀的字符继续匹配。如同 k m p kmp 算法一样, A C AC 自动机在匹配时如果当前字符匹配失败,那么利用 f a i l fail 指针进行跳转。由此可知如果跳转,跳转后的串的前缀,必为跳转前的模式串的后缀并且跳转的新位置的深度(匹配字符个数)一定小于跳之前的节点。因此我们可以利用 b f s bfs T r i e Trie 上面进行 f a i l fail 指针的构造。

f a i l fail 指针的构造

  • 【定义】 f a i l fail 指针指向最长的可匹配后缀, f a i l fail 指向点 p p ,表示从根节点到点 p p 是当前匹配位置的最长后缀。
  • 【构建方法】按深度 b f s bfs 整颗字典树,得到当前节点的 f a i l fail 指针需要查看其父亲的 f a i l fail 指针是否有自己这个儿子,如果没有,需要继续跳 f a i l fail 指针。
    在这里插入图片描述

常数优化

  • 最后我们在 A C AC 自动机上跑字符串的时候,我们在失配时,通过 f a i l fail 求出匹配点,用该点更新失配节点对应儿子,优化常数。
  • 【举例】假如 n e x t [ x ] [ i ] = = 0 next[x][i]==0 ,表示节点 x x 没有 i i 这个节点,因此我们会不断跳 x x f a i l fail 指针,直到找到一个节点 u u ,使得 s o n [ u ] [ i ] ! = 0 son[u][i]!=0 ,然后我们顺便设置 s o n [ x ] [ i ] = s o n [ u ] [ i ] son[x][i]=son[u][i] ,优化常数。

注意点

  • 给出 n n 个模式串以及 1 1 个匹配串,询问每个模式串在匹配串中出现的次数。在 A C AC 自动机上直接进行匹配时被匹配到的每个节点的 f a i l fail 节点也都被匹配到了,不能忘记计算其 f a i l fail 节点被匹配的次数。

三、 A C AC 自动机模板

struct Trie{
	int next[500010][26],fail[500010],end[500010]; //fail[i]是指从root到节点i这一段字母的最长后缀节点
	int root,L;	//L相当于tot
	int newnode()
	{
		for(int i = 0;i < 26;i++)
			next[L][i] = -1; //将root节点的26个子节点都指向-1
		end[L++] = 0; //每一个子节点初始化都不是单词结尾
		return L-1;
	}
 	void init()
 	{
 		L = 0;
 		root = newnode(); //此处返回root = 0
 	}
 	void insert(char buf[])
 	{
		int len = strlen(buf);
		int now = root;
		for(int i = 0;i < len;i++)
		{
			if(next[now][buf[i]-'a'] == -1)
				next[now][buf[i]-'a'] = newnode(); //不能在原有字典树中匹配的新字母则新建一个节点
			now = next[now][buf[i]-'a'];
 		}
 		end[now]++;	//now这个节点是一个单词的结尾
 	}
	void build()
	{
		queue<int>Q;
		fail[root] = root;
		for(int i = 0;i < 26;i++)
		{
			if(next[root][i] == -1)
				next[root][i] = root; //将root的未被访问的子节点指回root
			else
			{
				fail[next[root][i]] = root; //root子节点的失配指针指向root
				Q.push(next[root][i]); //队列中加入新节点
			}
		}
		while( !Q.empty() )
		{
			int now = Q.front();
			Q.pop();
			for(int i = 0;i < 26;i++)
				if(next[now][i] == -1)
					next[now][i] = next[fail[now]][i]; //now的第i个节点未被访问,则将now的第i个节点指向now的fail节点的第i个节点
				else
				{
					fail[next[now][i]]=next[fail[now]][i];
					Q.push(next[now][i]);
				}
		}
	}
	int query(char buf[])
	{
		int len = strlen(buf);
		int now = root;
		int res = 0;
		for(int i = 0;i < len;i++)
		{
			now = next[now][buf[i]-'a'];
			int temp = now;
			while( temp != root )
			{
				res += end[temp]; //访问到了这里说明,从root到tmp都已经匹配成功
				end[temp] = 0; //加上这句话则同一模式串在匹配串中只会匹配一次,而不会多次匹配
				temp = fail[temp]; //循环遍历从root到tmp这串字符串的所有后缀
			}
		}
		return res;
	}
	void debug()
	{
		for(int i = 0;i < L;i++)
		{
			printf("id = %3d,fail = %3d,end = %3d,chi = [",i,fail[i],end[i]);
			for(int j = 0;j < 26;j++)
				printf("%2d",next[i][j]);
			printf("]\n");
		}
	}
}ac;

AC自动机习题

1. Censored!

题意: n n 个基本字符组成一个长度为 m m 的字符串,要求字符串中不能出现给定的 p p 个非法串中任何一个,输出方案总数。 ( 1 n , m 50 , 0 p 10 ) (1\leq n,m\leq 50,0\leq p\leq 10)

思路: 数据范围比较小,因此不难往 d p dp 上进行思考。又因为有多个非法串,考虑在 A C AC 自动机建的整个 T r i e Trie 图上进行 d p dp

我们定义状态为 f [ i ] [ j ] f[i][j] ,表示长度为 i i ,最后一个字符在 A C AC 自动机的第 j j 个节点上,则枚举 j j 的所有子节点,设 n o w = n e x t [ j ] [ k ] now=next[j][k] ,即 n o w now j j 的第 k k 个子节点,则 f [ i ] [ n o w ] = f [ i ] [ n o w ] + f [ i 1 ] [ j ] f[i][now]=f[i][now]+f[i-1][j] ,当且仅当 n o w now j j 不为非法节点。

因此我们继续定义非法节点,一个点为非法节点,即该点所代表的字符串中出现了完整的非法串,很明显一个非法串的末尾节点是非法节点,并且若 f a i l [ n o w ] fail[now] 是非法节点,则 n o w now 也为非法节点,因为 f a i l [ n o w ] fail[now] 节点所代表的字符串为 n o w now 节点字符串的后缀。

除此之外,此题还有两个坑点。

  1. 没有取模,因此需要大整数。
  2. 字符的 A S C I I ASCII 码范围在 128 128 -128~128 之间, r e re 了一小时…

总结: A C AC 自动机上进行 d p dp ,就是以 A C AC 自动机上的节点作为状态进行转移,本质上与普通 d p dp 没有差别。

代码:

#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstring>
#include <queue>
#define rep(i,a,b) for(int i = a; i <= b; i++)
typedef long long ll;
using namespace std;

char buf[60],base = 0;
int n,m,p,mp[301];

struct Trie{
	int next[2510][60],fail[2510],end[2510]; //fail[i]是指从root到节点i这一段字母的最长后缀节点
	int root,L;	//L相当于tot
	int newnode()
	{
		for(int i = 0;i < 51;i++)
			next[L][i] = -1; //将root节点的26个子节点都指向-1
		end[L++] = 0; //每一个子节点初始化都不是单词结尾
		return L-1;
	}
 	void init()
 	{
 		L = 0;
 		root = newnode(); //此处返回root = 0
 	}
 	void insert(char buf[])
 	{
		int len = strlen(buf);
		int now = root;
		for(int i = 0;i < len;i++)
		{
			int pos = mp[buf[i]-base+150];
			if(next[now][pos] == -1)
				next[now][pos] = newnode(); //不能在原有字典树中匹配的新字母则新建一个节点
			now = next[now][pos];
 		}
 		end[now] = 1;	//now这个节点是一个单词的结尾
 	}
	void build()
	{
		queue<int>Q;
		fail[root] = root;
		for(int i = 0;i < 51;i++)
		{
			if(next[root][i] == -1)
				next[root][i] = root; //将root的未被访问的子节点指回root
			else
			{
				fail[next[root][i]] = root; //root子节点的失配指针指向root
				Q.push(next[root][i]); //队列中加入新节点
			}
		}
		while( !Q.empty() )
		{
			int now = Q.front();
			if(end[fail[now]]) end[now] = 1; //判断该节点是否非法
			Q.pop();
			for(int i = 0;i < 51;i++)
				if(next[now][i] == -1)
					next[now][i] = next[fail[now]][i]; //now的第i个节点未被访问,则将now的第i个节点指向now的fail节点的第i个节点
				else
				{
					fail[next[now][i]]=next[fail[now]][i];
					Q.push(next[now][i]);
				}
		}
	}
}ac;

struct BigInteger{
    ll A[50];
    enum{MOD = 1000000};
    BigInteger(){memset(A, 0, sizeof(A)); A[0]=1;}
    void set(ll x){memset(A, 0, sizeof(A)); A[0]=1; A[1]=x;}
    void print(){
        printf("%lld", A[A[0]]);
        for (ll i=A[0]-1; i>0; i--){
            if (A[i]==0){printf("000000"); continue;}
            for (ll k=10; k*A[i]<MOD; k*=10ll) printf("0");
            printf("%lld", A[i]);
        }
        printf("\n");
    }
    ll& operator [] (int p) {return A[p];}
    const ll& operator [] (int p) const {return A[p];}
    BigInteger operator + (const BigInteger& B){
        BigInteger C;
        C[0]=max(A[0], B[0]);
        for (ll i=1; i<=C[0]; i++)
            C[i]+=A[i]+B[i], C[i+1]+=C[i]/MOD, C[i]%=MOD;
        if (C[C[0]+1] > 0) C[0]++;
        return C;
    }
    BigInteger operator * (const BigInteger& B){
        BigInteger C;
        C[0]=A[0]+B[0];
        for (ll i=1; i<=A[0]; i++)
            for (ll j=1; j<=B[0]; j++){
                C[i+j-1]+=A[i]*B[j], C[i+j]+=C[i+j-1]/MOD, C[i+j-1]%=MOD;
            }
        if (C[C[0]] == 0) C[0]--;
        return C;
    }
}f[2][2510];

int main()
{
	scanf("%d%d%d",&n,&m,&p);
	scanf("%s",buf);
	rep(i,0,n-1) mp[buf[i]-base+150] = i;
	ac.init();
	rep(i,1,p){
		scanf("%s",buf);
		ac.insert(buf);
	}
	ac.build();
	f[0][0].set(1);
	rep(i,1,m){
		rep(j,0,ac.L-1) f[i%2][j].set(0);
		rep(j,0,ac.L-1)
			rep(k,0,n-1){
				int now = ac.next[j][k];
				if(ac.end[now] == 0 && ac.end[j] == 0) f[i%2][now] = f[i%2][now] + f[(i-1)%2][j];
			}
	}
	BigInteger ans; ans.set(0);
	rep(i,0,ac.L-1) ans = ans+f[m%2][i];
	ans.print();
	return 0;
}
2. 小明系列故事——女友的考验

题意: 给定 n n m m ,表示一共有 n n 个点,每个点都有其所对应的坐标, m m 条非法路径。先要从 1 1 号点走到 n n 号点,但路径中不能出现 m m 条非法路径中任意一条,求最短距离。 ( 1 n 50 , 1 m 100 ) (1\leq n\leq 50,1\leq m\leq 100)

思路: A C AC 自动机上跑最短路,思路比较明显。 A C AC 自动机上的每一个节点都代表一个状态,且不能通过任何非法节点。

需要在 A C AC 自动机的根节点的所有儿子上都建立新节点,且如果一个节点的 f a i l fail 节点为非法节点,则该节点也为非法节点。

建出 T r i e Trie 图后,直接在图上跑 d i j k s t r a dijkstra 最短路即可。

小坑点:坐标之差会爆 i n t int …(找了半小时 b u g bug

代码:

#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
const int N = 100+10;
typedef double db;
const db inf = 1e15;
using namespace std;

void dbg() {cout << "\n";}
template<typename T, typename... A> void dbg(T a, A... x) {cout << a << ' '; dbg(x...);}
#define logs(x...) {cout << #x << " -> "; dbg(x);}

int n,m,base[100],vis[25100];
db X[N],Y[N],dis[25100];
struct Node{
	db ans; int a,b;
	bool operator < (Node xx) const {
		return ans > xx.ans;
	}
};
priority_queue<Node> q;

struct Trie{
	int next[25100][60],fail[25100],end[25100]; //fail[i]是指从root到节点i这一段字母的最长后缀节点
	int root,L;	//L相当于tot
	int newnode()
	{
		for(int i = 0;i < 51;i++)
			next[L][i] = -1; //将root节点的26个子节点都指向-1
		end[L++] = 0; //每一个子节点初始化都不是单词结尾
		return L-1;
	}
 	void init()
 	{
 		L = 0;
 		root = newnode(); //此处返回root = 0
 	}
 	void insert(int len)
 	{
		int now = root;
		for(int i = 0;i < len;i++)
		{
			int pos = base[i];
			if(next[now][pos] == -1)
				next[now][pos] = newnode(); //不能在原有字典树中匹配的新字母则新建一个节点
			now = next[now][pos];
 		}
 		end[now] = 1;	//now这个节点是一个单词的结尾
 	}
	void build()
	{
		queue<int> Q;
		fail[root] = root;
		for(int i = 0;i < 51;i++)
		{
			if(next[root][i] == -1)
				next[root][i] = root; //将root的未被访问的子节点指回root
			else
			{
				fail[next[root][i]] = root; //root子节点的失配指针指向root
				Q.push(next[root][i]); //队列中加入新节点
			}
		}
		while( !Q.empty() )
		{
			int now = Q.front();
			if(end[fail[now]]) end[now] = 1; //判断该节点是否非法
			Q.pop();
			for(int i = 0;i < 51;i++)
				if(next[now][i] == -1)
					next[now][i] = next[fail[now]][i]; //now的第i个节点未被访问,则将now的第i个节点指向now的fail节点的第i个节点
				else
				{
					fail[next[now][i]]=next[fail[now]][i];
					Q.push(next[now][i]);
				}
		}
	}
}ac;

db dist(int i,int j){
	//坐标之差会爆int...
	db tmp = (X[i]-X[j])*(X[i]-X[j])+(Y[i]-Y[j])*(Y[i]-Y[j]);
	tmp = sqrt(tmp);
	return tmp;
}

void dijkstra(){
	while(q.size()) q.pop();
	rep(i,0,50)
		if(ac.next[ac.root][i] == 0){
			int p = ac.newnode();
			ac.next[ac.root][i] = p;
			rep(j,0,50) ac.next[p][j] = 0;
		}
	rep(i,0,ac.L) dis[i] = inf, vis[i] = 0;
	q.push({0,ac.next[ac.root][1],1}); dis[ac.next[ac.root][1]] = 0;
	db ans = inf;
	while(q.size()){
		int now = q.top().a, id = q.top().b; q.pop();
		if(vis[now]) continue;
		vis[now] = 1;
		if(id == n) ans = min(ans,dis[now]);
		rep(i,id+1,n){
			db tp = dis[now]+dist(id,i);
			int y1 = now, y2 = ac.next[now][i];
			while(y2 == 0){
				y1 = ac.fail[y1];
				y2 = ac.next[y1][i];
			}
			ac.next[now][i] = y2;
			if(!vis[y2] && !ac.end[y2] && dis[y2] > tp){
				dis[y2] = tp;
				q.push({dis[y2],y2,i});
			}
		}
	}
	if(ans == inf) printf("Can not be reached!\n");
	else printf("%.2f\n",ans);
}

int main(){
	while(~scanf("%d%d",&n,&m)){
		if(n == 0 && m == 0) break;
		rep(i,1,n) scanf("%lf%lf",&X[i],&Y[i]);
		ac.init();
		rep(i,1,m){
			int k; scanf("%d",&k);
			rep(j,0,k-1) scanf("%d",&base[j]);
			ac.insert(k);
		}
		ac.build();
		dijkstra();
	}
}	
3. Ring

题意: 给定 n n m m ,表示要组成一个长度为 n n 的字符串,再给出 m m 个字符串,每个串都有一个能量值,现让你组成一个串,使得你组成的串的总能量值最大。若能量值相同输出长度小的,长度相同则输出字典序小的。 ( 0 n 50 , 0 m 100 ) (0\leq n\leq 50,0\leq m\leq 100)

思路: 涉及多个串的问题,一般都采用 A C AC 自动机进行解决,我们以 A C AC 自动机中每一个节点为状态,进行状态转移。定义 f [ i ] [ j ] f[i][j] 表示长度为 i i 的串,当前处于 A C AC 自动机上的 j j 节点的最大能量值,定义 s [ i ] [ j ] s[i][j] f [ i ] [ j ] f[i][j] 状态下所组成的串,然后直接进行 d p dp 转移即可。

注意,此问题中每个节点的贡献值不单只是其自身串的贡献,还有其所有 f a i l fail 节点贡献的累加值。

代码:

#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
const int inf = 1e8;
using namespace std;

struct Trie{
	int next[500010][26],fail[500010],end[500010]; //fail[i]是指从root到节点i这一段字母的最长后缀节点
	int root,L;	//L相当于tot
	int newnode()
	{
		for(int i = 0;i < 26;i++)
			next[L][i] = -1; //将root节点的26个子节点都指向-1
		end[L++] = 0; //每一个子节点初始化都不是单词结尾
		return L-1;
	}
 	void init()
 	{
 		L = 0;
 		root = newnode(); //此处返回root = 0
 	}
 	void insert(char buf[],int tp)
 	{
		int len = strlen(buf);
		int now = root;
		for(int i = 0; i < len; i++){
			if(next[now][buf[i]-'a'] == -1)
				next[now][buf[i]-'a'] = newnode(); //不能在原有字典树中匹配的新字母则新建一个节点
			now = next[now][buf[i]-'a'];
 		}
 		end[now] = tp;	//now这个节点是一个单词的结尾
 	}
	void build()
	{
		queue<int>Q;
		fail[root] = root;
		for(int i = 0;i < 26;i++)
		{
			if(next[root][i] == -1)
				next[root][i] = root; //将root的未被访问的子节点指回root
			else
			{
				fail[next[root][i]] = root; //root子节点的失配指针指向root
				Q.push(next[root][i]); //队列中加入新节点
			}
		}
		while( !Q.empty() ) //按长度进行bfs
		{
			int now = Q.front();
			end[now] += end[fail[now]]; //继承前面节点的value
			Q.pop();
			for(int i = 0;i < 26;i++)
				if(next[now][i] == -1)
					next[now][i] = next[fail[now]][i]; //now的第i个节点未被访问,则将now的第i个节点指向now的fail节点的第i个节点
				else
				{
					fail[next[now][i]]=next[fail[now]][i];
					Q.push(next[now][i]);
				}
		}
	}
	int query(char buf[])
	{
		int len = strlen(buf);
		int now = root;
		int res = 0;
		for(int i = 0;i < len;i++)
		{
			now = next[now][buf[i]-'a'];
			int temp = now;
			while( temp != root )
			{
				res += end[temp]; //访问到了这里说明,从root到tmp都已经匹配成功
				end[temp] = 0; //加上这句话则同一模式串在匹配串中只会匹配一次,而不会多次匹配
				temp = fail[temp]; //循环遍历从root到tmp这串字符串的所有后缀
			}
		}
		return res;
	}
}ac;

char buf[211][111];
int n,m,f[55][1200]; //pre记录从哪个节点来以及是啥字符
string s[55][1200];

void solve(){
	rep(i,0,n)
		rep(j,0,ac.L) f[i][j] = -inf, s[i][j] = "\0";
	f[0][0] = 0;
	rep(i,1,n)
		rep(j,0,ac.L-1){
			rep(k,0,25){
				int now = ac.next[j][k];
				int tp = f[i-1][j]+ac.end[now];
				string hp = s[i-1][j]+(char)(k+'a');
				if(f[i][now] < tp || (f[i][now] == tp && hp < s[i][now])){
					f[i][now] = tp;
					s[i][now] = hp;
				}
			}
		}
	int ans = 0;
	rep(i,1,n)
		rep(j,0,ac.L-1) ans = max(ans,f[i][j]);
	if(ans == 0) printf("\n");
	else{
		string str = "\0"; int len = 100;
		rep(i,1,n){
			rep(j,0,ac.L-1)
				if(f[i][j] == ans){
					if(i < len || (i == len && s[i][j] < str))
							len = i, str = s[i][j];
				}
		}
		cout << str << endl;
	}
}

int main()
{
	int _; scanf("%d",&_);
	while(_--)
	{
		scanf("%d%d",&n,&m);
		ac.init();
		rep(i,1,m) scanf("%s",buf[i]);
		rep(i,1,m){
			int tp; scanf("%d",&tp);
			ac.insert(buf[i],tp);
		}
		ac.build();
		solve();
	}
	return 0;
}
4. [JSOI2009] 密码

题意: 给出 L L N N ,表示密码的长度和观察到的子串个数。再给出 N N 个子串,询问长度为 L L 的密码包含这 N N 个子串的所有可能方案。如果方案总数小于 42 42 ,则输出所有方案。 ( 1 L 25 , 1 N 10 ) (1\leq L\leq 25,1\leq N\leq 10)

思路: N N 范围比较小,因此考虑状压 d p dp f [ i ] [ S ] [ j ] f[i][S][j] 表示长度为 i i ,包含的子串状态为 S S ,当前在 A C AC 自动机的第 j j 个节点上,所有可能的方案数,直接转移即可求取答案。

但是此题还需要输出方案,因此我们对于每个 f [ i ] [ S ] [ j ] f[i][S][j] 开一个 v e c t o r vector 存储所有可能情况,但是需要剪枝,不然会 m l e mle ,不能让任何一个 v e c t o r vector 中的方案数大于 42 42 ,如此即可完成此题。

代码:

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;

struct Trie{
	int next[200][26],fail[200],end[200]; //fail[i]是指从root到节点i这一段字母的最长后缀节点
	int root,L;	//L相当于tot
	int newnode()
	{
		for(int i = 0;i < 26;i++)
			next[L][i] = -1; //将root节点的26个子节点都指向-1
		end[L++] = 0; //每一个子节点初始化都不是单词结尾
		return L-1;
	}
 	void init()
 	{
 		L = 0;
 		root = newnode(); //此处返回root = 0
 	}
 	void insert(char buf[],int flag)
 	{
		int len = strlen(buf);
		int now = root;
		for(int i = 0;i < len;i++)
		{
			if(next[now][buf[i]-'a'] == -1)
				next[now][buf[i]-'a'] = newnode(); //不能在原有字典树中匹配的新字母则新建一个节点
			now = next[now][buf[i]-'a'];
 		}
 		end[now] = flag;	//now这个节点是一个单词的结尾
 	}
	void build()
	{
		queue<int>Q;
		fail[root] = root;
		for(int i = 0;i < 26;i++)
		{
			if(next[root][i] == -1)
				next[root][i] = root; //将root的未被访问的子节点指回root
			else
			{
				fail[next[root][i]] = root; //root子节点的失配指针指向root
				Q.push(next[root][i]); //队列中加入新节点
			}
		}
		while( !Q.empty() )
		{
			int now = Q.front();
			end[now] += end[fail[now]];
			Q.pop();
			for(int i = 0;i < 26;i++)
				if(next[now][i] == -1)
					next[now][i] = next[fail[now]][i]; //now的第i个节点未被访问,则将now的第i个节点指向now的fail节点的第i个节点
				else
				{
					fail[next[now][i]]=next[fail[now]][i];
					Q.push(next[now][i]);
				}
		}
	}
}ac;

char buf[110];
int n,m;
ll dp[27][1050][80];
vector<string> base[27][1050][80];

void solve(){
	memset(dp,0,sizeof dp);
	dp[0][0][0] = 1;
	for(int i = 1; i <= n; i++)
		for(int S = 0; S <= (1<<m)-1; S++)
			for(int j = 0; j <= ac.L-1; j++)
				for(int k = 0; k <= 25; k++){
					int now = ac.next[j][k];
					dp[i][S|ac.end[now]][now] += dp[i-1][S][j];
				}
	ll ans = 0;
	for(int j = 0; j <= ac.L-1; j++) ans += dp[n][(1<<m)-1][j];
	if(ans <= 42){
		for(int i = 0; i <= n; i++)
			for(int S = 0; S <= (1<<m)-1; S++)
				for(int j = 0; j <= ac.L-1; j++){
					dp[i][S][j] = 0; base[i][S][j].clear();
				}
		dp[0][0][0] = 1;
		for(int i = 1; i <= n; i++)
			for(int S = 0; S <= (1<<m)-1; S++)
				for(int j = 0; j <= ac.L-1; j++){
					if(dp[i-1][S][j] == 0) continue;
					for(int k = 0; k <= 25; k++){
						int now = ac.next[j][k];
						dp[i][S|ac.end[now]][now] += dp[i-1][S][j];
						if((int)base[i][S|ac.end[now]][now].size()+(int)base[i-1][S][j].size() > ans) continue;
						if(dp[i-1][S][j] != 0 && (int)base[i-1][S][j].size() == 0){
							string tp = "\0"; tp += (char)('a'+k);
							base[i][S|ac.end[now]][now].push_back(tp);
						}
						else{
							for(auto &v:base[i-1][S][j]){
								string tp = v+(char)('a'+k);
								base[i][S|ac.end[now]][now].push_back(tp);
							}
						}
					}
				}
		vector<string> fin; fin.clear();
		for(int j = 0; j < ac.L; j++){
			for(auto &v:base[n][(1<<m)-1][j])
				fin.push_back(v);
		}
		printf("%lld\n",ans);
		sort(fin.begin(),fin.end());
		for(auto &v:fin)
			cout << v << endl;
	}
	else printf("%lld\n",ans);
}

int main()
{
	scanf("%d%d",&n,&m);
	ac.init();
	for(int i = 0;i < m;i++){
		scanf("%s",buf);
		ac.insert(buf,1<<i);
	}
	ac.build();
	solve();
	return 0;
}
5. String of Infinity

题意: 给定 m m ,表示构成字符串只能使用前 m m 个字符,再给定一个包含 n n 个禁止串的集合。然后询问是否能够构成一个无限长的串,满足以下三个条件。

  1. 该串中只使用了前 m m 个字符
  2. 该串中不包含任何禁止串
  3. 该串不能在任何位置开始出现循环节

如果可以构造则输出 Y e s Yes ,否则输出 N o No ( 1 n 100 , 1 m 26 ) (1\leq n\leq 100,1\leq m\leq 26)

思路: 先不考虑循环节的问题,如果单纯考虑能否构造一个只使用了前 m m 个字符,不包含禁止串且长度无限的串,这个问题就是一个经典问题。

我们将禁止串插入 A C AC 自动机中,所有的 n e x t next 出边组成一个有向图,我们将所有非法节点从图中去除,得到一个合法的有向图,我们可以在这个合法的图上任意走。因此仅当这个图中出现了环,我们才可以构建出一个无限长的串,否则不行。

在上面的基础上,我们考虑如何构建一个不出现循环节的长度无限的串,无限长则说明有环,那如果这是个简单环,则在不断绕行的过程中一定会出现循环节。而如果这个环中有好几个环,则可以在左边的环绕一圈,右边的再绕两圈,因此不会出现循环节。

因此此题只需要构建 A C AC 自动机之后,去除无效节点,然后对剩下的有向图跑强连通分量,对于每个强连通分量查看是否有多个环即可。一个简单环中最多只有 n n 条边,如果大于 n n 条边,则非简单环,因此只需判定一个环中的边数量即可。

代码:
由于 Z O J ZOJ 崩了,只能放一个不知道能不能 A C AC 的代码在这了…

#include <bits/stdc++.h>
const int N = 1e5+10;
typedef long long ll;
using namespace std;

char buf[N];
int n,m,pos[N],tot,head[N],dfn[N],top,sz[N],vis[N]; //top记录dfs序
vector<pair<int,int> > v[N];
ll c[N],ans[N];
struct Node{
	int to,next;
}e[N];
void add(int x,int y){
	e[++tot].to = y, e[tot].next = head[x], head[x] = tot;
}

struct Trie{
	int next[N][26],fail[N],end[N],fa[N]; //fail[i]是指从root到节点i这一段字母的最长后缀节点
	int root,L;	//L相当于tot
	int newnode()
	{
		for(int i = 0;i < 26;i++)
			next[L][i] = -1; //将root节点的26个子节点都指向-1
		fa[L] = 0; end[L++] = 0; //每一个子节点初始化都不是单词结尾
		return L-1;
	}
 	void init()
 	{
 		L = 0;
 		root = newnode(); //此处返回root = 0
 	}
 	void insert(char buf[])
 	{
		int len = strlen(buf), cnt = 0;
		int now = root;
		for(int i = 0;i < len;i++)
		{
			if(buf[i] == 'B') now = fa[now];
			else if(buf[i] == 'P') pos[++cnt] = now;
			else{
				if(next[now][buf[i]-'a'] == -1){
					next[now][buf[i]-'a'] = newnode(); //不能在原有字典树中匹配的新字母则新建一个节点
					fa[next[now][buf[i]-'a']] = now;
				}
				now = next[now][buf[i]-'a'];
			}
 		}
 	}
	void build()
	{
		queue<int>Q;
		fail[root] = root;
		for(int i = 0;i < 26;i++)
		{
			if(next[root][i] == -1)
				next[root][i] = root; //将root的未被访问的子节点指回root
			else
			{
				fail[next[root][i]] = root; //root子节点的失配指针指向root
				Q.push(next[root][i]); //队列中加入新节点
			}
		}
		while( !Q.empty() )
		{
			int now = Q.front();
			add(fail[now],now);
			Q.pop();
			for(int i = 0;i < 26;i++)
				if(next[now][i] == -1)
					next[now][i] = next[fail[now]][i]; //now的第i个节点未被访问,则将now的第i个节点指向now的fail节点的第i个节点
				else
				{
					fail[next[now][i]]=next[fail[now]][i];
					Q.push(next[now][i]);
				}
		}
	}
}ac;

inline int lowbit(int x) {return x&(~x+1);}
inline void update(int x,ll v) {for(;x<=ac.L;x+=lowbit(x)) c[x] += v;}
inline ll ask(int x){
	ll tp = 0;
	while(x) tp += c[x], x -= lowbit(x);
	return tp;
}

void dfs(int x){
	dfn[x] = ++top; sz[x] = 1;
	for(int i = head[x]; i; i = e[i].next){
		int y = e[i].to; dfs(y);
		sz[x] += sz[y];
	}
}

void solve(){
	int len = strlen(buf), now = ac.root;
	int cnt = 0;
	for(int i = 0; i < len; i++){
		if(buf[i] == 'B'){
			update(dfn[now],-1);
			now = ac.fa[now];
			vis[now] = 0;
			continue;
		}
		else if(buf[i] == 'P'){
			cnt++;
			for(auto &hp:v[cnt]){
				int x = hp.first, y = cnt, id = hp.second;
				ll mp = ask(dfn[pos[x]]+sz[pos[x]]-1)-ask(dfn[pos[x]]-1);
				ans[id] = mp;
			}
			continue;
		}
		else now = ac.next[now][buf[i]-'a'];
		if(!vis[now]){
			vis[now] = 1;
			update(dfn[now],1);
		}
		else{
			vis[now] = 0;
			update(dfn[now],-1);
		}
	}
	for(int i = 1; i <= m; i++) printf("%lld\n",ans[i]);
}

int main()
{
	scanf("%s",buf);
	scanf("%d",&m);
	for(int i = 1; i <= m; i++){
		int l,r; scanf("%d%d",&l,&r);
		v[r].push_back(make_pair(l,i));
	}
	ac.init();
	ac.insert(buf);
	tot = 1;
	ac.build();
	for(int i = 0; i < ac.L; i++)
		if(!dfn[i]) dfs(i);
	solve();
	return 0;
}
6. [NOI2011] 阿狸的打字机

题意: 给出一个长串。其中 ‘B’ 表示最后一个字母被删除,‘P’ 表示将当前字符打印出来。一共 m m 次询问,每次询问给出一个 ( x , y ) (x,y) ,表示查询第 x x 次打印的字符串在第 y y 次打印的字符串中出现了多少次。 ( 1 n , m 1 0 5 ) (1\leq n,m\leq 10^5)

思路: 首先先将长串加入到 A C AC 自动机中,然后记录每个节点的父节点,遇到 ‘B’ 就往上走,遇到 P P 就标记第 x x 次打印对应的是 A C AC 自动机上的哪个节点。

构造完 A C AC 自动机后,问题就变成了如何查询其中一个节点在另一个节点中出现的次数。我们首先考虑暴力匹配,其实就是取出字符串 y y ,然后在 A C AC 自动机中跑,所有经过的节点都标记一下,然后查询有多少个被标记的节点中包含 x x 。即在 y y 跑完之后,倒着将标记压到 f a i l fail 节点上,然后查询 x x 节点的标记次数即可。

在这个暴力过程中,我们可以发现一个节点在另一个节点中出现的次数,取决于其所有 f a i l fail 子孙被标记的次数,因此我们将查询离线,按照右端点排序。然后根据 A C AC 自动机的 f a i l fail 指针建立 f a i l fail 树,求出 d f s dfs 序,用树状数组维护每个节点子树中被标记的次数。

总结: 此题最大的收获是彻底加深了对于 f a i l fail 指针的理解,一个节点在另一个字符串中出现的次数,取决了该节点所有 f a i l fail 子树中被标记的次数之和。

代码:

#include <bits/stdc++.h>
const int N = 1e5+10;
typedef long long ll;
using namespace std;

char buf[N];
int n,m,pos[N],tot,head[N],dfn[N],top,sz[N],vis[N]; //top记录dfs序
vector<pair<int,int> > v[N];
ll c[N],ans[N];
struct Node{
	int to,next;
}e[N];
void add(int x,int y){
	e[++tot].to = y, e[tot].next = head[x], head[x] = tot;
}

struct Trie{
	int next[N][26],fail[N],end[N],fa[N]; //fail[i]是指从root到节点i这一段字母的最长后缀节点
	int root,L;	//L相当于tot
	int newnode()
	{
		for(int i = 0;i < 26;i++)
			next[L][i] = -1; //将root节点的26个子节点都指向-1
		fa[L] = 0; end[L++] = 0; //每一个子节点初始化都不是单词结尾
		return L-1;
	}
 	void init()
 	{
 		L = 0;
 		root = newnode(); //此处返回root = 0
 	}
 	void insert(char buf[])
 	{
		int len = strlen(buf), cnt = 0;
		int now = root;
		for(int i = 0;i < len;i++)
		{
			if(buf[i] == 'B') now = fa[now];
			else if(buf[i] == 'P') pos[++cnt] = now;
			else{
				if(next[now][buf[i]-'a'] == -1){
					next[now][buf[i]-'a'] = newnode(); //不能在原有字典树中匹配的新字母则新建一个节点
					fa[next[now][buf[i]-'a']] = now;
				}
				now = next[now][buf[i]-'a'];
			}
 		}
 	}
	void build()
	{
		queue<int>Q;
		fail[root] = root;
		for(int i = 0;i < 26;i++)
		{
			if(next[root][i] == -1)
				next[root][i] = root; //将root的未被访问的子节点指回root
			else
			{
				fail[next[root][i]] = root; //root子节点的失配指针指向root
				Q.push(next[root][i]); //队列中加入新节点
			}
		}
		while( !Q.empty() )
		{
			int now = Q.front();
			add(fail[now],now);
			Q.pop();
			for(int i = 0;i < 26;i++)
				if(next[now][i] == -1)
					next[now][i] = next[fail[now]][i]; //now的第i个节点未被访问,则将now的第i个节点指向now的fail节点的第i个节点
				else
				{
					fail[next[now][i]]=next[fail[now]][i];
					Q.push(next[now][i]);
				}
		}
	}
}ac;

inline int lowbit(int x) {return x&(~x+1);}
inline void update(int x,ll v) {for(;x<=ac.L;x+=lowbit(x)) c[x] += v;}
inline ll ask(int x){
	ll tp = 0;
	while(x) tp += c[x], x -= lowbit(x);
	return tp;
}

void dfs(int x){
	dfn[x] = ++top; sz[x] = 1;
	for(int i = head[x]; i; i = e[i].next){
		int y = e[i].to; dfs(y);
		sz[x] += sz[y];
	}
}

void solve(){
	int len = strlen(buf), now = ac.root;
	int cnt = 0;
	for(int i = 0; i < len; i++){
		if(buf[i] == 'B'){
			update(dfn[now],-1);
			now = ac.fa[now];
			vis[now] = 0;
			continue;
		}
		else if(buf[i] == 'P'){
			cnt++;
			for(auto &hp:v[cnt]){
				int x = hp.first, y = cnt, id = hp.second;
				ll mp = ask(dfn[pos[x]]+sz[pos[x]]-1)-ask(dfn[pos[x]]-1);
				ans[id] = mp;
			}
			continue;
		}
		else now = ac.next[now][buf[i]-'a'];
		if(!vis[now]){
			vis[now] = 1;
			update(dfn[now],1);
		}
		else{
			vis[now] = 0;
			update(dfn[now],-1);
		}
	}
	for(int i = 1; i <= m; i++) printf("%lld\n",ans[i]);
}

int main()
{
	scanf("%s",buf);
	scanf("%d",&m);
	for(int i = 1; i <= m; i++){
		int l,r; scanf("%d%d",&l,&r);
		v[r].push_back(make_pair(l,i));
	}
	ac.init();
	ac.insert(buf);
	tot = 1;
	ac.build();
	for(int i = 0; i < ac.L; i++)
		if(!dfn[i]) dfs(i);
	solve();
	return 0;
}
发布了244 篇原创文章 · 获赞 115 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_41552508/article/details/101387192