动态规划部分题集解

密码脱落

$X$ 星球的考古学家发现了一批古代留下来的密码。

这些密码是由 $A、B、C、D$ 四种植物的种子串成的序列。

仔细分析发现,这些密码串当初应该是前后对称的(也就是我们说的镜像串)。

由于年代久远,其中许多种子脱落了,因而可能会失去镜像的特征。

你的任务是:

给定一个现在看到的密码串,计算一下从当初的状态,它要至少脱落多少个种子,才可能会变成现在的样子。

输入格式

共一行,包含一个由大写字母 $ABCD$ 构成的字符串,表示现在看到的密码串。

输出格式

输出一个整数,表示至少脱落了多少个种子。

数据范围

输入字符串长度不超过 $1000$。

输入样例1:

ABCBA

输出样例1:

0

输入样例2:

ABDCDCBABC

输出样例2:

3

 

解题思路

  题意就是问我们在给出的字符串中,最少添加多少个字符,使其变成一个回文字符串。这题需要转换一下思维,就是原来我们是需要找到没有可以配对的字符,然后在某个对称的位置添加一个字符与其配对,或者说消灭了一个单个不构成回文的字符,这样就可以形成回文字符串。因此整个问题可以等价于至少要删除多少个字符可以变成回文串。删除字符后,得到的结果应该是一个回文字符串,因此我们应该在给定的字符串中找到最长的回文子序列(注意,不是回文字串)。最后要最少删除的字符的数量就为字符串总长度减去最长回文子序列,这个结果也就是最少添加的字符。

  求某个字符串的最长回文子序列可以用区间dp,求$\left[ {i, j} \right]$区间内的最长的回文子序列的长度。

  其中对于$str \left[ i \right]$在,$str \left[ j \right]$不在,以及$str \left[ i \right]$不在,$str \left[ j \right]$不在这两种情况的集合划分,是重复的。

  比如对于$str \left[ i \right]$在,$str \left[ j \right]$不在,意味着我们要在$\left[ {i, j - 1} \right]$中找到满足回文子序列的条件下,还应该满足该满足$str \left[ i \right]$是这个回文子序列的左端点。而我们的$f \left( {i, j - 1} \right)$是所有在$str \left[ {i \sim j-1} \right]$中的回文子序列的集合,也就是说在这个集合中的回文子序列,$str \left[ i \right]$不一定是序列的左端点,但包含$str \left[ i \right]$为左端点的情况。因为我们还可以把$f \left( {i, j - 1} \right)$所表示的集合划分为两类,一类是$str \left[ i \right]$为左端点的回文子序列,另一类是$str \left[ i \right]$不为左端点的回文子序列。

  因此,可以发现$f \left( {i, j - 1} \right)$是包含$str\left[ i \right]$为回文子序列左端点的这种情况,同时$f \left( {i, j - 1} \right)$所表示的集合又包含于$f \left( {i, j} \right)$。虽然有重复的集合元素,但由于是求集合元素的最大值,因此即使重复也不会影响最大值。

  因此这种集合划分是保证不漏,但会有重复。我们的划分方式是不重不漏的,但我们在计算某一个集合的时候会计算重复的。这种划分方式还是比较特殊的,就是如果我们发现某一种集合不好表示出来,我们可以用包含重复元素的集合来表示。求最大最小值可以这么划分,集合的划分一定要保证不漏,但可以有重复。但求数量就不可以,一定要保证不重不漏。

  $str \left[ i \right]$不在,$str \left[ j \right]$在也是同理。最后我们发现对于情况$str \left[ i \right]$和$str \left[ j \right]$都不在,是包含于$f \left( {i, j - 1} \right)$所表示的集合,以及$f \left( {i + 1, j} \right)$所表示的集合,因此对于这种情况我们可以不用计算,因为在算$f \left( {i, j - 1} \right)$或$f \left( {i + 1, j} \right)$的时候,已经算过$f \left( {i - 1, j - 1} \right)$了。

  AC代码如下:

 1 #include <cstdio>
 2 #include <cstring>
 3 #include <algorithm>
 4 using namespace std;
 5 
 6 const int N = 1010;
 7 
 8 char str[N];
 9 int f[N][N];
