Stamp:难点主要在 dp 转移的细节与分讨上,但通过改变状态设计可以大大简化分讨细节的题。
观察
首先要有一个观察:只要某一个前缀能被覆盖出来,那么无论它后面多出来多少,后面的字符串都可以帮他重新覆盖回去。这也就是 dp 为啥没有后效性的原因。
除此之外,还要注意一个字符串不仅能在其他字符串上面,还能被盖在最下层来达到用子串接着覆盖的效果。
暴力 dp 思路
设 \(dp_i\) 表示第 \(i\) 个字符的前缀能否被覆盖。每次转移的时候枚举当前位和 \(B\) 的第几个字符匹配,然后判断某一段是否相等,进行转移即可。这个做法感觉比较难写,但肯定没有假。时间复杂度 \(O(nm^2)\),显然可过。
更巧妙的 dp 思路
设 \(dp_{i,j}\) 表示 \(A\) 匹配到第 \(i\) 个,\(B\) 匹配到第 \(j\) 个是否可行。
那么接下来分为三个情况,这三种情况的前提条件就是 \(A_i=B_j\),否则一定无解:
- \(j=1\),也就是当前 \(B\) 刚开始匹配。也就是说前面的部分只要能被覆盖出就行了,不管他前面匹配到多少个,则 \(dp_{i,j}\gets dp_{i,j}\bigvee_{k=1}^{m}dp_{i-1,k}\)。
- 这个字符接着上一个字符覆盖,则 \(dp_{i,j}\gets dp_{i,j}\bigvee dp_{i-1,j-1}\)。
- 这个字符被盖在最下面,用自己的后缀覆盖。这种情况需要满足的条件是前一个字符串一定要先被覆盖完,才可以在下一个字符串覆盖前覆盖在它的下面。则 \(dp_{i,j}\gets dp_{i,j}\bigvee dp_{i-1,m}\)。
直接这样转移复杂度也是 \(O(nm^2)\) 的,但是第一种情况只要记录前一位是否含 \(1\) 即可优化为 \(O(nm)\)。总体时间复杂度 \(O(nm)\)。
代码
#include <bits/stdc++.h>
#define fi first
#define se second
#define lc (p<<1)
#define rc ((p<<1)|1)
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi=pair<int,int>;
bool dp[200005][10],hv[200005];
int n,m;
char a[200005],b[10];
int main()
{ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);cin>>n>>m>>a+1>>b+1;dp[0][0]=1;for(int i=1;i<=n;i++){for(int j=1;j<=m;j++){if(a[i]==b[j]){dp[i][j]|=dp[i-1][j-1];dp[i][j]|=dp[i-1][m];if(j==1)dp[i][j]|=hv[i-1];}hv[i]|=dp[i][j];}}if(dp[n][m])cout<<"Yes";else cout<<"No";return 0;
}