自动AC机
Keywords Research
板子题,同luoguP3808,不过是多测。
然后多测不清空,(MLE)两行泪。
板子放一下
#include<bits/stdc++.h>
using namespace std;
#define ll int
#define ull unsigned long long
#define ZZ_zuozhe int main()
#define S 1000010
ll n;
char tmp[S];
ll vcn=0;
struct vertex
{
ll fail;
ll son[30];
ll num;
}tree[S];
ll q[S],l,r;
void build()
{
ll len=strlen(tmp);
ll rt=0;
for(int i=0;i<len;i++)
{
ll t=tmp[i]-'a';
if(!tree[rt].son[t])tree[rt].son[t]=++vcn;
rt=tree[rt].son[t];
}
tree[rt].num++;
}
void getfail()
{
l=r=0;
tree[0].fail=0;
for(int i=0;i<26;i++)
{
if(tree[0].son[i])
{
tree[tree[0].son[i]].fail=0;
q[++r]=tree[0].son[i];
}
}
while(l<r)
{
ll u=q[++l],v;
for(int i=0;i<26;i++)
{
v=tree[u].son[i];
if(v)
{
tree[v].fail=tree[tree[u].fail].son[i];
q[++r]=v;
}
else tree[u].son[i]=tree[tree[u].fail].son[i];
}
}
}
ll AC()
{
ll len=strlen(tmp);
ll u=0,ans=0,v;
for(int i=0;i<len;i++)
{
ll t=tmp[i]-'a';
u=tree[u].son[t];
v=u;
while(v&&tree[v].num!=-1)
{
ans+=tree[v].num;
tree[v].num=-1;
v=tree[v].fail;
}
}
return ans;
}
void clear()
{
memset(tree,0,sizeof tree);
vcn=0;
}
ll T;
ZZ_zuozhe
{
scanf("%d",&T);
while(T--)
{
clear();
scanf("%d",&n);
while(n--)
{
scanf("%s",tmp);
build();
}
getfail();
scanf("%s",tmp);
printf("%d
",AC());
}
return 0;
}
玄武密码
求最大匹配前缀。多模式串。
似乎是板子……(然而当时并没打熟
跳fail的时候需要把能匹配的位置都标上,能匹配上的肯定是前缀。
然后再扫一下(trie),找之前的标记,并取最大下标。
求最大匹配后缀的话是不是要倒一下……一会问问
其实只有四个字母应该压下空间的,不过这题不压也能过(懒 ZZ 懒)
然后就是以后要看清题里给的是小写字母还是大写的,要不数组会越界于无形之中(如果数据正好又比较小,(wa)都不知道怎么(wa)的……
TJOI2013 单词
文章单词之间是有空格的,分开插入就行,刚开始想多了……
(哪有英语文章不写空格的啊喂
(fail)有一个性质就是,一个串的(fail)其实是这个串的一个后缀,也就是这个串包含了他的(fail)
所以说,一个单词是多少单词的(fail),他就出现了多少次
考虑一个(fail)指针组成的(fail)树,那么一个单词出现次数就是(fail)树中以他为根的子树大小
刚开始以为是要在(trie)上做一个(dfs),但是这样统计实际上是不完全的,例如:
abc
babc
这时只考虑了一个串是另外一个串的前缀的情况。
核心代码长这样:
for(int i=vcn;i>=1;i--)tree[tree[q[i]].fail].num+=tree[q[i]].num;
for(int i=0;i<n;i++)printf("%lld
",tree[tail[i]].num);
POI2000 病毒
看题意很迷惑,然后画了点图,发现大概是要在(trie)上找到一个不经过串尾的环
不经过串尾保证他不会和某个模式串匹配,环保证他能无限循环
然后(dfs)就行了
注意:一个结点不断跳(fail),肯定是能跳到(0)的,但是需要保证这一路上没有危险节点,所以标记危险节点的时候,除了标模式串尾的结点,不如把在多次跳(fail)后会到达危险节点的结点也标上。
然后dfs时跳的是两个儿子,而没有判(0),这是因为之前在(getfail)的时候已经把这样不存在的儿子引到父亲的(fail)的儿子了,所以其实没有影响的。
代码或许需要存一下……
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define ZZ_zuozhe int main()
#define S 60005
ll n;
struct vertex
{
ll fail;
ll son[2];
}tree[S];
ll dg[S];
ll q[S];
ll vcn=0;
char tmp[S];
void ins()
{
ll rt=0;
ll len=strlen(tmp);
for(int i=0;i<len;i++)
{
if(!tree[rt].son[tmp[i]-'0'])tree[rt].son[tmp[i]-'0']=++vcn;
rt=tree[rt].son[tmp[i]-'0'];
}
dg[rt]=1;
}
void getfail()
{
ll l,r;
l=r=0;
tree[0].fail=0;
for(int i=0;i<2;i++)
{
if(tree[0].son[i])
{
tree[tree[0].son[i]].fail=0;
q[++r]=tree[0].son[i];
}
}
while(l<r)
{
ll u=q[++l],v;
for(int i=0;i<2;i++)
{
v=tree[u].son[i];
if(v)
{
tree[v].fail=tree[tree[u].fail].son[i];
dg[v]|=dg[tree[v].fail];
q[++r]=v;
}
else tree[u].son[i]=tree[tree[u].fail].son[i];
}
}
}
bool vis[S]={0},fvis[S]={0};
bool dfs(ll rt)
{
if(vis[rt])return 1;
if(dg[rt]||fvis[rt])return 0;
vis[rt]=fvis[rt]=1;
bool res=0;
if(!dg[tree[rt].son[0]])res|=dfs(tree[rt].son[0]);
if(!dg[tree[rt].son[1]])res|=dfs(tree[rt].son[1]);
//if(!dg[tree[rt].fail])res|=dfs(tree[rt].fail);
vis[rt]=0;
return res;
}
ZZ_zuozhe
{
scanf("%lld",&n);
for(int i=1;i<=n;i++)
{
scanf("%s",tmp);
ins();
}
getfail();
if(dfs(0))puts("TAK");
else puts("NIE");
return 0;
}
最短母串
警醒:把一个数类型设成(char),白调(2h)
一点点dp(其实主要是状压),但我可以送命了qaq
trie树先建出来
(n)的范围不是很大,所以可以考虑用状压记录各个字符串是否被用过,至于状态,因为一个字符串用过肯定经过了他的结尾那个结点,所以每个结点记录上状态,在建树的时候结尾要附上这个字符串的状态((1<<x))
根据fail的性质,getfail时,结点和他的fail的状态要合并,因为到了这个节点就是可以选择跳fail的,就相当于已经走了之后的路了
为什么此处必定跳fail呢?因为这里如果有fail就说明这个节点的后面可以匹配了,这个时候不跳串会更长,浪费,不合题意。
用一个vis数组记录是否被访问过,(vis[i][j])代表节点(i)是否以状态(j)被访问过,用来避免bfs时由于重复的字符串得不到最优解的情况
那么做一个bfs,每次队头取出一个结点和他在进队时处于的状态,枚举他的所有儿子,然后合并状态,再塞进队列里,等着下一次用
如果出现了状态填满了,就输出,这里利用一个小技巧,把之前处理的时候把状态编号指向他父亲的编号,输出方便
#include<bits/stdc++.h>
using namespace std;
#define ll int
#define ull unsigned long long
#define ZZ_zuozhe int main()
#define NS 5005
ll n;
char tmp[55];
struct vertex
{
ll fail;
ll son[26];
ll st;
}tree[NS];
ll vcn=0;
queue<ll> q;
void getfail()
{
for(int i=0;i<26;i++)
{
if(tree[0].son[i])
{
q.push(tree[0].son[i]);
}
}
while(!q.empty())
{
ll u=q.front();
q.pop();
//cout<<u<<endl;
for(int i=0;i<26;i++)
{
if(tree[u].son[i])
{
tree[tree[u].son[i]].fail=tree[tree[u].fail].son[i];
tree[tree[u].son[i]].st|=tree[tree[tree[u].fail].son[i]].st;
q.push(tree[u].son[i]);
}
else tree[u].son[i]=tree[tree[u].fail].son[i];
}
}
}
bool vis[NS][1<<12|1];
char ans[NS];
ll nod=0;
ll fa[NS*(1<<12|1)],an[NS*(1<<12|1)];
ll tot=0;
queue<ll>q1,q2;
ZZ_zuozhe
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%s",tmp);
ll len=strlen(tmp);
ll rt=0;
for(int j=0;j<len;j++)
{
ll t=tmp[j]-'A';
if(!tree[rt].son[t])tree[rt].son[t]=++vcn;
rt=tree[rt].son[t];
}
tree[rt].st|=(1<<(i-1));
}
getfail();
vis[0][0]=1;
q1.push(0);
q2.push(0);
ll tt=0;
while(!q1.empty())
{
ll rt=q1.front(),st=q2.front();
q1.pop();q2.pop();
if(st==((1<<n)-1))
{
while(tt)
{
ans[++nod]=an[tt];
tt=fa[tt];
}
for(int i=nod;i>0;i--)putchar(ans[i]+'A');
return 0;
}
for(int i=0;i<26;i++)
{
if(!tree[rt].son[i])continue;
if(!vis[tree[rt].son[i]][st|tree[tree[rt].son[i]].st])
{
vis[tree[rt].son[i]][st|tree[tree[rt].son[i]].st]=1;
q1.push(tree[rt].son[i]);
q2.push(st|tree[tree[rt].son[i]].st);
fa[++tot]=tt;
an[tot]=i;
}
}
tt++;
}
return 0;
}
文本生成器
也是dp
既然让求可读文本总数,不妨反着想,用总方案数(26^n)减去不碰边界的方案数
其实跟前面那个病毒挺像的,都是标记危险节点,然后遍历的时候故意不走
设(f[i][j])为在(j)点,有(i)位的时候,不碰边界的方案总数
那么
最后快速幂求(26^n),再减去所有(f[m][~])的和,记得取模。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ZZ_zuozhe int main()
#define ull unsigned long long
#define P 10007
#define N 65
#define M 105
#define S 6005
ll n,m;
struct vertex
{
ll fail;
ll son[26];
bool dg;
}tree[S];
char tmp[M];
ll vcn=0;
void ins()
{
ll len=strlen(tmp);
ll rt=0;
for(int i=0;i<len;i++)
{
ll t=tmp[i]-'A';
if(!tree[rt].son[t])tree[rt].son[t]=++vcn;
rt=tree[rt].son[t];
}
tree[rt].dg=1;
}
void getfail()
{
queue<ll>Q;
for(int i=0;i<26;i++)if(tree[0].son[i])Q.push(tree[0].son[i]);
while(!Q.empty())
{
ll rt=Q.front();
Q.pop();
for(int i=0;i<26;i++)
{
ll v=tree[rt].son[i];
if(v)
{
tree[v].fail=tree[tree[rt].fail].son[i];
tree[v].dg|=tree[tree[v].fail].dg;
Q.push(v);
}
else tree[rt].son[i]=tree[tree[rt].fail].son[i];
}
}
}
ll f[M][S];
ll ksm(ll a,ll b)
{
ll res=1;
while(b)
{
if(b&1)res=res*a%P;
a=a*a%P;
b>>=1;
}
return res%P;
}
ll sol()
{
f[0][0]=1;
ll res=0;
for(int i=1;i<=m;i++)
{
for(int j=0;j<=vcn;j++)
{
for(int k=0;k<26;k++)
{
if(!tree[tree[j].son[k]].dg)
{
f[i][tree[j].son[k]]+=f[i-1][j];
f[i][tree[j].son[k]]%=P;
}
}
}
}
res=ksm(26,m);
ll tmp=0;
for(int i=0;i<=vcn;i++)tmp=(tmp+f[m][i])%P;
return ((res-tmp)%P+P)%P;
}
ZZ_zuozhe
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%s",tmp);
ins();
}
getfail();
printf("%lld",sol());
return 0;
}
背单词
警醒:线段树(rt<<1)打成(rt),白调(3h)
为什么今天全是(sb)错误啊qaq
AC自动机,dfs序,线段树
首先分析下题意:每个单词是后一个单词的子串
,这个放在AC自动机里怎么理解呢
其实,每一个单词是后一个单词的子串,就是说后一个单词某一个结点的fail指向了前一个单词的结尾节点,此时后一个单词的前缀的后缀就是前一个单词,显然满足条件。
于是,问题转化为:前后两个单词满足后一个单词某一个结点的fail是前一个单词的结尾节点,从fail树的角度考虑,即在fail树中,后一个单词某结点在fail树以前一个单词结尾节点为根的子树中
关于子树的判断,dfs序是绝佳的选择
首先明确如果某个节点的价值,是他的fail树子树里任何一个节点都有机会享受到的,而dfs序是连续的,那么这就可以用线段树维护
因为取的是子序列,所以每次取出一个单词,枚举其中的每一个字母,选取当前能得到价值最大的一个节点(线段树维护最大值,单点查询),这个价值加上当前单词的价值,就是前(i)个单词的最大价值。
得到最大价值后,这个最大价值是可以让前一个单词结尾节点的fail子树内所有结点共享的,既然记录了dfs序,这里就可以用线段树的区间修改来解决。
这个过程中,如果遇到价值为负的单词,可以直接跳过,因为前面单词的选择不会对后面单词是否选择产生什么影响,那就干脆不要选负的。
在前面处理完毕后,枚举得到的每一个最大值,就可以得出全局的最大值。
#include<bits/stdc++.h>
using namespace std;
#define ll int
#define ull undigned long long
#define ZZ_zuozhe int main()
#define S 1000005
#define pb push_back
#define INF 100000000
ll T,n;
ll w[S];
char tmp[S];
vector<char> v[S];
struct trie
{
ll fail;
ll son[30];
ll num;
}tree[S];
ll vcn=1;
void ins()
{
ll len=strlen(tmp);
ll rt=1;
for(int i=0;i<len;i++)
{
ll t=tmp[i]-'a';
if(!tree[rt].son[t])tree[rt].son[t]=++vcn;
rt=tree[rt].son[t];
}
///
}
void getfail()
{
queue<int> Q;
tree[1].fail=1;
for(int i=0;i<26;i++)
{
if(tree[1].son[i])tree[tree[1].son[i]].fail=1;
Q.push(tree[1].son[i]);
}
while(!Q.empty())
{
ll u=Q.front();
Q.pop();
for(int i=0;i<26;i++)
{
ll v=tree[u].son[i];
if(v)
{
tree[v].fail=tree[tree[u].fail].son[i];
Q.push(v);
}
else tree[u].son[i]=tree[tree[u].fail].son[i];
}
}
}
struct edge
{
ll u,v;
}e[S<<1];
ll head[S],next[S],cnt=0;
void add(ll u,ll v)
{
++cnt;
e[cnt].u=u;
e[cnt].v=v;
next[cnt]=head[u];
head[u]=cnt;
}
ll l[S],r[S],tt=0;
void dfs(ll rt)
{
l[rt]=++tt;
for(int i=head[rt];i;i=next[i])dfs(e[i].v);
r[rt]=tt;
}
struct xds
{
ll son[2];
ll lz;
ll val;
}t[S<<2];
#define lson rt<<1,l,m
#define rson rt<<1|1,m+1,r
inline void upd(ll rt){t[rt].val=max(t[rt<<1].val,t[rt<<1|1].val);}
void build(ll rt,ll l,ll r)
{
t[rt].val=-INF;
t[rt].lz=0;
if(l==r)
{
return;
}
ll m=(l+r)>>1;
build(lson);
build(rson);
upd(rt);
}
void pushdown(ll rt)
{
if(t[rt].lz)
{
t[rt<<1].lz=max(t[rt<<1].lz,t[rt].lz);
t[rt<<1|1].lz=max(t[rt<<1|1].lz,t[rt].lz);
t[rt<<1].val=max(t[rt<<1].val,t[rt].lz);
t[rt<<1|1].val=max(t[rt<<1|1].val,t[rt].lz);
t[rt].lz=0;
}
}
ll query(ll rt,ll l,ll r,ll nl,ll nr)
{
if(nl<=l&&r<=nr)
{
return t[rt].val;
}
pushdown(rt);
ll m=(l+r)>>1;
ll res=0;
if(m>=nl)res=max(res,query(lson,nl,nr));
if(m<nr)res=max(res,query(rson,nl,nr));
return res;
}
void update(ll rt,ll l,ll r,ll nl,ll nr,ll val)
{
if(nl<=l&&r<=nr)
{
t[rt].val=max(t[rt].val,val);
t[rt].lz=max(t[rt].lz,val);
return;
}
pushdown(rt);
ll m=(l+r)>>1;
if(m>=nl)update(lson,nl,nr,val);
if(m<nr)update(rson,nl,nr,val);
upd(rt);
}
ll f[S];
void clear()
{
memset(tree,0,sizeof tree);
memset(e,0,sizeof e);
memset(head,0,sizeof head);
memset(next,0,sizeof next);
cnt=1;
memset(l,0,sizeof l);
memset(r,0,sizeof r);
tt=0;
memset(t,0,sizeof t);
for(int i=1;i<=n;i++)v[i].clear();
memset(f,0,sizeof f);
}
ZZ_zuozhe
{
//freopen("owo.in","r",stdin);
//freopen("owo.out","w",stdout);
scanf("%d",&T);
while(T--)
{
//ll a=0;
clear();
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%s%d",tmp,&w[i]);
//if(w[i]>0)a+=w[i];
ins();
ll len=strlen(tmp);
for(int j=0;j<len;j++)v[i].pb(tmp[j]);
}
getfail();
for(int i=2;i<=vcn;i++)add(tree[i].fail,i);
dfs(1);
build(1,1,tt);
for(int i=1;i<=n;i++)
{
if(w[i]<=0)continue;
ll len=v[i].size();
ll rt=1,ma=0;
for(int j=0;j<len;j++)
{
ll t=v[i][j]-'a';
rt=tree[rt].son[t];
ma=max(ma,query(1,1,tt,l[rt],l[rt]));
}
f[i]=max(ma,0)+w[i];
update(1,1,tt,l[rt],r[rt],f[i]);
}
ll ans=-INF;
for(int i=1;i<=n;i++)ans=max(ans,f[i]);
printf("%d
",ans);
//cout<<a<<' '<<tt<<' '<<vcn<<endl;
}
return 0;
}
好长啊我想学压行
密码
awsl
第一眼感觉是限长度版的最短母串,然后写假了,只有(20pts)
其实是挺像的,都用了状压……但是这个不太一样
最短母串里因为要求最短,所以我们转移的时候判了一个是否有儿子,但这个不要求最短,有时甚至必须有多余的字符,所以按照最短母串的思路是走不通的
之前在一篇题解里好像看见过“AC自动机+DP,一般都是设(f[i][j]),(i)是当前匹配长度,(j)是当前节点,然后视情况再加一维其他信息”
这里我们用状压思想,所以就加上一维已有单词的状态,即设(f[i][j][S]),(i,j)意义同上,(S)为当前状态
则有转移方程:
做这样的题时,注意在(getfail)时同时将(fail)结点的状态合并至本结点。
答案即为(sum f[L][][S_满])
关于输出:
我们先用dfs预处理出所有结点的位置是否能通向一个符合要求的串,然后输出时利用他剪枝,如果不通就不走了。输出就是一边dfs一边存下当前答案,够长度了就输出
(这题不写输出给(50pts)???)
打算有时间来一遍二刷,不留代码了
[BJWC2011]禁忌
ac自动机版GT考试
首先要读懂题……这种带有剧情的题有时候会读着很麻烦……
- 字母集(A)上的每个非空字符串对应了一个魔法。其中(A)是包含了前(alphabet)个小写字母的集合。
- 有一个集合(T),包含了(N)个字母集(A)上的字符串。(T)中的每一串称为一个禁忌串(Taboo string)
- 一个魔法,或等价地,其对应的串(s)因为包含禁忌而对使用者造成的伤害按以下方式确定:把(s)分割成若干段,考虑其中是禁忌串的段的数目,不同的分割可能会有不同的数目,其最大值就是这个伤害。
第一条这个(A)里头有一堆小写字母,每个非空字符串就是把这些字母随便拼(这条似乎无关紧要
第二条就是给一个集合(T),里面都是满足要求的(A)中的字符串,这些串叫禁忌串,一会会给
第三条就是在一个字符串中,有多少相互不重叠的禁忌串,就造成多大伤害
那么题中给出了所有禁忌串,现在需要我们求一个长度为(len)的,字母从(A)中取的,随机的串中禁忌串数量的期望(因为要达到题中的最大情况,就是在切的时候恰好把存在的每一个不重叠的禁忌串都切出来
考虑到多模式串,我们可以建出AC自动机
考虑匹配的过程,每个节点如果是一个禁忌串的末尾,就应当直接切,也就是前面扔掉,再从头进行匹配(假如此时有一个也正在匹配的更长的禁忌串,去匹配它显然不优)这个时候,对答案产生了贡献,我们把这部分贡献计入答案,并将它纳入之后的转移中。而如果这时并不能匹配成功,就枚举此节点所有的儿子并等概率转移。
转移类似于这个的意思:
这个形式就可以用矩阵加速。那么考虑初始矩阵。
我们的矩阵下标为节点编号,(M[i][j])代表的是匹配位置(i
ightarrow j)的期望
另外,我们设(M^n)中的(M[i][vcn+1])为以(i)为开头,共有(n)位时对答案的总贡献,那么(M[0][vcn+1])即为答案。
设置初始矩阵:
(egin{cases}
if(tree[tree[i].son[j]].dg)\
M_0[i][0]+=frac{1}{alphabet}(回到树根,供下一步转移)\
M_0[i][vcn+1]+=frac{1}{alphabet}(计入答案)\
else\
M_0[i][j]+=frac{1}{alphabet}(向下走一步)\
特别地,设M_0[vcn+1][vcn+1]=1,便于转移
end{cases})
之后就是简单的矩阵快速幂啥的,代码很好写不放,期望题大概主要考察思路吧