10 
11 int main() {
12     scanf("%s", str);
13     
14     int n = strlen(str);
15     for (int len = 1; len <= n; len++) {
16         for (int i = 0; i + len - 1 < n; i++) {
17             int j = i + len - 1;
18             if (len == 1) {
19                 f[i][j] = 1;
20             }
21             else {
22                 if (str[i] == str[j]) f[i][j] = f[i + 1][j - 1] + 2;
23                 f[i][j] = max(f[i][j], f[i][j - 1]);
24                 f[i][j] = max(f[i][j], f[i + 1][j]);
25             }
26         }
27     }
28     printf("%d", n - f[0][n - 1]);
29     
30     return 0;
31

  下面给出和这种划分方式很像的题。

 

最长公共子序列

给定两个长度分别为 $N$ 和 $M$ 的字符串 $A$ 和 $B$,求既是 $A$ 的子序列又是 $B$ 的子序列的字符串长度最长是多少。

输入格式

第一行包含两个整数 $N$ 和 $M$。

第二行包含一个长度为 $N$ 的字符串,表示字符串 $A$。

第三行包含一个长度为 $M$ 的字符串,表示字符串 $B$。

字符串均由小写字母构成。

输出格式

输出一个整数,表示最大长度。

数据范围

$1 \leq {N, M} \leq 1000$

输入样例:

4 5
acbd
abedc

输出样例:

3

解题思路

  这题集合的划分和上面的那题一样,由于是求最大值,因此集合可以有重复。

  $f \left( {i, j - 1} \right)$和$f \left( {i, j - 1} \right)$是包含重复元素的,这两个集合又包含于$f \left( {i, j} \right)$所表示的集合。

  AC代码如下:

 1 #include <cstdio>
 2 #include <algorithm>
 3 using namespace std;
 4 
 5 const int N = 1010;
 6 
 7 char str1[N], str2[N];
 8 int f[N][N];
 9 
10 int main() {
11     int n, m;
12     scanf("%d %d %s %s", &n, &m, str1 + 1, str2 + 1);
13     
14     for (int i = 1; i <= n; i++) {
15         for (int j = 1; j <= m; j++) {
16             f[i][j] = max(f[i - 1][j], f[i][j -1]);
17             if (str1[i] == str2[j]) f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
18         }
19     }
20     printf("%d", f[n][m]);
21     
22     return 0;
23 }

 

括号配对

Hecy 又接了个新任务:BE 处理。

BE 中有一类被称为 GBE。

以下是 GBE 的定义:

  • 空表达式是 GBE
  • 如果表达式 A 是 GBE,则 [A] 与 (A) 都是 GBE
  • 如果 A 与 B 都是 GBE,那么 AB 是 GBE

下面给出一个 BE,求至少添加多少字符能使这个 BE 成为 GBE。

注意:BE 是一个仅由 ( 、) 、[ 、] 四种字符中的若干种构成的字符串。

输入格式

输入仅一行,为字符串 BE。

输出格式

输出仅一个整数,表示增加的最少字符数。

数据范围

对于所有输入字符串,其长度小于 $100$。

输入样例:

[])

输出样例:

1

 

解题思路

  这题也是集合的重复划分。思路与密码脱落一样,需要转换成求某段区间的子序列,子序列要满足括号的匹配,且是最长的子序列。最后要添加的括号数量就是字符串总长度减去最长的括号匹配的子序列。

  情况$\left( A \right) / \left[ A \right]$的集合的划分和上面两题差不多一样。而对于$AB$情况的划分,我们把这种类型看成这种形式$()()()()...$,我们把最左边那个匹配的括号看成一个整体,剩下的看成另外一个整体,即左边是一个$A$,右边是若干个$A$的拼接。对于右边可以用$f \left( {k + 1, j} \right)$来表示,左边的话,由于没有用于表示单独一个$A$这样的集合,因此可以用$f \left( {i, k} \right)$来表示,$A$这种情况的集合包含于$f \left( {i, k} \right)$所表示的集合。会有重复,但不影响结果。而$A$可能包含在从$i$开始的不同长度的区间中,因此要枚举不同的区间长度。这种划分方式有点像石子合并这题。

  AC代码如下:

 1 #include <cstdio>
 2 #include <cstring>
 3 #include <algorithm>
 4 using namespace std;
 5 
 6 const int N = 110;
 7 
 8 char str[N];
 9 int f[N][N];
10 
11 int main() {
12     scanf("%s", str + 1);
13     
14     int n = strlen(str + 1);
15     for (int len = 2; len <= n; len++) {
16         for (int i = 1; i + len - 1 <= n; i++) {
17             int j = i + len - 1;
18             if (str[i] == '(' && str[j] == ')' || str[i] == '[' && str[j] == ']') f[i][j] = f[i + 1][j - 1] + 2;
19             f[i][j] = max(f[i][j], max(f[i + 1][j], f[i][j - 1]));
20             
21             for (int k = i + 1; k + 1 < j; k++) {
22                 f[i][j] = max(f[i][j], f[i][k] + f[k + 1][j]);
23             }
24         }
25     }
26     
27     printf("%d", n - f[1][n]);
28     
29     return 0;
30 }

 

参考资料

  AcWing 1222. 密码脱落(蓝桥杯C++ AB组辅导课):https://www.acwing.com/video/780/

  AcWing 897. 最长公共子序列(闫氏DP分析法):https://www.acwing.com/video/946/

  AcWing 1070. 括号配对(蓝桥杯C++ AB组辅导课):https://www.acwing.com/video/791/

posted @ 2022-03-12 12:17  onlyblues  阅读(105)  评论(0编辑  收藏  举报
Web Analytics