AcWing 1052 设计密码

题目描述:

你现在需要设计一个密码 S,S需要满足:

  • S的长度是 N;
  • S只包含小写英文字母;
  • S不包含子串 T;

例如:abc和 abcde 是 abcde 的子串,abd 不是 abcde的子串。

请问共有多少种不同的密码满足要求?

由于答案会非常大,请输出答案模 10^9+7的余数。

输入格式

第一行输入整数N,表示密码的长度。

第二行输入字符串T,T中只包含小写字母。

输出格式

输出一个正整数,表示总方案数模 109+7后的结果。

数据范围

1≤N≤50,
1≤|T|≤N,|T|是T的长度。

输入样例1:

2
a

输出样例1:

625

输入样例2:

4
cbc

输出样例2:

456924

分析:

本题考查KMP算法+状态机,应该说单独的KMP或者状态机问题都不太难,但是结合到一起便不那么容易了。首先回顾下KMP算法,KMP算法我更习惯于邓公的思路,就不延续y总的kmp思路讲解了。之前写kmp的题解见AcWing 831 KMP字符串,详细介绍串匹配各种算法的文章见第十一章:串

while(i < m && j < n){
            if(T[i] == P[j]) i++,j++;
            else    i -= j - 1,j = 0;
}

蛮力串匹配的算法就是定义i指向文本串T,j指向模式串P,当二者指向的字符相等,则继续比对下一字符,i++,j++。否则,j从模式串的开头重新开始,i也从之前模式串第一个字符对应文本串的下一个字符开始比对。

while(i < m && j < n){
            if(j == -1 || T[i] == P[j]) i++,j++;
            else    j = ne[j];
    }

KMP算法对蛮力算法的修改就两点,第一点是失配时i不动,j不再回到模式串开头而是回到ne[j],另外当j = ne[j] = -1表示模式串退到了通配符哨兵,此时文本串指针i应该右移一位,j也应该从头比对,同样是i++,j++,与匹配时的动作一致。

void init_next(){
    ne[0] = -1;
    for(int i = 1;i <= n;i++){
        int t = ne[i - 1];
        while(t != -1 && P[i - 1] != P[t])   t = ne[t];
        ne[i] = t + 1;
    }
}

next数组的构造过程是先设哨兵结点ne[0] = -1,后面ne[1]是在第二个字符失配时显然j应该退到第一个字符,其他情况下,当串匹配失配于模式串j位置时,需要在j的真前缀P[0,j)中找到最长的真前缀使其与真后缀相等。而ne[j-1]可以已经给P[0,j-1)找到了一个以ne[j-1]-1为末尾的真前缀和以j-2为末尾的真后缀相等,只要p[j-1] == p[ne[j-1]],真前缀与真后缀相等的长度便可以继续延长一个单位了,否则只能继续寻找next[next[j-1]],直到找到相等的为止,找到的位置t是与j-1相等的位置,最坏情况是通配符-1,t的下一个位置就是在j处失配时j应该转移的位置,故ne[j] = t + 1。

下面分析KMP匹配过程与状态机的联系。先分析下失配时的动作,

上图先求出了P的next数组,匹配过程中第一次失配于c与a,于是j = next[2] = 0,移动j = 0,分析c依旧不等于a,继续移动j = -1,这是失配时j的移动过程,换而言之,当文本串遍历到i指向c的位置,按照之前比对的结果,模式串第一次是j = 2的位置与i比对,失配后j先跳到0,又跳到-1,本次比对结束,i++,j++。而成功的匹配是比对到文本串第i个字符,第一次是与模式串的最后一个字符c比对,发现相等了j便自增到了m,匹配结束。所以对本题而言,长度为n的文本串,每个位置的字符有26种可能,一共是26^n种可能,对于其中的任意一种,如果包含了模式串P,当且仅当在KMP算法中模式串指针j移动到了m。所以我们可以遍历下文本串,从开头遍历到末尾,遍历到第i个位置时,枚举a到z26种情况,如果在i的比对中j未走到m,则是合法方案,j走到了m,则不加上当前枚举到的字母的方案数。

即在枚举第i个元素时,根据第i个元素的26种情况,j会选择向左跳到每个位置,或者跳到j+1,就对应了上图这样一个状态机,第i个元素可以引出26条边,本题所求的不包含模式串的合法方案数就是在状态中走n步但是不走到j = m的走法数量。设f[i][j]表示比对到文本串第i个字符时模式串指针已经跳到j的方案数,这里的j是与i比对最开始的j。

为了方便处理边界,我们在处理文本串时下标从1开始,而处理模式串时仍从下标0开始,i = 0表示文本串为空的情况,j = 0与之比对显然失配,所以是一种合法的方案,故边界状态f[0][0] = 1,第一个字符就是空,模式串后面的字符自然不会与之比对,所以后面的状态都是不合法的,f[0][j] = 0,j > 0。下面考虑状态转移方程,f[i][j]表示走到第i步时刚好遇见的模式串位置是j,在比对过程中j最终会跳到t位置,然后在下次比对中i + 1的位置遇见的就是t + 1,所以到走到第i步遇见j的方案数会转移到走到i + 1步遇见t+1的方案数,只要t + 1小于m,就可以成功转移,我们令t++,从而得到状态转移方程为f[i+1][t] = f[i+1][t] + f[i][j],j从0到m-1。最终跳到了i = n的位置,将f[n][j]累加起来就是我们要求的合法方案数了。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 55,mod = 1e9 + 7;
char P[N];
int f[N][N],ne[N];
int n,m,t;
void init_next(){
    ne[0] = -1;
    for(int i = 1;i <= m;i++){
        int t = ne[i-1];
        while(t != -1 && P[i-1]!=P[t])  t = ne[t];
        ne[i] = t + 1;
    }
}
int main(){
    scanf("%d%s",&n,P);
    m = strlen(P);
    init_next();
    f[0][0] = 1;
    for(int i = 0;i < n;i++){
        for(int j = 0;j < m;j++){
            for(char k = 'a';k <= 'z';k++){
                t = j;
                while(t != -1 && P[t] != k) t = ne[t];//失配时t跳到ne[t]
                t++;
                if(t < m)    f[i+1][t] = (f[i+1][t] + f[i][j]) % mod;
            }
        }
    }
    int res = 0;
    for(int i = 0;i < m;i++)    res = (res + f[n][i]) % mod;
    printf("%d\n",res);
    return 0;
}

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值