【ACWing】1052. 设计密码

题目地址:

https://www.acwing.com/problem/content/1054/

你现在需要设计一个密码 S S S S S S需要满足: S S S的长度是 N N N S S S只包含小写英文字母; S S S不包含子串 T T T;例如: a b c abc abc a b c d e abcde abcde a b c d e abcde abcde的子串, a b d abd abd不是 a b c d e abcde abcde的子串。请问共有多少种不同的密码满足要求?由于答案会非常大,请输出答案模 1 0 9 + 7 10^9+7 109+7的余数。

输入格式:
第一行输入整数 N N N,表示密码的长度。第二行输入字符串 T T T T T T中只包含小写字母。

输出格式:
输出一个正整数,表示总方案数模 1 0 9 + 7 10^9+7 109+7后的结果。

数据范围:
1 ≤ N ≤ 50 1≤N≤50 1N50
1 ≤ ∣ T ∣ ≤ N 1≤|T|≤N 1TN ∣ T ∣ |T| T T T T的长度。

字符串匹配的过程可以用状态机来模拟。实际上KMP算法就是在模拟一种状态机,该状态机是依据模式串生成,并且可以接受以模式串为子串的任意字符串。即,我们先由模式串生成KMP里的next数组,接着用这个数组构造出一个有限状态自动机,使得这个DFA(Deterministic Finite Automaton,确定有限状态自动机的英文缩写)只接受以该模式串为子串的字符串。我们以字符串"aba"为模式串举例,具体来说如下:
1、先求出s = "aba"的next数组。参考https://blog.csdn.net/qq_46105170/article/details/113805346。这里的next数组可以是未优化版的也可以是优化版的,因为本质上来说,它们构造出的DFA识别的语言是一样的。
2、构造一个DFA,它有三个状态,分别是 0 , 1 , 2 , 3 0,1,2,3 0,1,2,3 0 0 0是初始状态, 3 3 3是接受状态(该DFA只有一个接受状态)。先构造边 δ ( 0 , a ) = 1 , δ ( 1 , b ) = 2 , δ ( 2 , a ) = 3 \delta(0,a)=1,\delta(1,b)=2,\delta(2,a)=3 δ(0,a)=1,δ(1,b)=2,δ(2,a)=3,这三个转移边是显然的,对应的情况是恰好存在子串"aba"。接下来考虑失配边,这个字符串的next数组是 n e = [ − 1 , 0 , 0 ] n_e=[-1,0,0] ne=[1,0,0],这个数组规定了如果在 s [ i ] s[i] s[i]存在失配,应该如何跳转。跳转规则如下:比如在 s [ 1 ] s[1] s[1] s [ 2 ] s[2] s[2]处失配的时候(对应的是当前状态在 1 1 1 2 2 2的时候,比如当前位于状态 1 1 1,然后读入了'a'字符,这样就发生了失配),比如当前读到的字符是'c',那么就回到状态 0 0 0,然后继续在状态 0 0 0处匹配'c'(也就是看一下 s [ 0 ] s[0] s[0]是否等于'c'),仍然不匹配,那么就跳到 n e [ 0 ] = − 1 n_e[0]=-1 ne[0]=1,这个 − 1 -1 1并不是一个真实状态,它是一个虚拟的状态,假想 s [ − 1 ] s[-1] s[1]是一个通配符,可以匹配任意字符,那么此时就匹配上了,沿着这条虚拟的边走到状态 0 0 0(所以我们可以特判一下状态 − 1 -1 1就行了,不需要真在程序里写这个状态);再比如,在状态 1 1 1的时候读到了字符'b',那么此时是匹配的,直接沿着匹配边走到下一个状态,也就是状态 2 2 2
3、总结一下该DFA的跳转规则。当当前位于状态 i i i,并且读入了字符 α \alpha α的时候,进行如下循环:只要当前处于的状态 j j j不是 − 1 -1 1,并且 s [ j ] s[j] s[j] α \alpha α不匹配,那么就跳转到 n e [ j ] n_e[j] ne[j]去,直到 j = − 1 j=-1 j=1或者 s [ j ] = α s[j]=\alpha s[j]=α为止,此时沿着匹配边走一步到状态 j + 1 j+1 j+1去。总结来说就是 δ ( i , α ) = j + 1 \delta(i,\alpha)=j+1 δ(i,α)=j+1

由于上述DFA接受某个字符串,当且仅当其以构造该DFA的模式串为子串。这样一来,要求不含 S S S为子串的字符串数量,相当于就是在问,从DFA的状态 0 0 0出发,跳转 N N N次,并且中途和终点没有跳到接受状态 l S l_S lS的路径个数。这可以用动态规划来做。设 f [ k ] [ p ] f[k][p] f[k][p]是跳 t t t步跳到状态 t t t的路径条数,那么可以按照跳到状态 t t t之前在哪儿来分类,则有: f [ k ] [ p ] = ∑ q → p f [ k − 1 ] [ q ] f[k][p]=\sum_{q\to p} f[k-1][q] f[k][p]=qpf[k1][q]初始条件 f [ 0 ] [ 0 ] = 1 f[0][0]=1 f[0][0]=1(因为初始状态就是状态 0 0 0)。最终答案就是: ∑ p = 0 l S − 1 f [ N ] [ p ] \sum_{p=0}^{l_S-1} f[N][p] p=0lS1f[N][p]即跳 N N N步没有跳到状态 l S l_S lS的路径条数。

由于不方便知道某个状态之前是哪个状态,我们可以用当前状态来更新未来状态,即可以用 f [ k − 1 ] [ q ] f[k-1][q] f[k1][q]来累加到 f [ k ] [ p ] f[k][p] f[k][p]上去。代码如下:

#include <iostream>
#include <string>
using namespace std;

const int N = 55, mod = 1e9 + 7;
int n;
string s;
int f[N][N];

// 求next数组
void build_next(string p, int ne[]) {
    
    
    ne[0] = -1;
    for (int i = 0, j = -1; i < s.size() - 1;)
        if (j < 0 || p[j] == p[i]) {
    
    
            i++;
            j++;
            ne[i] = p[i] != p[j] ? j : ne[j];
        } else j = ne[j];
}

int main() {
    
    
    cin >> n;
    cin >> s;

    int ne[s.size()];
    build_next(s, ne);

    f[0][0] = 1;
    for (int i = 1; i <= n; i++)
        for (int j = 0; j < s.size(); j++)
            for (char ch = 'a'; ch <= 'z'; ch++) {
    
    
                int u = j;
                while (u != -1 && ch != s[u]) u = ne[u];
                u++;
                // 状态s.size()是接受态,走到其的路径条数不用计算
                if (u < s.size()) f[i][u] = (f[i][u] + f[i - 1][j]) % mod;
            }

    int res = 0;
    for (int i = 0; i < s.size(); i++) res = (res + f[n][i]) % mod;

    cout << res << endl;

    return 0;
}

时间复杂度 O ( N l S 2 ) O(Nl_S^2) O(NlS2),空间 O ( N l S ) O(Nl_S) O(NlS)

猜你喜欢

转载自blog.csdn.net/qq_46105170/article/details/114429486