Luogu P8112 [Cnoi2021]符文破译 题解

P8112 [Cnoi2021]符文破译

借用 KMP 思想优化的动态规划。

首先,用 \(dp[i]\) 表示把前 \(i\) 位的字符完全匹配需要的最少词缀数(下标均从 \(1\) 开始)。那么,我们可以从点 \(i+1\) 开始,向后逐位与字符串 \(T\) 比较。设此时匹配到了 \(T\) 中的第 \(j\) 位,如果相等,则易得转移方程:

\[dp[i+j]=\min(dp[i+j],dp[i]+1) \]

如果不相等或到达了字符串 \(T\) 末尾,则证明在此之后不会更长的有魔法词缀,可以结束这一次匹配,令 \(i=i+1\) 计算下一位即可。

很明显,这个算法的时间复杂度是 \(O(|S||T|)\) 的,当数据范围达到 \(|S|,|T|\le10^6\) 时,算法必然超时。

考虑优化这个算法,我们知道,如果不相等或到达了字符串 \(T\) 末尾,失配后是可以直接跳过一部分不可能产生新的解的数据。这样就自然而然地想到了用这个思想把单模字符串匹配优化到 \(O(|S|+|T|)\) 的 KMP 算法。

借助 KMP 的思想,首先求出字符串 \(T\)\(next\) 数组,然后开始按照 KMP 的方式匹配:(设此时文本串匹配到第 \(i\) 项,模式串匹配到第 \(j\) 项)

设置一个名为 \(now\) 的临时变量,用于存储如果匹配的最少词缀数。

可以直接逐位比较。如果相等,则按照 KMP 思想,将模式串和文本串指针一起后移,令 \(dp[i]=now\) 后比较下一位。

如果不相等,可以令 \(j=next[j]\) 之后重新计算 \(now\) 的值。因为一旦匹配失败,只能再次选择一个词缀。每次 KMP 算法在匹配失败后,会利用最长公共前后缀的性质使得文本串指针 \(i\) 不往前跳。而每次利用最长公共前后缀的性质,会改变模式串匹配的起始位置,所以需要重新计算 \(now\) 的值。可以直接用 \(dp[i-j]\) 计算出模式串匹配的起始位置的前一个位置,把 \(now\) 的值更新为 \(dp[i-j]+1\) 以保证正确性。模式串匹配到末尾也是同理。

DP 边界:\(dp[0]=1\)

DP 目标:\(dp[|S|]\)

时间复杂度:\(O(|S|+|T|)\)

注意,由于有无解的情况,所以当 \(next\) 数组跳到 \(-1\) 时,应该直接判定无解并输出 Fake。因为如果 \(next\) 数组跳到 \(-1\) 证明匹配第一个字符就失配了,此时后面没有办法再进行匹配,无解。

完整代码:(由于代码中的字符串下标是从 \(0\) 开始的,所以可能会和上文的讲解有些出入)

#include <bits/stdc++.h>
using namespace std;
int lt,ls,next1[10000010],f[10000010];
char t[10000010],s[10000010]; 
void get_next(char t[],int next[])
{
	int i=0,j=-1;
	next[0]=-1;
	while(i<lt)
	   {
	   	if(j==-1||t[i]==t[j])i++,j++,next[i]=j;
	   	else j=next[j];
	   }
}

bool kmp(char s[],char t[],int next[])
{
	int i=0,j=0,now=1;
	f[0]=1;
	while(i<ls)
	   {
	   	if(j==-1)return 0;
	   	if(s[i]==t[j])i++,j++,f[i]=now;
	   	else j=next[j],now=f[i-j]+1;
	   	if(j==lt)now=f[i-j]+1,j=next[j];
	   }
	return 1;
}

int main()
{
	scanf("%d%d%s%s",&lt,&ls,t,s);
	get_next(t,next1);
	if(!kmp(s,t,next1))printf("Fake");
	else printf("%d",f[ls]);
    return 0;
}

AC记录

posted @ 2025-02-12 08:29  w9095  阅读(50)  评论(0)    收藏  举报