AcWing
  • 首页
  • 课程
  • 题库
  • 更多
    • 竞赛
    • 题解
    • 分享
    • 问答
    • 应用
    • 校园
  • 关闭
    历史记录
    清除记录
    猜你想搜
    AcWing热点
  • App
  • 登录/注册

AcWing 1052. 设计密码【线性DP+KMP自动机模型】    原题链接    中等

作者: 作者的头像   一只野生彩色铅笔 ,  2021-07-05 22:22:33 ,  所有人可见 ,  阅读 6044


138


30

最近在补全提高课所有题目的题解,宣传一下汇总的地方提高课题解补全计划

题目描述

给定一个字符串 $T$,和一个整数 $N$

现需设计一个密码 $S$, $S$ 需满足:

  1. $S$ 的长度是 $N$
  2. $S$ 只包含小写英文字母
  3. $S$ 不包含子串 $T$

求该密码的 方案数,答案对 $10^9+7$ 取模

分析

首先读者需要了解一部分自动机的概念,我推荐一下OI-Wiki的这篇博客 自动机 - OI Wiki

了解自动机是学字符串的基础,之后学习 AC自动机、 KMP 、 后缀自动机SAM 都有很大的帮助

求方案数 的题目难免不会让人想到用 动态规划 来求解

设计一个 合法的密码,从前往后 线性构造 是一个不错的枚举方案

因为如果 当前设计的密码 是合法的话,在他后面添上一个 新字符,可能会导致 新密码不合法 的情况,只有可能是 新密码 的 后缀子串 中出现了 $T$

通过 线性构造 的方法,我们只需判断 后缀子串 中是否出现 $T$ 即可

那么如果我们想用线性DP来求解,应该如何记录当前的状态呢?

首先,毫无疑问是用当前构造的密码长度 $i$ 作为 线性DP 的阶段

可是如何记录当前的长度为 $i$ 的字符串的 后缀 与 字符串 $T$ 的匹配情况呢?

关于 单母串 和 单模式串 的 在线匹配,会自然而然的想到一个 字符串算法 —— 前缀函数与KMP

借用 KMP 的思想,我们可以用参数 $j$ 来表示当前 母串后缀 与 模式串 匹配的 最大长度

KMP 求前缀函数的 在线性 ,使得我们可以任意枚举第 $i+1$ 个字符后,快速获得其 前缀函数值

该 前缀函数值 也就是 $i+1$ 状态对应的新的 $j$ 的值 $\delta(\pi(i),ch)$

于是我们就可以利用 $f_{i,j}$ 来更新 $f_{i+1,\delta(\pi(i),ch)}$ 的状态,具体看下面的分析:

自动机模型分析

本题对于不同的 模式串,自动机模型 不同,因此我以 模式串 ababc 举例

IMG_F9DF9121A324-1.jpeg

先看上面的转移

IMG_85E8DEFEB425-1.jpeg

该转移表示 $s_{i+1} = ch$ 而发生的转移,也就是枚举的字符 $ch$ 刚好等于 模式串 的第 $i$ 个字符

于是就会使 最大长度 增加 $1$,即 $f_{i+1,j+1} ~+=~ f_{i,j}$

然后我再具体分析一下这个状态机里的 $f_{i,3}$ 状态(其他状态可以类比)

我们枚举下一个添加的字符(即枚举 $s_{i+1}$ 的字符 $ch$)时:

IMG_095AE9A05B7B-1.jpeg

根据如下转移函数:

$$ \delta(i,ch) = \begin{cases} i+1 &s_{i+1}=ch \\\ \delta(\pi(i),ch) &s_{i+1}\ne ch \text{^} i > 0 \\\ 0 &s_{i+1}\ne ch \text{^} i = 0\\\ \end{cases} $$

对应于本样例就是:

  1. $ch$ 是 $b$,则最大长度增加一,对应到 $f_{i+1,j+1}$
  2. $ch$ 不是 $b$,则根据自动机跳到位置 $p$,如果 $s_{p+1} = ch$ 则最大长度增加一,对应到 $f_{i+1,p+1}$
  3. $ch$ 不是 $b$,则根据自动机跳到位置 $0$,如果 $s_{1} \ne ch$ 则最大长度为0,对应到 $f_{i+1,0}$

闫氏DP分析法

状态表示—集合$f_{i,j}$: 考虑构造一个 长度 为 $i$ 的密码,且后缀与模式串匹配的 最大长度 为 $j$ 的方案

状态表示—属性$f_{i,j}$: 方案的总数 $Sum$

状态计算$f_{i,j}$:

$$ f_{i+1,\delta(\pi(j),ch)} ~+=~ f_{i,j} \quad(其中 a\le ch\le z) $$

Code

时间复杂度: $O(26N^3)$

初始状态: $f_{0,0}$

目标状态: $f_{n,j} \quad 其中j\in[0,M)$

