【BJOI2017】魔法咒语

时间限制 : 1000 MS 空间限制 : 131072 KB

问题描述

Chandra 是一个魔法天才。
从一岁时接受火之教会洗礼之后,Chandra 就显示出对火元素无与伦比的亲和力,轻而易举地学会种种晦涩难解的法术。这也多亏 Chandra有着常人难以企及的语言天赋,让她能轻松流利地说出咒语中那些极其拗口的魔法词汇。

直到十四岁,开始学习威力强大的禁咒法术时,Chandra 才遇到了障碍。

根据火之魔法规则,禁咒的构成单位是 N 个基本词汇。施法时只要凝聚精神力,说出一段用这些词语组成的长度恰好等于 L 的语言,就能释放威力超乎想象的火法术。过去的魔法师们总结了几种表达起来最连贯的组合方式,方便施法者以最快语速完成法术。

但具有魔法和语言双重天才的 Chandra不满足于这几种流传下来的禁咒,因为她可以毫无困难地说出普通人几乎不可能表达的禁咒语句。然而,在实际施法时,Chandra 发现有些自创禁咒念出后不但没有预期效果,反而会使自己的精神力迅速枯竭,十分难受。

这个问题令 Chandra 万分不解。她大量阅读典籍,到处走访魔法学者,并且不顾精神折磨一次又一次尝试新咒语,希望找出问题的答案。

很多年过去了,在一次远古遗迹探险中,Chandra意外闯进了火之神艾利克斯的不知名神殿。根据岩土特征分析,神殿应该有上万年的历史,这是极其罕见的。Chandra 小心翼翼地四处探索,沿着魔力流动来到一间密室。她看见密室中央悬浮着一本书籍。在魔法保护下书籍状况完好。精通上古语言的 Chandra 读过此书,终于解开了多年的困惑。

禁咒法术之所以威力强大,是因为咒语借用了火之神艾利克斯的神力。这本书里记载了艾利克斯生平忌讳的 M 个词语,比如情敌的名字、讨厌的植物等等。使用禁咒法术时,如果语言中含有任何忌讳词语,就会触怒神力而失效,施法者也一并遭受惩罚。

例如,若 ”banana” 是唯一的忌讳词语,“an”、”ban”、”analysis” 是基本词汇,禁咒长度须是11,则“bananalysis” 是无效法术,”analysisban”、”anbanbanban”是两个有效法术。注意:一个基本词汇在禁咒法术中可以出现零次、一次或多次;只要组成方式不同就认为是不同的禁咒法术,即使书写形式相同。

谜题破解,Chandra 心情大好。她决定计算一共有多少种有效的禁咒法术。

由于答案可能很大,你只需要输出答案模 1,000,000,007 的结果。

输入格式

第一行,三个正整数 N, M, L。
接下来 N 行,每行一个只含小写英文字母的字符串,表示一个基本词汇。
接下来 M 行,每行一个只含小写英文字母的字符串,表示一个忌讳词语。

输出格式

仅一行,一个整数,表示答案(模 10^9+7)。

样例输入 1

4 2 10
boom
oo
ooh
bang
ob
mo

样例输出 1

14

样例输入 2

3 1 3
a
ab
aba
aaa

样例输出 2

3

样例输入 3

3 1 14
ban
an
analysis
banana

样例输出 3

15

提示

样例解释 1】

有效的禁咒法术共有 14 种:boom/bang/oo,oo/oo/oo/oo/oo,oo/oo/ooh/ooh,

oo/ooh/oo/ooh,oo/ooh/ooh/oo,ooh/oo/oo/ooh,ooh/oo/ooh/oo,

ooh/ooh/boom,ooh/ooh/oo/oo,ooh/ooh/bang,ooh/bang/ooh,

bang/oo/oo/oo,bang/ooh/ooh,bang/bang/oo。

【样例解释 2】

有效的禁咒法术有 a/ab, ab/a, aba 共三种。注意,ab/a 和 aba 算成两种不同的禁咒法术。

【数据规模与约定】

本题一共有 10 个测试点。

下表是每个测试点的数据规模和约定:
这里写图片描述

题解

