DP与KMP的结合

闲话

其实我也不太知道这个套路叫啥,他的过程就是DP和KMP的结合,毕竟是通过KMP来加速暴力匹配的过程,标题就写成KMP优化DP了.与之对应的还有一种AC自动机上优化的,其实两个套路一模一样,这里就只展示两个题,AC自动机的如果之后有空也会补上来.

设计密码

原题链接:Acwing1052

(由于acwing的题目上锁了,可能看不到,这个题是Indeedtokyo的19面试题,可能没得地方给评测)

题目大意:要求你构造一个长度为\(n\)的字符串\(s\),并且不包含子串\(t\).求\(s\)的方案数,对\(10^9+7\)取模.

此处不包含的定义是连续的一段不等于\(t\),不能是断开的几段拼起来是\(t\).

数据范围:

\(1 \leq n \leq 50\)

\(1 \leq |t| \leq n\)

思路

一个比较直接的想法肯定就是枚举了,不过既然方案数需要取模,说明方案数非常大,这个时候往往指向使用\(dp\)求方案数的思路.考虑设计状态:首先第一维肯定是保存当前构造到哪一位,同时我们还要知道现在具体匹配到\(t\)中的哪一个位置了,也就是当前这个位置肯定会和\(t\)产生字符串的匹配,问题就是现在连续的匹配到了谁的问题,这个东西肯定不好直接求,直接丢状态里.

那么可以写一下\(dp\)的具体流程了:

  • 状态:\(f[i][j]\)表示当前在\(s\)的第\(i\)位,并且\(s[i]\)这一位填上某个元素之后,与\(t\)的匹配长度是\(j\)的方案数.
  • 入口:\(f[0][0]=0\)其他全部置为\(-\infin\).
  • 转移:考虑\(f[i][j]\)是由谁转移而来的是非常麻烦的,这里考虑当前这个状态\(f[i][j]\)可以转移到哪个状态,那么枚举下一位,也就是\(i+1\)这一位具体要填什么字符,拿他去进一步和\(t\)进行匹配,匹配的一个新位置记作\(u\),那么下一个状态就是\(f[i + 1][u]\).显然当\(u==|t|\)时意味着\(s\)里面有一个子串\(t\)存在了,这种情况必须要删除.往下就可以写出方程形式:\(f[i + 1][u] += f[i][j]\).
  • 出口:首先要做到最后一位,第一维肯定是\(n\),第二维只要保证不取\(|t|\)就可以了,直接把方案总和起来.

接着可以发现,这个枚举\(s[i + 1]\)的取值,并且往后匹配的过程恰好就是拿\(t\)去做匹配,那么类似KMP处理就可以了.

不过这里其实有个出口第一维的取值的细节问题,下一个问题会考虑到,这个问题不太需要.

代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 55,MOD = 1e9 + 7;
char s[N],T[N];
int succ[N],f[N][N];
int main()
{
	int n;scanf("%d",&n);
	scanf("%s",T + 1);
	int m = strlen(T + 1);
	for(int i = 2,j = 0;i <= m;++i)
	{
		while(j && T[i] != T[j + 1])	j = succ[j];
		if(T[i] == T[j + 1])	++j;
		succ[i] = j;
	}
	f[0][0] = 1;
	for(int i = 0;i <= n;++i)
	{
		for(int j = 0;j < m;++j)
		{
			for(char k = 'a';k <= 'z';++k)
			{
				int u = j;
				while(u && (u == n || k != T[u + 1])) u = succ[u];
				if(k == T[u + 1])	++u;
				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;
}

CF1163 D Mysterious Code

题目大意:有两个串\(s\)\(t\)只包含小写字符,另有一个字符\(c\),其中包含小写字符或者*.现在要求你把*全部填上小写字符,并且规定两个字符串的牛逼操作\(f(c,s)\),他的值是\(s\)串在\(c\)中的出现次数,注意这里的出现次数跟上面那道题是一样连续的,而不是断开的.求\(f(c,s) - f(c,t)\)的最大值.所有*位置必须填上字符,不可以空.

数据范围:

\(1 \leq |c| \leq 1000\)

\(1 \leq |s| ,|t| \leq 50,s \neq t\)

思路

注意到较小的数据范围,接下来考虑直接dp:

  • 状态:\(f[i][j][k]\)表示当前\(c\)做到第\(i\)位,在\(s\)中的匹配位置是\(j\),在\(t\)中的匹配位置是\(k\).取得的\(f(c,s)-f(c,t)\)的所有可能取值中的最大值.

  • 入口:\(f[1][0]=0\)其他全部\(-\infin\).

  • 转移:与上面一致,如果当前这一位是*则直接枚举取值,否则就直接按原有的值做.这里分别对两个串做匹配,记\(j`,k`\)分别是在\(s\)\(t\)中下一位匹配到的新位置,只有当他们分别等于另一串的长度的时候才意味着匹配成功,当\(s\)匹配成功时权值增加一,同样\(t\)则减少一.直接转移即可.

  • 出口:第一维取\(n+1\)其他两维任选,取最大值即可.

这里的出口是有差别的,最开始把\(c\)这个串挪动成\(1\)开始的话,那么如果有串是枚举到\(c\)的最后一位,也就是\(i=n\)时才匹配上,那么他转移的时候,这一位本身要权值增加或者权值减少的,但是会把这个修改丢到\(n+1\)位上,所以最后一步等于说把本来该在这一位的移动到了下一位去,尽管下一位\(c\)并没有值,这是这种转移方式带来的.当然你也可以选择不把\(c\)设置从1开始,枚举的时候也从\([0,n-1]\),这个时候答案就要在\(n\)处统计了,可能看的好看点.

代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define forn(i,x,n) for(int i = x;i <= n;++i)	

const int N = 55,M = 1005;
char c[M];
int f[M][N][N];
struct kmp
{
	char s[N],next[N];
	int n;
	void build()
	{
		next[1] = 0;
		for(int i = 2,j = 0;i <= n;++i)
		{
			while(j > 0 && s[i] != s[j + 1]) j = next[j];
			if(s[i] == s[j + 1]) ++j;
			next[i] = j;
		}
	}
}s,t;

int main()
{
	scanf("%s",c + 1);
	int n = strlen(c + 1);
	scanf("%s%s",s.s + 1,t.s + 1);
	s.n = strlen(s.s + 1);t.n = strlen(t.s + 1);
	s.build();t.build();
	
	memset(f,-0x3f,sizeof f);
	f[1][0][0] = 0;
	forn(i,1,n)	forn(j,0,s.n) forn(k,0,t.n)
	{
		for(char u = (c[i] == '*' ? 'a' : c[i]);u <= (c[i] == '*' ? 'z' : c[i]);++u)
		{
			int j_ = j,k_ = k;
			while(j_ > 0 && (j_ == s.n || u != s.s[j_ + 1]))	j_ = s.next[j_];
			if(u == s.s[j_ + 1])	++j_;
			while(k_ > 0 && (k_ == t.n || u != t.s[k_ + 1]))	k_ = t.next[k_];
			if(u == t.s[k_ + 1])	++k_;
			
			int ct = (j_ == s.n) - (k_ == t.n);
			f[i + 1][j_][k_] = max(f[i + 1][j_][k_],f[i][j][k] + ct);
		}
	}
	int res = -1e9;
	forn(j,0,s.n)	forn(k,0,t.n)	res = max(res,f[n + 1][j][k]);
	printf("%d",res);
    return 0;
}

posted @ 2021-01-16 12:55  随处可见的阿宅  阅读(319)  评论(0编辑  收藏  举报