【BZOJ2553】禁忌,AC自动机+期望DP+矩乘

传送门
先考虑选最多禁忌串的问题
感受一下,如果禁忌串之间没有包含关系,一定是可以从前往后贪心搞的,直接建AC自动机跑匹配,找到一个禁忌串的末尾就回到根上,并把禁忌串数量+1
(所以起初我想的是把包含其他禁忌串的禁忌串去掉再建AC自动机,不过后来发现其实直接跑就可以了,因为顺着跑下去,先到达的末尾节点一定是被包含的禁忌串,包含的禁忌串是跑不到的)
这就是一种经典的AC自动机DP了,每个节点继承fail的状态,然后 f[i][j][k] 表示串长为 i ,跑到AC自动机的第j个节点,当前禁忌串共有 k 个的方案数,枚举字符转移即可,复杂度O(len|T|lenminTi),可以得70分
看到 len109 ,想想能不能加速转移(矩阵乘法?卷积?推公式?)
可是即使能够快速转移,我们计算的是期望,如果依照这种思路做,最后除以总方案数根本没法算,这怎么办呢?
长度为len的字符串中包含的禁忌串数量”的期望等价于“长度为len的字符串经过AC自动机末尾节点的次数”的期望
因为末尾节点独立,根据期望的线性性质,它等于“经过各末尾节点次数”的期望之和
这样我们就把问题转移到考虑每个末尾节点的贡献上
……
然后就不会做了:(,并不是很懂期望的那一套理论,只会矩乘求概率爽爽
询问了红太阳mrazer,理解了一下期望的线性性质,得知只要倒着DP就可以了,这样思路就比较明显了, f[i][j] 表示,当前在 j 节点,走in步的期望经过次数,枚举下一个字符,然后考虑后继点就可以了;而且容易发现,这个东西可以矩乘了,转移矩阵中第i行第j列表示从j节点跳到i节点的概率
对于跳回根节点并数量+1的情况,转移矩阵再加一列常数列,如果能跳回根节点并数量+1,就为1,反之为0;初始矩阵的常数列设为1就可以了
时间复杂度 O(|T|3loglen)
总结一下,之前也做过一些期望的题目,但感觉根本连门都没入,像这道题目并没有很高的思维难度,也是一堆知识点的堆砌,最后没有想到倒着DP可以做期望,功亏一篑,很不应该,不过也恰恰反应了自身的薄弱,以后还是要加深思考,多做点综合应用和锻炼思维的题目,少写板子题

#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
int len,alp,n,root=1,cnt=1;
int trie[80][26],fail[80];
char s[17];
bool is_end[80];
queue<int>q;
struct matrix
{
    int r,c;
    long double a[80][80];
    void clr(){r=c=0;memset(a,0,sizeof(a));}
}ini,tra; 
matrix operator *(matrix A,matrix B)
{
    matrix C;
    C.clr();
    for (int k=1;k<=A.c;++k)
        for (int i=1;i<=A.r;++i)
            if (A.a[i][k]!=0)
                for (int j=1;j<=B.c;++j)
                    C.a[i][j]+=A.a[i][k]*B.a[k][j];
    C.r=A.r;C.c=B.c;
    return C;
}
void insert(char *s)
{
    int len=strlen(s),now=root;
    for (int i=0;i<len;++i)
    {
        if (!trie[now][s[i]-'a']) trie[now][s[i]-'a']=++cnt;
        now=trie[now][s[i]-'a'];
    }
    is_end[now]=1;
}
void build()
{
    int x,tmp;
    for (q.push(root);!q.empty();q.pop())
    {
        x=q.front();
        is_end[x]|=is_end[fail[x]];
        for (int i=0;i<alp;++i)
        if (trie[x][i])
        {
            tmp=fail[x];
            while (tmp&&!trie[tmp][i]) tmp=fail[tmp];
            if (tmp&&x!=root) fail[trie[x][i]]=trie[tmp][i];
            else fail[trie[x][i]]=root;
            q.push(trie[x][i]);
        }
    }
}
main()
{
    scanf("%d%d%d",&n,&len,&alp);
    for (int i=1;i<=n;++i)
        scanf("%s",s),
        insert(s);
    build();
    ini.r=1;ini.c=cnt+1; 
    ini.a[1][cnt+1]=1;
    tra.r=cnt+1;tra.c=cnt+1;
    tra.a[cnt+1][cnt+1]=1;
    for (int i=1;i<=cnt;++i)
    if (!is_end[i])
        for (int tmp,j=0;j<alp;++j)
        {
            tmp=i;
            while (tmp&&!trie[tmp][j]) tmp=fail[tmp];
            if (tmp)
            {
                if (!is_end[trie[tmp][j]]) tra.a[trie[tmp][j]][i]+=1.0/alp;
                else tra.a[root][i]+=1.0/alp,tra.a[cnt+1][i]+=1.0/alp;
            }
            else
                tra.a[root][i]+=1.0/alp;
        }
    for (;len;len>>=1,tra=tra*tra)
        if (len&1) ini=ini*tra;
    printf("%.10lf",(double)ini.a[1][1]);
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值