AC自动机 提高篇
文本生成器
首先考虑一个容斥,算出不包含任何一个单词的文章的数量。
我们设 \(dp_{i,j}\) 表示当前文章长度为 \(i\),最后一个字符在 \(AC\) 自动机上的 \(j\) 号点的方案数。我们要求的答案就是 \(\displaystyle 26^m-\sum_{i=0}^{idx}f_{m,i}\)。
于是我们考虑怎么转移。
首先,我们在建立 \(AC\) 自动机的时候,如果发现一个节点 \(i\) 指向的 \(ne_i\) 节点有结束标记,那么我们把这个点也打上结束标记。其他东西没有区别
然后我们对于 \(AC\) 自动机上的每一个点 \(j\),设他的其中一个字节点为 \(k\)。那么如果 \(k\) 节点没有结束标记,则 \(f_{i,k}+f_{i-1,j}\leftarrow f_{i,k}\)。于是就做完了,代码:
#include<bits/stdc++.h>
#define int long long
#define N 10005
#define M 105
#define mod 10007
using namespace std;
int tr[N][26],cnt[N],ne[N],n,m,idx,f[M][N];
char s[N];
//f_{i,j}表示字符串长度i,最后一个字符的点为j,且不包含任意一个模式串的方案数
int ksm(int x,int y){
int res=1;
while(y){
if(y&1)(res*=x)%=mod;
(x*=x)%=mod;
y>>=1;
}
return res;
}
void ins(){
int p=0;
for(int i=0;s[i];i++){
int t=s[i]-'A';
if(tr[p][t]==0)tr[p][t]=++idx;
p=tr[p][t];
}
cnt[p]=1;
}
void build(){
queue<int>q;
for(int i=0;i<26;i++){
if(tr[0][i]!=0){
q.push(tr[0][i]);
}
}
while(!q.empty()){
int t=q.front();
q.pop();
for(int i=0;i<26;i++){
int c=tr[t][i];
if(c==0){
tr[t][i]=tr[ne[t]][i];
}
else{
if(cnt[tr[ne[t]][i]]==1)cnt[tr[t][i]]=1;
ne[c]=tr[ne[t]][i];
q.push(c);
}
}
}
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>s;
ins();
}
build();
f[0][0]=1;//空字符串,方案数1
for(int i=1;i<=m;i++){
for(int j=0;j<=idx;j++){
for(int k=0;k<26;k++){
if(cnt[tr[j][k]]==1)continue;
(f[i][tr[j][k]]+=f[i-1][j])%=mod;
}
}
}
int sum=ksm(26,m),res=0;
for(int i=0;i<=idx;i++){
(res+=f[m][i])%=mod;
}
if(sum<res)sum+=mod;
cout<<sum-res;
return 0;
}
数数
发现就是上一题的 \(dp\) 变成了数位 \(dp\),于是我们采用记忆化搜索实现。
我们设 \(f_{i,j,k,l}\) 表示当前到了第 \(i\) 位数,最后一个数字在 \(AC\) 自动机上的位置为 \(j\),并且当前前 \(i\) 位是或不是和限制的数完全相同,当前前 \(i\) 位是不是全是 \(0\)。
但是由于我们记搜是最后到只剩一位才返回答案,相当于是倒着算的,所以我们初始要把限制数字翻转。
剩下的 \(dp\) 方式几乎和上一题一样,代码:
#include<bits/stdc++.h>
#define int long long
#define N 1605
#define mod 1000000007
using namespace std;
int tr[N][10],cnt[N],ne[N],n,m,idx,f[N][N][2][2];
string t;
char s[N];
void ins(string s){
int p=0;
for(int i=0;s[i];i++){
int t=s[i]-'0';
if(tr[p][t]==0)tr[p][t]=++idx;
p=tr[p][t];
}
cnt[p]=1;
}
void build(){
queue<int>q;
for(int i=0;i<10;i++){
if(tr[0][i]!=0){
q.push(tr[0][i]);
}
}
while(!q.empty()){
int t=q.front();
q.pop();
for(int i=0;i<10;i++){
int c=tr[t][i];
if(c==0){
tr[t][i]=tr[ne[t]][i];
}
else{
ne[c]=tr[ne[t]][i];
cnt[c]|=cnt[ne[c]];
q.push(c);
}
}
}
}
int dp(int dep,int ac_pos,bool is_lim,bool has_zer){
if(dep==0)return cnt[ac_pos]==0;
if(cnt[ac_pos]==1)return 0;
int &v=f[dep][ac_pos][is_lim][has_zer];
if(v!=-1)return v;
int lim=is_lim?(s[dep]-'0'):9ll;
int sum=0;
for(int i=0;i<=lim;i++){
int p1=(has_zer&&(i==0))?0:tr[ac_pos][i];
bool f1=(is_lim&&(i+'0'==s[dep]));
bool f2=(has_zer&&(i==0));
(sum+=dp(dep-1,p1,f1,f2))%=mod;
}
return v=sum;
}
signed main(){
cin>>s+1>>n;
int len=strlen(s+1);
reverse(s+1,s+len+1);
memset(f,-1,sizeof f);
for(int i=1;i<=n;i++){
cin>>t;
ins(t);
}
build();
int res=dp(len,0,1,1)+mod-1;
if(res>mod)res-=mod;
cout<<res;
return 0;
}
阿狸的打字机
首先我们要建出失配树。什么是失配树?就是把每个点连向他的失配指针构成的一棵树。例如:

