CSP202009-5 密信与计数

CSP202009-5 密信与计数

题目

题目可以在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;
}
  • 6
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值