自动机概述
一、适用问题
自动机主要适用于多个模式串下的匹配问题,常嵌套 进行考察或者利用其 树的性质进行出题。但是这些问题都有一个相同的特点,就是一定会题目给出或者人为建出多个字符串进行匹配,此点可以用于辨别 自动机问题。
二、 自动机算法解析
基本步骤
- 构造一颗 树,作为 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!
题意:
用
个基本字符组成一个长度为
的字符串,要求字符串中不能出现给定的
个非法串中任何一个,输出方案总数。
思路:
数据范围比较小,因此不难往
上进行思考。又因为有多个非法串,考虑在
自动机建的整个
图上进行
。
我们定义状态为 ,表示长度为 ,最后一个字符在 自动机的第 个节点上,则枚举 的所有子节点,设 ,即 为 的第 个子节点,则 ,当且仅当 和 不为非法节点。
因此我们继续定义非法节点,一个点为非法节点,即该点所代表的字符串中出现了完整的非法串,很明显一个非法串的末尾节点是非法节点,并且若 是非法节点,则 也为非法节点,因为 节点所代表的字符串为 节点字符串的后缀。
除此之外,此题还有两个坑点。
- 没有取模,因此需要大整数。
- 字符的 码范围在 之间, 了一小时…
总结:
在
自动机上进行
,就是以
自动机上的节点作为状态进行转移,本质上与普通
没有差别。
代码:
#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. 小明系列故事——女友的考验
题意:
给定
和
,表示一共有
个点,每个点都有其所对应的坐标,
条非法路径。先要从
号点走到
号点,但路径中不能出现
条非法路径中任意一条,求最短距离。
思路:
自动机上跑最短路,思路比较明显。
自动机上的每一个节点都代表一个状态,且不能通过任何非法节点。
需要在 自动机的根节点的所有儿子上都建立新节点,且如果一个节点的 节点为非法节点,则该节点也为非法节点。
建出 图后,直接在图上跑 最短路即可。
小坑点:坐标之差会爆 …(找了半小时 )
代码:
#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
题意:
给定
和
,表示要组成一个长度为
的字符串,再给出
个字符串,每个串都有一个能量值,现让你组成一个串,使得你组成的串的总能量值最大。若能量值相同输出长度小的,长度相同则输出字典序小的。
思路:
涉及多个串的问题,一般都采用
自动机进行解决,我们以
自动机中每一个节点为状态,进行状态转移。定义
表示长度为
的串,当前处于
自动机上的
节点的最大能量值,定义
为
状态下所组成的串,然后直接进行
转移即可。
注意,此问题中每个节点的贡献值不单只是其自身串的贡献,还有其所有 节点贡献的累加值。
代码:
#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] 密码
题意:
给出
和
,表示密码的长度和观察到的子串个数。再给出
个子串,询问长度为
的密码包含这
个子串的所有可能方案。如果方案总数小于
,则输出所有方案。
思路:
范围比较小,因此考虑状压
,
表示长度为
,包含的子串状态为
,当前在
自动机的第
个节点上,所有可能的方案数,直接转移即可求取答案。
但是此题还需要输出方案,因此我们对于每个 开一个 存储所有可能情况,但是需要剪枝,不然会 ,不能让任何一个 中的方案数大于 ,如此即可完成此题。
代码:
#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
题意:
给定
,表示构成字符串只能使用前
个字符,再给定一个包含
个禁止串的集合。然后询问是否能够构成一个无限长的串,满足以下三个条件。
- 该串中只使用了前 个字符
- 该串中不包含任何禁止串
- 该串不能在任何位置开始出现循环节
如果可以构造则输出 ,否则输出 。
思路:
先不考虑循环节的问题,如果单纯考虑能否构造一个只使用了前
个字符,不包含禁止串且长度无限的串,这个问题就是一个经典问题。
我们将禁止串插入 自动机中,所有的 出边组成一个有向图,我们将所有非法节点从图中去除,得到一个合法的有向图,我们可以在这个合法的图上任意走。因此仅当这个图中出现了环,我们才可以构建出一个无限长的串,否则不行。
在上面的基础上,我们考虑如何构建一个不出现循环节的长度无限的串,无限长则说明有环,那如果这是个简单环,则在不断绕行的过程中一定会出现循环节。而如果这个环中有好几个环,则可以在左边的环绕一圈,右边的再绕两圈,因此不会出现循环节。
因此此题只需要构建 自动机之后,去除无效节点,然后对剩下的有向图跑强连通分量,对于每个强连通分量查看是否有多个环即可。一个简单环中最多只有 条边,如果大于 条边,则非简单环,因此只需判定一个环中的边数量即可。
代码:
由于
崩了,只能放一个不知道能不能
的代码在这了…
#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’ 表示将当前字符打印出来。一共
次询问,每次询问给出一个
,表示查询第
次打印的字符串在第
次打印的字符串中出现了多少次。
思路:
首先先将长串加入到
自动机中,然后记录每个节点的父节点,遇到 ‘B’ 就往上走,遇到
就标记第
次打印对应的是
自动机上的哪个节点。
构造完 自动机后,问题就变成了如何查询其中一个节点在另一个节点中出现的次数。我们首先考虑暴力匹配,其实就是取出字符串 ,然后在 自动机中跑,所有经过的节点都标记一下,然后查询有多少个被标记的节点中包含 。即在 跑完之后,倒着将标记压到 节点上,然后查询 节点的标记次数即可。
在这个暴力过程中,我们可以发现一个节点在另一个节点中出现的次数,取决于其所有 子孙被标记的次数,因此我们将查询离线,按照右端点排序。然后根据 自动机的 指针建立 树,求出 序,用树状数组维护每个节点子树中被标记的次数。
总结:
此题最大的收获是彻底加深了对于
指针的理解,一个节点在另一个字符串中出现的次数,取决了该节点所有
子树中被标记的次数之和。
代码:
#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;
}