题目描述有点长,看起来比较吓人,但题意还是比较简单的。
对于前60%的数据,直接对忌讳词语建AC自动机,然后在机子上跑记忆化搜索或递推就可以了。
本题主要难点在后40%,尤其是最后20%的数据上。我们可以把AC自动机看成一张图,在字符串后接上某基本词语就相当于从图上一点走一条路径到达另一点。观察数据的特殊性——基本词语长度为1或2且 L L 很大。如果所有基本词语长度都只有1,那么在图上的转移路径就只会经过一条边,题目问题就转化为了已知图上两点间有一条边,求从起点(AC自动机根节点)出发走L条边后在任意点停下的方案数——经典的矩阵乘法问题!要处理禁忌词汇,只需提前预处理出所有路径的起止点,当终点为某禁忌词汇的末尾时,删去该路径即可。对于基本词语长度为2的情况,我们需要构造虚拟点在原路径的起止点间做中转,相当于起点到终点需走两步(经过两条边),注意最后计算结果时不能包含虚拟点!
为什么前60%的数据不能用后40%的方法?基础词汇太长->虚拟点太多->时空复杂度爆炸!
详见代码……

代码

#include<stdio.h>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<queue>
using namespace std;
typedef long long ll;
typedef ll arr[205][205];
const ll mod=1e9+7;
ll n,m,l,tot,val[105],fail[105],trie[105][30],G[105][55];
ll N,ans,f[105][105];
arr I,A;
char c[55][105],tab[105];
queue<ll> q;
namespace ACAM
{
    void Insert()
    {
        ll p=0,len=strlen(tab+1);
        for(ll i=1;i<=len;i++)
        {
            ll t=tab[i]-'a';
            if(!trie[p][t]) trie[p][t]=++tot;
            p=trie[p][t];
        }
        val[p]++;
    }
    void Getfail()
    {
        for(ll i=0;i<26;i++)
            if(trie[0][i]) q.push(trie[0][i]);
        while(!q.empty())
        {
            ll x=q.front();q.pop();
            for(ll i=0;i<26;i++)
            {
                ll son=trie[x][i];
                if(!son)
                {
                    trie[x][i]=trie[fail[x]][i];
                    continue;
                }
                q.push(son);
                ll y=fail[x];
                while(y&&!trie[y][i]) y=fail[y];
                fail[son]=trie[y][i],val[son]|=val[fail[son]];
            }
        }
    }
    ll Goto(ll p,ll x)
    {
        ll len=strlen(c[x]+1);
        for(ll i=1;i<=len;i++)
            if(!val[trie[p][c[x][i]-'a']]) p=trie[p][c[x][i]-'a'];
            else return -1;
        return p;
    }
    ll DFS(ll p,ll step)
    {
        if(step==l) return 1;
        if(~f[p][step]) return f[p][step];
        f[p][step]=0;
        for(ll i=1;i<=n;i++)
            if((~G[p][i])&&step+strlen(c[i]+1)<=l)
                f[p][step]=(f[p][step]+DFS(G[p][i],step+strlen(c[i]+1)))%mod;
        return f[p][step];
    }
}
namespace matrix
{
    void mul(arr y,arr x)
    {
        arr z;
        memset(z,0,sizeof(z));
        for(ll i=0;i<=N;i++)
            for(ll j=0;j<=N;j++)
                for(ll k=0;k<=N;k++)
                    z[i][j]=(z[i][j]+y[i][k]*x[k][j]%mod)%mod;
        memcpy(y,z,sizeof(z));
    }
    void ksm(ll b)
    {
        for(ll i=0;i<=N;i++) I[i][i]=1;
        while(b)
        {
            if(b&1) mul(I,A);
            b>>=1;
            mul(A,A);
        }
    }
}
int main()
{
    scanf("%lld%lld%lld",&n,&m,&l);
    for(ll i=1;i<=n;i++) scanf("%s",c[i]+1);
    for(ll i=1;i<=m;i++) scanf("%s",tab+1),ACAM::Insert();
    ACAM::Getfail();
    for(ll i=0;i<=tot;i++)
        for(ll j=1;j<=n;j++) G[i][j]=ACAM::Goto(i,j);
    if(l<=100)
    {
        memset(f,-1,sizeof(f));
        ACAM::DFS(0,0);
        printf("%lld\n",f[0][0]);
    }
    else
    {
        N=2*tot+1;
        for(ll i=0;i<=tot;A[i][i+tot+1]++,i++)
            for(ll j=1;j<=n;j++)
                if(~G[i][j])
                    if(strlen(c[j]+1)==1) A[i][G[i][j]]++;
                    else A[i+tot+1][G[i][j]]++;
        matrix::ksm(l);
        for(ll i=0;i<=tot;i++) ans=(ans+I[0][i])%mod;
        printf("%lld\n",ans);
    }
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值