AcWing 1052. 设计密码


题目传送门

一、思路

  • 状态机

  • 动态规划

  • 问题
    为什么这样的状态表示是可行的呢?
    因为\(S\)数组中的第\(n\)位有\(26\)个小写字母,匹配在\(T\)中的位置一定存在(因为不匹配,匹配到的位置是\(0\)),所以把所有\(f[n][0 \sim m-1]\)加起来即为总方案数。

二、原始版本

#include 

using namespace std;
const int N = 55;
const int mod = 1e9 + 7;

int n;          //n个长度的密码串
int m;          //模板串的长度
int ne[N];      //kmp的ne数组
char p[N];      //模板串
int f[N][N];    //f[i][j]表示密码已经生成了i位,并且第i位匹配到模板串中位置为j时的方案数,这是方案数互相依赖相加的准确依据

int main() {
    //构建的密码长度n
    cin >> n >> (p + 1);//模板串p,模板串的下标是从1开始的
    //计算模板串s的字符串长度
    m = strlen(p + 1);

    //kmp求ne数组,模板代码
    for (int i = 2, j = 0; i <= m; i++) {
        while (j && p[i] != p[j + 1]) j = ne[j];
        if (p[i] == p[j + 1]) j++;
        ne[i] = j;
    }

    //已经匹配了0位,且匹配的子串的位置是0时的方案数为1;(初始化)
    f[0][0] = 1;

    for (int i = 0; i < n; i++)//枚举密码串的每一位,这是一个DP打表的过程,所以从小到大遍历每一位
        for (int j = 0; j < m; j++)//根据状态定义,需要枚举的第二维就是模板串的每一个位置
            //j表示第i位密码匹配到的位置,因为不能包含子串,所以不能匹配到m这个位置
            //认为前面i,j已经完成匹配的情况下,讨论密码串第i+1位的26种可能
            for (char k = 'a'; k <= 'z'; k++) {
                //在s[i+1]=k的时候,模板串需要跳到哪个位置?
                int u = j;
                while (u && k != p[u + 1]) u = ne[u];
                if (k == p[u + 1]) u++;
                //[i+1,u]这个状态是可以被[i,j]转化而来的
                if (u < m) f[i + 1][u] = (f[i + 1][u] + f[i][j]) % mod;
            }
    int res = 0;
    for (int i = 0; i < m; i++) res = (res + f[n][i]) % mod;
    //将所有的方案数加起来即为总方案数
    printf("%d", res);
    return 0;
}

优化一维复杂度降低到n^2
将建图部分抽取出来即可。。。!
时间复杂度:O(26n2)

三、优化版本

#include 

using namespace std;

const int N = 55;
const int mod = 1e9 + 7;

int n;          //n个长度的密码串
int m;          //模板串的长度
int ne[N];      //kmp的ne数组
char p[N];      //模板串
int f[N][N];    //f[i][j]表示密码已经生成了i位,并且第i位匹配到模板串中位置为j时的方案数,这是方案数互相依赖相加的准确依据
int g[N][26];   //


int main() {
    //构建的密码长度n
    cin >> n >> (p + 1);//模板串p,模板串的下标是从1开始的
    //计算模板串s的字符串长度
    m = strlen(p + 1);

    //kmp求ne数组,模板代码
    for (int i = 2, j = 0; i <= m; i++) {
        while (j && p[i] != p[j + 1]) j = ne[j];
        if (p[i] == p[j + 1]) j++;
        ne[i] = j;
    }

    // 预处理(可以优化一下,有重复的不用重新计算)
    for (int j = 0; j < m; j++)
        for (int k = 'a'; k <= 'z'; k++) {
            int u = j;
            while (u && p[u + 1] != k) u = ne[u];
            if (p[u + 1] == k) u++;
            //记录下这个数据
            g[j][k - 'a'] = u;
        }
    // 状态计算
    f[0][0] = 1;
    for (int i = 0; i < n; i++)//枚举密码串的每一位,这是一个DP打表的过程,所以从小到大遍历每一位
        for (int j = 0; j <= m; j++)//根据状态定义,需要枚举的第二维就是模板串的每一个位置
            //j表示第i位密码匹配到的位置,因为不能包含子串,所以不能匹配到m这个位置
            //认为前面i,j已经完成匹配的情况下,讨论密码串第i+1位的26种可能
            for (char k = 'a'; k <= 'z'; k++) {
                //模板串跳到哪个位置?
                //模板串跳到哪个位置,与两个因素有关,1:j现在所在的位置,2:遇到的s[i+1]是什么,即k
                int u = g[j][k - 'a'];
                //状态转移
                if (u < m) f[i + 1][u] = (f[i + 1][u] + f[i][j]) % mod;
            }

    int res = 0;
    for (int i = 0; i < m; i++) res = (res + f[n][i]) % mod;
    //将所有的方案数加起来即为总方案数
    printf("%d", res);
    return 0;
}