题目
题目可以在CSP官网中查看到哟!
算法思想
一看这一道题,很容易想到的就是用AC自动机进行字符串匹配,而最后要统计满足条件的合法密文的数量,又有AC自动机中的状态,因此,使用动态规划进行求解。接下来就是动态规划中的状态转移方程了。
首先,小编的另一篇博客AC自动机-详解AC自动机以及模板详细图解介绍了AC自动机算法以及提供了AC自动机算法的模板,不了解AC自动机算法的朋友可以查看那篇博客对AC自动机算法进行入门。
其次,就是有关动态规划的题目最令人头疼的事情了,就是对状态的定义以及状态转移方程的建立。通过对题目的解读、AC自动机中节点以及解密加密时页数的跳转,我们可以建立如下状态F[i][j][k],表示表示长度为 i,密文匹配位于AC自动机的结点 j,且正在查看解密密码本的第k页的明文个数。状态定义完之后,就要考虑转移方程了,假设当前进行到F[i][j][k],代表着长度为 i 的密文,跳转到AC自动机中 j 的节点,正在查看解密密码本第 k页时的满足条件的合法密文的页数,接下来枚举每一个词典中单词,用解密密码本对应出密文,借助AC自动机来判断加上这个单词后的明文对应的密文是否非法,合法则转移,不合法则不转移。如果合法,设 s 为词典中的枚举的单词,tj 为匹配AC自动机之后的节点序号,tk 为匹配 s 单词之后正在查看解密密码本的页数,则转移方程就是:
F [i + len[s]] [ tj ] [ tk ] = F [i + len[s]] [ tj ] [ tk ] + F [ i ] [ j ] [ k ],动态规划部分就是对F三维数组进行遍历,再遍历每一个词典中的单词,按照如上的转移方式进行转移即可。
要注意的是,如上动态规划中枚举的是词典中的单词,以及AC自动机的模板串也是词典中的单词,均是明文,合格的密文要求不包含词典中的任何一个串为子串。因此需要一个数组记载着加密(解密密码本的逆)的过程。
代码详解
1、输入输出、函数调用(主函数)
这一部分主要对输入、输出进行了处理,同时调用函数AC自动机的创建(insert(char *s))、调用函数AC自动机的Fail指针的创建(build())以及调用函数AC自动机匹配字符串以及动态规划(solve())。对题目进行分析,我们可以发现,解密密码本在求解过程中并不会直接用到。在求解的过程中,我们需要用到加密密码的过程(也就是从明文变成密文),因此,在这个部分中,在读入解密密码本的时候,我们直接利用数组Out[ i ][ j ]数组存储从明文到密文的数据。Out[ i ][ j ]数组代表着在第 i 页,明文 j + ‘a’ 对应着密文 Out[ i ][ j ] + ‘a’;同时对页数的跳转也是需要存储的,Next[ i ][ j ]数组代表着在第 i 页,遇见明文 j + ‘a’ 后跳转到第 Next[ i ][ j ] 页上。具体代码如下:
int main()
{
int i, j;
cnt = 0;
newnode(0);
cin >> n >> m; //输入n,m
for (i = 0; i < 26; i++) //输入解密密码本
{
for (j = 1; j <= n; j++) //n 页
{
char S[1010];
scanf("%s", S);
Out[j][S[0] - 'a'] = i; //提取出密文字母
int len = strlen(S);
int v = 0; //提取出对应的跳转的页数
if (len == 2) v = S[1] - '0';
else v = (S[1] - '0') * 10 + S[2] - '0';
Next[j][S[0] - 'a'] = v; //提取出密文对应的跳转的页数
}
}
K = 0;
while (scanf("%s", s[K]) != EOF) //输入词典
{
insert(s[K]);
Len[K] = strlen(s[K]); //AC自动机的模板串的建立
for (i = 0; i < Len[K]; i++)
s[K][i] = s[K][i] - 'a';
K++;
}
build(); //AC自动机的fail指针的建立
solve(); //求解过程,包括动态规划以及字符串匹配
return 0;
}
2、AC自动机模板串的建立
这一部分与我的另一篇博客AC自动机-详解AC自动机以及模板中AC自动机模板串的建立是一致的算法思想,具体代码如下:
void newnode(int x)
{
int i;
for (i = 0; i < 26; i++) ch[x][i] = 0;
fail[x] = 0;
mark[x] = false;
}
void insert(char *s)
{
int i, k;
int len = strlen(s);
int x = 0;
for (i = 0; i < len; i++)
{
k = s[i] - 'a';
if (ch[x][k] == 0)
{
cnt++;
ch[x][k] = cnt;
newnode(ch[x][k]);
}
x = ch[x][k];
}
mark[x] = true;
}
3、AC自动机的fail指针的建立
这一部分与我的另一篇博客AC自动机-详解AC自动机以及模板中AC自动机模板串的建立是一致的算法思想,具体代码如下:
void build()
{
int i;
int L = 0, R = 0;
for (i = 0; i < 26; i++)
{
if (ch[0][i])
{
R++;
Q[R] = ch[0][i];
}
}
while (L < R)
{
L++;
int x = Q[L];
for (i = 0; i < 26; i++)
{
if (ch[x][i])
{
fail[ch[x][i]] = ch[fail[x]][i];
mark[ch[x][i]] |= mark[fail[ch[x][i]]];
R++;
Q[R] = ch[x][i];
}
else {
ch[x][i] = ch[fail[x]][i];
}
}
}
}
4、求解过程(包括AC自动机字符串匹配与动态规划的实现)
这一部分AC自动机字符串匹配的算法思想,小编在AC自动机-详解AC自动机以及模板这篇博客中已经提过了。而动态规划的实现在算法思想中也已经详细地进行了解释,具体代码如下:
void solve()
{
int i, j, k, l, ll;
F[0][0][1] = 1;
for (i = 0; i <= m; i++) //长度为1-m
{
for (j = 0; j <= cnt; j++) //遍历AC自动机所有节点
{
for (k = 1; k <= n; k++) //遍历所有页
{
if (F[i][j][k])
{
F[i][j][k] = F[i][j][k] % MOD;
Ans[i] = Ans[i] + F[i][j][k];
bool flag = false;
int tj, tk, x;
for (l = 0; l < K; l++) //遍历所有词典里面的语句
{
if (i + Len[l] > m) continue;
flag = true;
tj = j;
tk = k;
for (ll = 0; ll < Len[l]; ll++)
{
x = Out[tk][s[l][ll]];
tk = Next[tk][s[l][ll]];
tj = ch[tj][x];
if (mark[tj])
{
flag = false; break;
}
}
if (flag) F[i + Len[l]][tj][tk] = F[i + Len[l]][tj][tk] + F[i][j][k];
}
}
}
}
}
for (i = 1; i <= m; i++)
{
Ans[i] = Ans[i] % MOD;
cout << Ans[i] << endl;
}
}
完整代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#define MOD 998244353
using namespace std;
int cnt = 0;
int ch[60][26]; //用于AC自动机的字典树
int fail[60]; //AC自动机fail
bool mark[60]; //自动机的终止状态
int n, m, K;
int Out[60][26]; //明文->密文
int Next[60][26]; //转换页数
char s[60][60]; //词典
int Len[60]; //词典中单词长度
int Q[60]; //建立AC自动机fail时BFS使用的队列
long long F[1010][51][51];//F[i][j][k]代表长度为i,密文匹配在第j个节点,且正在查看解密码本第k页的铭文个数
long long Ans[1010];
void newnode(int x)
{
int i;
for (i = 0; i < 26; i++) ch[x][i] = 0;
fail[x] = 0;
mark[x] = false;
}
void insert(char *s)
{
int i, k;
int len = strlen(s);
int x = 0;
for (i = 0; i < len; i++)
{
k = s[i] - 'a';
if (ch[x][k] == 0)
{
cnt++;
ch[x][k] = cnt;
newnode(ch[x][k]);
}
x = ch[x][k];
}
mark[x] = true;
}
void build()
{
int i;
int L = 0, R = 0;
for (i = 0; i < 26; i++)
{
if (ch[0][i])
{
R++;
Q[R] = ch[0][i];
}
}
while (L < R)
{
L++;
int x = Q[L];
for (i = 0; i < 26; i++)
{
if (ch[x][i])
{
fail[ch[x][i]] = ch[fail[x]][i];
mark[ch[x][i]] |= mark[fail[ch[x][i]]];
R++;
Q[R] = ch[x][i];
}
else {
ch[x][i] = ch[fail[x]][i];
}
}
}
}
void solve()
{
int i, j, k, l, ll;
F[0][0][1] = 1;
for (i = 0; i <= m; i++) //长度为1-m
{
for (j = 0; j <= cnt; j++) //遍历AC自动机所有节点
{
for (k = 1; k <= n; k++) //遍历所有页
{
if (F[i][j][k])
{
F[i][j][k] = F[i][j][k] % MOD;
Ans[i] = Ans[i] + F[i][j][k];
bool flag = false;
int tj, tk, x;
for (l = 0; l < K; l++) //遍历所有词典里面的语句
{
if (i + Len[l] > m) continue;
flag = true;
tj = j;
tk = k;
for (ll = 0; ll < Len[l]; ll++)
{
x = Out[tk][s[l][ll]];
tk = Next[tk][s[l][ll]];
tj = ch[tj][x];
if (mark[tj])
{
flag = false; break;
}
}
if (flag) F[i + Len[l]][tj][tk] = F[i + Len[l]][tj][tk] + F[i][j][k];
}
}
}
}
}
for (i = 1; i <= m; i++)
{
Ans[i] = Ans[i] % MOD;
cout << Ans[i] << endl;
}
}
int main()
{
int i, j;
cnt = 0;
newnode(0);
cin >> n >> m; //输入n,m
for (i = 0; i < 26; i++) //输入解密密码本
{
for (j = 1; j <= n; j++) //n 页
{
char S[1010];
scanf("%s", S);
Out[j][S[0] - 'a'] = i; //提取出密文字母
int len = strlen(S);
int v = 0; //提取出对应的跳转的页数
if (len == 2) v = S[1] - '0';
else v = (S[1] - '0') * 10 + S[2] - '0';
Next[j][S[0] - 'a'] = v; //提取出密文对应的跳转的页数
}
}
K = 0;
while (scanf("%s", s[K]) != EOF) //输入词典
{
insert(s[K]);
Len[K] = strlen(s[K]); //AC自动机的模板串的建立
for (i = 0; i < Len[K]; i++)
s[K][i] = s[K][i] - 'a';
K++;
}
build(); //AC自动机的fail指针的建立
solve(); //求解过程,包括动态规划以及字符串匹配
return 0;
}