接着我们考虑先建出 \(AC\) 自动机:
小写字母,直接向下走。
P,存下这个点的编号。B,回到父节点。
然后我们建出失配树。接着考虑怎么处理询问:
有多少个 \(x\) 在 \(y\) 中出现,事实上就是,有多少个 \(y\) 中的节点的 \(ne\) 指针直接或间接指向 \(x\) 的结束点。
于是我们对每个点 \(i\) 连一条 \(ne_i\rightarrow i\) 的边。如果 \(y\) 的一个点在 \(x\) 结束点为根的子树内,那么说明这个点的 \(ne\) 直接或间接指向 \(x\) 结束点。
所以我们把失配树做一个 \(dfs\) 序,使得一个点的子树变成一段区间,这就变成了一个单点修改,区间查询的问题。我们使用树状数组实现。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 100005
using namespace std;
int n,m,tr[N][26],ne[N],idx;
int din[N],dout[N],res[N],sum,h[N];
int a[N],fa[N],tot;
string s;
struct edge{
int to,nxt;
}e[N];
struct ask{
int x,id;
};
vector<ask>q[N];
struct bit{
int c[N];
int lowbit(int x){
return x&-x;
}
void modify(int x,int v){
while(x<=sum){
c[x]+=v;
x+=lowbit(x);
}
}
int qry(int x){
int res=0;
while(x){
res+=c[x];
x-=lowbit(x);
}
return res;
}
}bits;
void add(int a,int b){
e[++tot]={b,h[a]};
h[a]=tot;
}
void ins(string s){
int p=0;
for(int i=0;s[i];i++){
if(s[i]>='a'&&s[i]<='z'){
int t=s[i]-'a';
if(tr[p][t]==0){
tr[p][t]=++idx;
fa[tr[p][t]]=p;
}
p=tr[p][t];
}
else if(s[i]=='P')a[++n]=p;
else p=fa[p];
}
}
void dfs(int u,int f){
din[u]=++sum;
for(int i=h[u];i;i=e[i].nxt){
int j=e[i].to;
if(j==f)continue;
dfs(j,u);
}
dout[u]=sum;
}
void build(){
queue<int>q;
for(int i=0;i<26;i++){
if(tr[0][i]!=0){
q.push(tr[0][i]);
}
}
while(!q.empty()){
int t=q.front();
q.pop();
for(int i=0;i<26;i++){
int c=tr[t][i];
if(c==0){
tr[t][i]=tr[ne[t]][i];
}
else{
ne[c]=tr[ne[t]][i];
q.push(c);
}
}
}
for(int i=1;i<=idx;i++){
add(ne[i],i);
}
dfs(0,0);
}
signed main(){
cin>>s>>m;
ins(s);
build();
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
q[y].push_back({x,i});
}
int len=s.size();
int id=0,p=0;
for(int i=0;i<len;i++){
if(s[i]>='a'&&s[i]<='z'){
int t=s[i]-'a';
p=tr[p][t];
bits.modify(din[p],1);
}
else if(s[i]=='P'){
id++;
for(auto j:q[id]){
int x=j.x,id=j.id;
res[id]=bits.qry(dout[a[x]])-bits.qry(din[a[x]]-1);
}
}
else{
bits.modify(din[p],-1);
p=fa[p];
}
}
for(int i=1;i<=m;i++){
cout<<res[i]<<'\n';
}
return 0;
}
AC自动机 提高篇的更多相关文章
- 【学术篇】SPOJ GEN Text Generator AC自动机+矩阵快速幂
还有5天省选才开始点字符串这棵技能树是不是太晚了点... ~题目の传送门~ AC自动机不想讲了QAQ.其实很久以前是学过然后打过板子的, 但也仅限于打过板子了~ 之前莫名其妙学了一个指针版的但是好像不 ...
- 算法总结篇---AC自动机
目录 写在前面 算法流程 引例: 概述: Trie树的构建(第一步) 失配指针(第二步) 构建失配指针 字典树和字典图 多模式匹配 例题 写在前面 鸣谢: OiWiki 「笔记」AC 自动机---Lu ...
- 多模字符串匹配算法之AC自动机—原理与实现
简介: 本文是博主自身对AC自动机的原理的一些理解和看法,主要以举例的方式讲解,同时又配以相应的图片.代码实现部分也予以明确的注释,希望给大家不一样的感受.AC自动机主要用于多模式字符串的匹配,本质上 ...
- 浅谈AC自动机
写在前面:从10月23日开始写这篇博文,离NOIP2018只有十多天了.坚持不停课的倔强蒟蒻(我)尽量每天挤时间多搞一搞信竞(然而还要准备期中考试).NOIP争取考一个好成绩吧. 一.简介 AC自动机 ...
- AC自动机学习笔记-2(Trie图&&last优化)
我是连月更都做不到的蒟蒻博主QwQ 考虑到我太菜了,考完noip就要退役了,所以我决定还是把博客的倒数第二篇博客给写了,也算是填了一个坑吧.(最后一篇?当然是悲怆のnoip退役记啦QAQ) 所以我们今 ...
- AC自动机学习笔记-1(怎么造一台AC自动机?)
月更博主又来送温暖啦QwQ 今天我们学习的算法是AC自动机.AC自动机是解决字符串多模匹配问题的利器,而且代码也十分好打=w= 在这一篇博客里,我将讲解AC自动机是什么,以及怎么构建一个最朴素的AC自 ...
- 初学AC自动机
前言 一直听说\(AC\)自动机是一个很难很难的算法,而且它不在\(NOIP\)提高组范围内(这才是关键),所以我一直没去学. 最近被一些字符串题坑得太惨,于是下定决心去学\(AC\)自动机. 简介 ...
- AC 自动机刷题记录
目录 简介 第一题 第二题 第三题 第四题 第五题 第六题 简介 这就是用来记录我对于<信息学奥赛一本通 · 提高篇>一书中的习题的刷题记录以及学习笔记. 一般分专题来写(全部写一起可能要 ...
- 浅析 AC 自动机
目录 简述 AC 自动机是什么 AC 自动机有什么用 AC 自动机·初探 AC 自动机·原理分析 AC 自动机·代码实现 AC 自动机·更进一步 第一题 第二题 第三题 从 AC 自动机到 fail ...
- AC自动机-算法详解
What's Aho-Corasick automaton? 一种多模式串匹配算法,该算法在1975年产生于贝尔实验室,是著名的多模式匹配算法之一. 简单的说,KMP用来在一篇文章中匹配一个模式串:但 ...
随机推荐
- ps top命令查看内存空间
[root@VM-4-3-centos local]# ps aux --sort -rss | head USER PID %CPU %MEM VSZ RSS TTY STAT START TIME ...
- Nuxt3 的生命周期和钩子函数(三)
title: Nuxt3 的生命周期和钩子函数(三) date: 2024/6/27 updated: 2024/6/27 author: cmdragon excerpt: 摘要:概述了Nuxt3的 ...
- ARM Cortex-A系列处理器性能分类比较
在如今这个电子产品泛滥的年代,仅仅靠品牌或是外观已经不足以辨别产品的优劣,其内置的处理器自然也就成为了分辨产品是否高端的标准之一.那么我们今天就不妨好好了解一下近几年来电子产品中较为主流的RAM处理器 ...
- 基于OMAPL138+FPGA核心板——MCSDK开发入门(下)
本文测试板卡为创龙科技 SOM-TL138F 是一款基于 TI OMAP-L138(定点/浮点 DSP C674x + ARM9)+ 紫光同创 Logos/Xilinx Spartan-6 低功耗 F ...
- linux常见终端命令和一些小问题的解决
此文章为linux常见终端命令汇总和一些小问题的解决方法,会不定期更新. [常见指令] 1. 误按 Ctrl+s 锁住终端. ubuntu16命令行误按 Ctrl + s 导致终端锁定,Ctrl + ...
- 设置Docker容器里的时间
启动容器时,添加环境变量 docer run -e TZ=Asia/Shanghai --rm myalpine date -e TZ=Asia/Shanghai
- 记一次 .NET某酒业业务系统 崩溃分析
一:背景 1. 讲故事 前些天有位朋友找到我,说他的程序每次关闭时就会自动崩溃,一直找不到原因让我帮忙看一下怎么回事,这位朋友应该是第二次找我了,分析了下 dump 还是挺经典的,拿出来给大家分享一下 ...
- API网关实践-网易云轻舟微服务
微服务最佳实践中,我们需要通过统一的 API 网关进行服务能力的共享,API 网关为用户提供发布.管理.保护和监控 API的能力,帮助用户在自己的多个系统之间,或者内部系统与合作伙伴以及第三方的系统之 ...
- vs 常用的调试技巧
本地调试,一般打断点, 然后下一步,或者步入,或者运行到上一步. 有专用的对战窗口. 条件断点,输入当前变量的名称,然后打印变量值变量名和对战的一些信息,当然也可以选择进入断点后是否进一步运行 线程调 ...
- Jetpack Compose学习(12)——Material Theme的主题色切换
原文:Jetpack Compose学习(12)--Material Theme的主题色切换-Stars-One的杂货小窝 闲着无事研究了下Jetpack Compose M3 主题切换效果 本系列以 ...