一、问题引入
给出一个长度为n的字符串S和一个长度为m的不含重复字符的字符串T,每次你可以在S中删除一个等于T的子序列,最多可以删除多少次?
如字符串S=ababccd,T=abc,可以选择s1s2s5
,s1s2s6
,s1s4s5
,s1s4s6
,s3s4s5
,s3s4s6
进行删除,删除后分别得到abcd,abcd,bacd,bacd,abcd,abcd。
如果删除后得到abcd,则还可以再进行一次删除,最多可以删除2次。
这里注意,字符串T种只会包含小写字母以及T中不会出现重复的字符
二、问题分析
提炼一下本题的意思,我们可以把本题精炼为求字符串S的字串中,在每个字符不能重复出现的条件下的子串中字符串T的个数。比如在上文的例子中,ababccd
中的abc
就只有2个,因此答案为2。
三、解法分析
1.暴力求解
如果我们通过暴力求解的思路来求解这个问题的话,我们需要枚举S中的每一个字符作为起点,之后如果与T中的第一个字符匹配了就再去匹配T中的下一个字符…时间复杂度为O(NM)这显然不是一个合适的算法。
2.线性求解
此解法实际上使用到了动态规划的思路来解决问题,如果在阅读本篇博客之前从未接触过动态规划算法则会比较难理解本题代码的思路。在这里先列出含有注释的代码,在后文中再结合代码来进行算法的分析。
#include <bits/stdc++.h>
#define endl '\n'
#define int long long
#define INF 0x3f3f3f3f
#define ull unsigned long long
#define mem(a, b) memset(a, b, sizeof(a))
#define ck(x) cerr << #x << "=" << x << '\n';
using namespace std;
typedef pair<int, int> PII;
const int N = 1e6 + 7, mod = 1e9 + 7;
void solve()
{
int n, m;
cin >> n >> m;//n和m分别是s和t的长度
string s, t;
cin >> s >> t;
vector<int> cnt(m + 5), pos(26, -1);
//vector动态开辟数组,cnt[m+5],pos[26](pos数组中的值全赋值为-1)
//pos数组大小为26,用来记录字符串T中26个英文字母是否在T中出现,若出现,记录下位置
for (int i = 0; i < m; i++)
{
pos[t[i] - 'a'] = i + 1;//用来记录每个字符在T中的位置
}
cnt[0] = n;//动态规划初始化
for (int i = 0; i < n; i++)//从左往右遍历S数组
{
char c = s[i];
int x = pos[c - 'a'];//记录一下S中第i个字符c在T中的位置
if (x != -1 && cnt[x - 1] > 0)//如果这个字符c在T中存在并且c的前一个字符已经出现过了
{
//匹配成功,状态转移
cnt[x - 1] -= 1;
cnt[x] += 1;
}
}
cout << cnt[m] << "\n";//输出最后匹配到的状态
}
signed main()
{
std::ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
solve();
return 0;
}
光看代码可能比较难以理解,我们先结合一个例子来看看:
S=“ababcc”
T=“abc”
cnt[4]={6,0,0,0}
pos[26]={1,2,3,-1,-1,-1…}
#1:S[0]=a,x=1
cnt[4]={5,1,0,0}:“a”
#2:S[0]=b,x=2
cnt[4]={5,0,1,0}:“ab”
#3:S[0]=a,x=1
cnt[4]={4,1,1,0}:“ab”“a”
#4:S[0]=b,x=2
cnt[4]={4,0,2,0}:“ab”“ab”
#5:S[0]=c,x=3
cnt[4]={4,0,1,1}:“abc”“ab”
#6:S[0]=c,x=3
cnt[4]={4,0,0,2}:“abc”“abc”
最后得出匹配到的字串T为2个
通过样例我们发现,cnt[i]的含义是用来动态存储枚举过程中的,由T的前i个字符组成的子串,出现的个数,当我们枚举完S后,cnt[m]就是我们要求的答案。
通过动态规划的思路,我们可以极大的简化时间复杂度,但是理解上就会稍微复杂一些。关键在于cnt数组的意义理解和对于动态规划的状态转移的理解。
作者:Avalon·Demerzel