#include <iostream>
#include <cstring>

using namespace std;

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

int n, m;
char s[N];
int f[N][N], ne[N];

int main()
{
    cin >> n >> s + 1;
    m = strlen(s + 1);
    //KMP预处理前缀表
    for (int i = 2, j = 0; i <= m; ++ i)
    {
        while (j && s[i] != s[j + 1]) j = ne[j];
        if (s[i] == s[j + 1]) ++ j;
        ne[i] = j;
    }
    //DP
    f[0][0] = 1;
    for (int i = 0; i < n; ++ i)
    {
        for (int j = 0; j < m; ++ j)
        {
            for (char ch = 'a'; ch <= 'z'; ++ ch)   //枚举第i+1个字符
            {
                int ptr = j;    //计算枚举到第i+1个字符后,后缀匹配的最大长度
                while (ptr && s[ptr + 1] != ch) ptr = ne[ptr];
                if (s[ptr + 1] == ch) ++ ptr;
                f[i + 1][ptr] = (f[i + 1][ptr] + f[i][j]) % mod;    //更新i+1的状态
            }
        }
    }
    //统计所有目标状态
    int res = 0;
    for (int j = 0; j < m; ++ j) res = (res + f[n][j]) % mod;
    cout << res << endl;
    return 0;
}

40 评论


用户头像
Perf   2021-09-02 23:06      8    踩      回复

回过头来复习这道题目的时候发现其实第二层循环的作用巧妙在于它可以借助next数组表示当前构造串和不可匹配子串的匹配程度(字符串匹配状态机的状态)。如果换一种方式来枚举的话(比如第二层循环枚举前一个字母是什么或者其他)就会导致无法直接表示与不可匹配子串的匹配程度(每一个字母都有可能对应不可匹配子串上的很多状态)。这样想应该没有错吧hh
ps:彩铅巨巨的题解合集对于复习的同学来说也是很赞的资源

用户头像
一只野生彩色铅笔   2021-09-02 23:07      9    踩      回复

解释的很棒👍,也谢谢支持hh

用户头像
utt   2023-05-10 23:29         踩      回复

