题目大意
给定一个全部由小写英文字母组成的字符串,允许你至多删掉其中 3 3 3 个字符,结果可能有多少种不同的字符串?题目链接在这里。
解析
题目用到的是动态规划的思想,但是我一开始没想到,看了解析也不明白,最后苦思冥想才明白,只能说菜就多练。
上面的那篇解析,说明文字比较少,这里我整理一下自己的思路。首先定义状态:dp[i][j]
表示
s
s
s 的前
i
i
i 个字符组成的字符串里面要删除
j
j
j 个字符,得到的字符串有多少种。如果记s[a:b]
为
s
s
s 的第
a
a
a 个(含)到第
b
b
b 个(不含)字符所组成的子串。
现在需要得到状态转移方程。显然,当删除的这
j
j
j 个字符包含s[i]
的时候,对答案的贡献是dp[i - 1][j - 1]
;如果删除的
j
j
j 个字符中不包含s[i]
,那么对答案的贡献就是dp[i - 1][j]
。于是可以写出状态转移方程:
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
−
1
]
+
d
p
[
i
−
1
]
[
j
]
(1)
\mathit{dp}[i][j]=\mathit{dp}[i-1][j-1]+\mathit{dp}[i-1][j]\tag1
dp[i][j]=dp[i−1][j−1]+dp[i−1][j](1)
但是,这个状态转移方程是错误的,因为有可能会有重复计算的情况。正如字符串s = "abcac"
,如果要求dp[5][2]
,那么dp[4][1]
中包含了"abc"
,而dp[4][2]
中的"ab"
加上未被删除的"c"
依然构成"abc"
。
所以我们的dp[i][j]
必须减去重复的情况,但是这种情况的总数怎么来的,那篇博客并没有讲得很清楚。
我们考虑这样一个字符串s = "abcadefak"
,它的长度是
9
9
9。那么请问在计算
d
p
[
9
]
[
4
]
\mathit{dp}[9][4]
dp[9][4] 的时候会出现重复的情况吗?显然不会。因为'k'
只在字符串中出现了一次,而:
- 不删除
s[9]
:所得的子串尾部永远是'k'
。 - 删除
s[9]
:删除了唯一的一个'k'
,不管再怎么进行剩余的删除操作,子串的尾部永远不可能是'k'
。
所以不存在所谓的重复情况。
再考虑这样一个例子,s = "abcefgfeb"
,长度为
9
9
9,要求
d
p
[
9
]
[
6
]
\mathit{dp}[9][6]
dp[9][6]。这也不会出现重复的情况,因为虽然'b'
在s[2]
和s[9]
都出现了,但是:
- 不删除
s[9]
:所得子串尾部永远是'b'
。 - 删除
s[9]
:虽然前面还有一个b
,但是如果想要所得子串尾部是'b'
,必须把s[2]
之后的所有字符也都给删了。但是我们只能删 6 6 6 个字符,是删不了这么多的。
所以这种情况也不会有重复。那什么时候会有重复呢?针对上面的例子,你应该已经悟出来了:
存在重复的情况:当字符串s
中存在与s[i]
相同的字符,且这样的字符离s[i]
最近的那个位于s[x]
(显然有
1
≤
x
<
i
1\leq x<i
1≤x<i),那么在
j
≥
i
−
x
j\geq i-x
j≥i−x 时,求dp[i][j]
依式
(
1
)
(1)
(1) 存在冗余情况。
总结一下就是:
- 前面存在相同字符。
- 前面最近的这个“相同字符”要离的足够近(满足 i − x ≤ j i - x\leq j i−x≤j 就是足够近)。
这样才会存在冗余情况。存在性已经说明完毕了,那么当这个重复存在的时候,到底有多少个重复的呢?下面就让你明白。比如说字符串 s = ⋯ a ⋯ ⏟ 一共有 i − x − 1 个字符 a s=\cdots\red{a}\underbrace{\cdots}_{一共有i-x-1个字符}\red{a} s=⋯a一共有i−x−1个字符 ⋯a,第一个 a a a 位于 s [ x ] s[x] s[x],第二个(也是最后一个) a a a 位于 s [ i ] s[i] s[i]。这里假设 i − x ≤ j i-x\le j i−x≤j,那么两种情况分别是:
- 不删除 s [ i ] s[i] s[i]: s 1 ′ = ⋯ a ⋯ ⏟ 删除 j 个 a s_1^\prime=\underbrace{\cdots\red{a}\cdots}_{删除j个}\red a s1′=删除j个 ⋯a⋯a。
- 删除 s [ i ] s[i] s[i]:为了有可能和 s 1 ′ s_1^\prime s1′ 重复,必须有 s 2 ′ = ⋯ a ⏟ 删除 j − ( i − x ) 个,并且保证末尾还是 a ⋯ ⏟ 这 i − x − 1 个全部删掉 a s_2^\prime=\underbrace{\cdots\red a}_{删除j-(i-x)个,并且保证末尾还是 a}\underbrace{\cdots}_{这i-x-1个全部删掉}\sout{\red a} s2′=删除j−(i−x)个,并且保证末尾还是a ⋯a这i−x−1个全部删掉 ⋯a。
记
⋯
a
⋯
\cdots\red{a}\cdots
⋯a⋯ 删除
j
j
j 个所得到的字符串是
s
1
′
′
s_1^{\prime\prime}
s1′′,
⋯
a
\cdots\red a
⋯a 删除
j
−
(
i
−
x
)
j-(i-x)
j−(i−x) 个,并且保证末尾还是
a
a
a 的字符串是
s
2
′
′
s_2^{\prime\prime}
s2′′。根据具体删除操作的不同,
s
1
′
′
s_1^{\prime\prime}
s1′′ 和
s
2
′
′
s_2^{\prime\prime}
s2′′ 随之而不同,但如果删得好,那么
s
1
′
′
s_1^{\prime\prime}
s1′′ 和
s
2
′
′
s_2^{\prime\prime}
s2′′ 这个时候可能是同一个字符串
s
′
′
s^{\prime\prime}
s′′。那么我们需要考虑的重复情况总数就是,
s
′
′
s^{\prime\prime}
s′′ 可能的情况数。
细心地你肯定发现了,凡是
s
2
′
′
s_2^{\prime\prime}
s2′′ 通过一通操作得到的字符串
s
′
′
s^{\prime\prime}
s′′,
s
1
′
′
s_1^{\prime\prime}
s1′′ 都是可以特定删除方式得到的。所以考虑的重复情况总数就是
s
2
′
′
s_2^{\prime\prime}
s2′′ 可能的情况数。
为了保证
⋯
a
\cdots\red a
⋯a 删除
j
−
(
i
−
x
)
j-(i-x)
j−(i−x) 个后末尾还是
a
a
a,我们可以简单地保留最后这个
a
a
a,也可以将这个
a
a
a 直到前面的一个
a
a
a(如果有的话)中的内容全部删除,也可以将这个
a
a
a 直到前面的前面的那个
a
a
a(如果有的话) 中的内容全部删除……。但是不管怎样,简单地保留这个
a
a
a 是可以涵盖所有可能生成的字符串的。所以,“重复”的计量标准就是
d
p
[
x
−
1
]
[
j
−
(
i
−
x
)
]
\mathit{dp}[x-1][j-(i-x)]
dp[x−1][j−(i−x)]。
AC Code
有了上面的分析,不难写出代码:
#include <bits/stdc++.h>
#define N 1000005
using namespace std;
typedef long long ll;
ll dp[N][4];
string s;
int len,prepos[N];
int main () {
cin >> s;len = s.size();
int temp[26];memset(temp,0,sizeof(temp));
for(int i = 1;i <= len;i++) {
int co = s[i - 1] - 'a';
prepos[i] = temp[co];
temp[co] = i;
}
dp[1][0] = 1;dp[1][1] = 1;dp[0][0] = 1;
for(int i = 2;i <= len;i++)
for(int j = 0;j < 4;j++) {
if(i < j) dp[i][j] = 0;
else if(i == j) dp[i][j] = 1;
else if(j == 0)
dp[i][j] = 1;
else {
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
int ppos = prepos[i];
if(ppos && i - ppos <= j)
dp[i][j] -= dp[ppos - 1][j - i + ppos];
}
}
return cout << (dp[len][0] + dp[len][1] + dp[len][2] + dp[len][3]),0;
}
时间复杂度是 O ( n ) O(n) O(n),其中 n n n 为字符串的最大长度。此题还可以拓展,如果题目改为至多删除 k k k 个字符,那么时间复杂度是 O ( n k ) O(nk) O(nk)。