AC自动机上的dp

有些时候,我们会碰到这样的一类问题:给定n个字符串,求长度为m的(不)包含这n个字符串的字符串个数。即字符串匹配的计数问题。
我们先来看这样一道题:

例题一

【poj 2778】给出m个疾病基因片段(m<=10),每个片段不超过10个字符。求长度为n的不包含任何一个疾病基因片段的DNA序列共有多少种?(n<=2000000000)
【分析】

part1:

我们先考虑一下朴素的dp:设dp(i,j)dp(i,j)dp(i,j)表示用了i个字符,拼出了符合题意的字符串j的方案数。则dp(i,j)=∑j′⊆jdp(i−1,j′)dp(i, j)=\sum_{j&#x27; \subseteq j}{dp(i-1,j&#x27;)}dp(i,j)=jjdp(i1,j)
这个想法不得不说着实不好

part2:

考虑使用AC自动机。则原问题转化为:求使这m个字符串都匹配不上的字符串的个数。所以,匹配过程中经过的点,其fail链上的节点(包括本身)都不能有结束节点。
按照这个想法,我们修改一下状态:设dp(i,j)dp(i,j)dp(i,j)表示从根节点开始走i步到j号节点的方案数(路径合法),则:
dp(i,j)=∑!t[k].flagdp(i−1,k)(t[k].flag表示k号节点的fail链上是否有结束节点)dp(i,j)=\sum_{!t[k].flag}{dp(i-1,k)}(t[k].flag表示k号节点的fail链上是否有结束节点)dp(i,j)=!t[k].flagdp(i1,k)t[k].flagkfail
当n的范围较小时,这个算法足以解决问题(比方说下面的那道题)。但发现n的范围……GG

part3:

再仔细分析一下。如果我们刚拿到AC自动机,可以得出的是到了节点i,若打算匹配字符x将会跳转到节点j。于是,我们如果把这个关系看成一个图,则对于每一个这样的关系,给i和j连边,得到了一个邻接矩阵G。
不难发现,G[i][j]G[i][j]G[i][j]表示
从i走一步到j的方案数

联想到矩阵乘法:
c[i][j]=∑k=1na[i][k]∗b[k][j]c[i][j]=\sum_{k=1}^na[i][k]*b[k][j]c[i][j]=k=1na[i][k]b[k][j]
根据乘法原理+加法原理,若将G[i][j]G[i][j]G[i][j]平方,得到的新矩阵则表示从i走两步到j的方案数
推广一下:Gn[i][j]G^n[i][j]Gn[i][j]表示从i走n步到j的方案数
于是我们用矩阵快速幂求得G的n次方G’,则有:
ans=∑i=0num∣矩阵大小G′[0][i]ans = \sum_{i=0}^{num|矩阵大小}G&#x27;[0][i]ans=i=0numG[0][i]
至此,本题完美解决
【代码】

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#define ll long long
using namespace std;
const int mm = 15, mod = 100000;
int cnt;
struct trie
{
    int son[4], fail;
    bool flag;
} t[105];
struct matrix
{
    ll a[105][105];
    matrix(){memset(a, 0, sizeof a);}
} ret, h;
matrix operator *(const matrix &a, const matrix &b)
{
    matrix ret;
    for(int i = 0; i <= cnt; i++)
        for(int j = 0; j <= cnt; j++)
            for(int k = 0; k <= cnt; k++)
                ret.a[i][j] += (a.a[i][k] * b.a[k][j]) % mod, ret.a[i][j] %= mod;
    return ret;
}
queue<int > q;
bool vis[10005];
char s[mm];
void ksm(int b)
{
    while(b)
    {
        if(b & 1)
            ret = ret * h;
        h = h * h, b >>= 1;
    }
}
inline int getnum(char c)
{
    if(c == 'A')
        return 0;
    else if(c == 'C')
        return 1;
    else if(c == 'G')
        return 2;
    else
        return 3;
}
void push()
{
    int n = strlen(s), r = 0;
    for(int i = 0; i < n; i++)
    {
        int v = getnum(s[i]);
        if(!t[r].son[v])
            t[r].son[v] = ++cnt;
        r = t[r].son[v];
    }
    t[r].flag = 1;
}
void get_fail()
{
    q.push(0);
    while(!q.empty())
    {
        int s = q.front();
        q.pop();
        for(int i = 0; i < 4; i++)
        {
            int to = t[s].son[i];
            if(to)
                q.push(to);
            if(!s)
            {
                if(to)
                    t[to].fail = 0;
                else t[s].son[i] = 0;
            }
            else
            {
                if(to)
                    t[to].fail = t[t[s].fail].son[i], t[to].flag|=t[t[to].fail].flag;
                else
                    t[s].son[i] = t[t[s].fail].son[i];
            }
            to = t[s].son[i];
            if(!t[to].flag)
                h.a[s][to]++;
             //对于修改儿子节点的分析见例题二
        }
    }
}
int main()
{
    int n, m, i;
    scanf("%d%d", &m, &n);
    while(m--)
        scanf("%s", s), push();
    get_fail();
    for(i = 0; i <= cnt; i++)
        ret.a[i][i] = 1;
    ksm(n);
    ll ans = 0;
    for(i = 0; i <= cnt; i++)
        ans += ret.a[0][i], ans %= mod;
    printf("%lld\n", ans);
}
例题2

【bzoj1030】题意:JSOI交给队员ZYX一个任务,编制一个称之为“文本生成器”的电脑软件:该软件的使用者是一些低幼人群,
他们现在使用的是GW文本生成器v6版。该软件可以随机生成一些文章―――总是生成一篇长度固定且完全随机的文
章—— 也就是说,生成的文章中每个字节都是完全随机的。如果一篇文章中至少包含使用者们了解的一个单词,
那么我们说这篇文章是可读的(我们称文章a包含单词b,当且仅当单词b是文章a的子串)。但是,即使按照这样的
标准,使用者现在使用的GW文本生成器v6版所生成的文章也是几乎完全不可读的?。ZYX需要指出GW文本生成器 v6
生成的所有文本中可读文本的数量,以便能够成功获得v7更新版。你能帮助他吗?

Input
  输入文件的第一行包含两个正整数,分别是使用者了解的单词总数N (<= 60),GW文本生成器 v6生成的文本固
定长度M;以下N行,每一行包含一个使用者了解的单词。这里所有单词及文本的长度不会超过100,并且只可能包
含英文大写字母A…Z

Output
  一个整数,表示可能的文章总数。只需要知道结果模10007的值。
【分析】
正面处理有些困难,所以我们转而求不合法的方案数。
那么好像就跟上面那个题一样了
如果按照part3的方法做的话,发现连一次矩阵乘法都做不完,还会炸空间
此时我们就可以按照part2的方法来做:

dp(i,j)dp(i,j)dp(i,j)表示从根节点开始走i步到j号节点的方案数(路径合法),则:
dp(i,j)=∑!t[k].flagdp(i−1,k)(t[k].flag表示k号节点的fail链上是否有结束节点)dp(i,j)=\sum_{!t[k].flag}{dp(i-1,k)}(t[k].flag表示k号节点的fail链上是否有结束节点)dp(i,j)=!t[k].flagdp(i1,k)t[k].flagkfail

【代码】

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int mn = 6005, mod = 10007;
struct trie
{
    int son[26], fail;
    bool flag;
}t[mn];
queue<int >q;
int f[mn][mn], cnt;
char s[mn];
void push()
{
    int n = strlen(s), from = 0;
    for(int i = 0; i < n; i++)
    {
        int to = s[i] - 'A';
        if(!t[from].son[to])
            t[from].son[to] = ++cnt;
        from = t[from].son[to];
    }
    t[from].flag = 1;
}
void init()
{
    for(int i = 0; i < 26; i++)
        if(t[0].son[i])
            q.push(t[0].son[i]);
    while(!q.empty())
    {
        int from = q.front(); q.pop();
        for(int i = 0; i < 26; i++)
        {
            int to = t[from].son[i];
            if(to)
                t[to].fail = t[t[from].fail].son[i], q.push(to), t[to].flag |= t[t[to].fail].flag;
            else
                t[from].son[i] = t[t[from].fail].son[i];
            //由于对儿子节点的修改,son[i]的含义发生了改变。它不再表示儿子节点的位置,而是表示从该节点开始走一步到达的节点位置
        }
    }
}
int ksm(int a, int b)
{
    int ret = 1, h = a;
    while(b)
    {
        if(b & 1)
            ret *= h, ret %= mod;
        h *= h, h %= mod, b >>= 1;
    }
    return ret;
}
int main()
{
    int n, m, i, j, k;
    scanf("%d%d", &n, &m);
    for(i = 1; i <= n; i++)
        scanf("%s", s), push();
    init(), f[0][0] = 1;
    for(i = 1; i <= m; i++)
        for(j = 0; j <= cnt; j++)
            if(!t[j].flag)
                for(k = 0; k < 26; k++)
                    f[i][t[j].son[k]] += f[i - 1][j], f[i][t[j].son[k]] %= mod;
    int ans = 0;
    for(i = 0; i <= cnt; i++)
        if(!t[i].flag)
            ans += f[m][i], ans %= mod;
    printf("%d\n", ((ksm(26, m) - ans) % mod + mod) % mod);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值