还是没完全理解第二层循环的枚举方式:对于当前第i个字符, [0,m)这些状态中,不是每一个都可以达到吧?然后再基于某个不能到达的状态,去计算i+1的状态?(虽然有可能回退到0

用户头像
utt   2023-05-10 23:44         踩      回复

或者说可以这样理解:对于前i个字符,最长前后缀长度在[0,m)这个区间的每一个值,一定可以构造出某种密码串使其满足

用户头像
糖豆   2023-05-22 16:49    回复了 utt 的评论         踩      回复

好问题!我想和楼主说一下我的理解:
状态表示是f[i][j],也就是一个二维的DP表,我们的任务是填表,那么按代码的意思是从上到下从左到右填充,也就是每一行代表构造S串的第i位,每一列代表构造P串的第j位,但由于j<i,所以,并不是一个完整的二维表,而是一个左下角为直角的直角三角形填表。
正序枚举填充,会保证每个位置都有正确的数值,也就大可不必担心所依赖的单元格中没有正确填充。

回到现实意义中来,因为可以填充所有小写字母, 没有什么限制,也就保证了最长公共前后缀肯定会被构造出来、

用户头像
关静小朋友   2023-08-16 12:06      3    踩      回复

感觉就是填表法和刷表法的区别吧,填表法要找前驱状态,而刷表法就是用当前状态转移到其他状态
因为next数组只知道下一步要跳到哪些位置(j可以转移到的位置),而不知道哪些位置可以跳到这个位置(前驱状态)
所以这道题枚举下一个字母会更好一些,因为只要给出下一个字母是什么,我就可以用next数组求出来,当前的j跳到哪些位置。


用户头像
J2_1   2024-02-11 22:04      1    踩      回复

tql 大佬写的题解就是牛逼 ORZ


用户头像
黑黑的憨憨   2023-08-13 23:25      1    踩      回复

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%大佬,催更,跪了orz


用户头像
wh2011   2023-08-06 16:46      1    踩      回复

orz


用户头像
wh2011   2023-08-06 16:46      1    踩      回复

%%%%%


用户头像
frank2023   2022-07-23 08:42      1    踩      回复

这个题解牛逼坏了


用户头像
tonngw   2022-05-28 22:14      1    踩      回复

大佬,请教一下图用什么软件画的呀

用户头像
incra   2022-11-01 16:30      1    踩      回复

同问


用户头像
思念聚成海   2021-12-05 20:27      1    踩      回复

你是铅笔,我是菜笔


用户头像
lzy2002   2021-07-24 20:13      1    踩      回复

ptr等于m时候不会加到答案里吗?

用户头像
一只野生彩色铅笔   2021-07-26 16:09      1    踩      回复

这里可以理解为先用dp把所有状态搜出来

最后再去统计的目标状态,在目标状态统计里,没有去枚举 $ptr=m$ 的情况


用户头像
Tilbur   2021-07-05 22:34      1    踩      回复

我懂了

用户头像
一只野生彩色铅笔   2021-07-05 22:37      2    踩      回复

TQL光头哥

用户头像
qwwjh   2021-10-28 17:06         踩      回复

tql光头哥


用户头像
lightmon5210   2023-05-17 23:24         踩      回复

ne[2]不是固定是1吗


用户头像
Zcras   2023-03-08 17:26         踩      回复

‘’后缀与模式串匹配的最大长度’‘请问这里为什么是最大长度啊qwq,没太理解这一点

用户头像
不想要WAaa   2023-12-07 18:57         踩      回复

你在预处理模式串的时候j不仅代表了不能匹配时要退回的位置,也代表了你前面完成匹配的子串的长度,所以只要能够继续匹配就会一直j++,当你不能匹配退回了你的j就需要更新为ne[j],所以你的j的大小也代表了当前能够匹配的最大长度


用户头像
RwChen   2022-11-10 10:19         踩      回复
假设不能构造出
abcabd
虽然你构造出了abcabc满足了条件
但你继续构造时任然要注意后面的abcabc后面的abc在继续构造时仍可能成为abcabd

用户头像
夜语声烦   2022-08-05 22:32         踩      回复

为啥不限制 if(u < m) 也一样能过,是数据太弱吗?

用户头像
糖豆   2022-08-07 11:49         踩      回复

那个不是限制,是优化,没用的不计算而已。你看一下最终的统计办法:

 for (int i = 0; i < m; i++) res = (res + f[n][i]) % MOD;

对于>=m的$f[n][i]$是不参与计数的,上面你统计不统计都没用,加上判断后就不计算了,因为最终也不统计。加上特判会快$2ms$,仅此而已。


用户头像
呆呆兽   2022-04-29 16:47         踩      回复

数学专业出身的题解就是严谨


用户头像
kuan525   2022-03-12 03:54         踩      回复

请教一下大佬,上面说是有重边的情况我能理解(多个点指向一个点),但是要是一个点可以向前跳到多个点呢,这种情况不用更新所有吗,例如给一个例子abcabcabcddd,前面有三个abc,在遍历到第三个abc的时候为什么只跳到第二个abc而不更新第一个abc呢,求解

用户头像
一只野生彩色铅笔   2022-03-12 11:36      2    踩      回复

你这个问题属于没有理解什么是 KMP,可以先去看一下 OI-WiKi 上对于 KMP 的解释和证明

同学不要急于求成,基础不稳地动山摇

用户头像
kuan525   2022-03-12 14:12    回复了 一只野生彩色铅笔 的评论         踩      回复

%%%%,谢谢佬了!


用户头像
海盐饼干   2021-08-19 14:42         踩      回复

%%%%%%%%%%%%%


用户头像
WAK   2021-07-31 10:53         踩      回复

请教一下,kmp模拟的第二重循环这里什么意思呀,是代表拿模板的第j个字母来匹配么?

用户头像
一只野生彩色铅笔   2021-07-31 11:01      1    踩      回复

以自动机中的状态j为当前状态,枚举下一个字母,然后拼成的新子串和母串匹配

然后根据前缀函数到达他的新状态ptr

用户头像
WAK   2021-07-31 23:04    回复了 一只野生彩色铅笔 的评论         踩      回复

跳的是匹配串吧,不是原串。

用户头像
一只野生彩色铅笔   2021-07-31 23:12    回复了 WAK 的评论      1    踩      回复

KMP中调的是匹配串里的指针

本题里是根据前缀函数找状态

差不多一个意思


用户头像
WAWA鱼   2021-07-27 11:22         踩      回复

tql,必须顶上去

用户头像
一只野生彩色铅笔   2021-07-27 12:45         踩      回复

谢谢支持w


用户头像
Peter_5   2021-07-05 22:51         踩      回复

这题当初觉得太难就跳过了,以后就可以看彩铅巨巨的题解了(´▽`)ノ♡

用户头像
一只野生彩色铅笔   2021-07-05 22:55         踩      回复

配合y总食用更佳ww

用户头像
Peter_5   2021-07-05 22:57    回复了 一只野生彩色铅笔 的评论         踩      回复

(。・ω・。)ノ♡

用户头像
凌乱之风   2021-07-07 07:23         踩      回复

+1


App 内打开
你确定删除吗?
1024
x

© 2018-2025 AcWing 版权所有  |  京ICP备2021015969号-2
用户协议  |  隐私政策  |  常见问题  |  联系我们
AcWing
请输入登录信息
更多登录方式: 微信图标 qq图标 qq图标
请输入绑定的邮箱地址
请输入注册信息