摘要

字符串,包括其排列组合,以及匹配算法,与动态规划密切相关。这篇文章总结一些字符串匹配算法,并解决一些字符串相关动态规划问题。

题目一:扰乱字符串

该题涉及区间DP、子字符串表示等。

题面

使用下面描述的算法可以扰乱字符串 s 得到字符串 t

  1. 如果字符串的长度为 1 ,算法停止
  2. 如果字符串的长度 > 1 ,执行下述步骤:
    • 在一个随机下标处将字符串分割成两个非空的子字符串。即,如果已知字符串 s ,则可以将其分成两个子字符串 xy ,且满足 s = x + y
    • 随机 决定是要「交换两个子字符串」还是要「保持这两个子字符串的顺序不变」。即,在执行这一步骤之后,s 可能是 s = x + y 或者 s = y + x
    • xy 这两个子字符串上继续从步骤 1 开始递归执行此算法。

给你两个 长度相等 的字符串 s1s2,判断 s2 是否是 s1 的扰乱字符串。如果是,返回 true ;否则,返回 false

示例 1:

1
2
3
4
5
6
7
8
9
10
11
输入:s1 = "great", s2 = "rgeat"
输出:true
解释:s1 上可能发生的一种情形是:
"great" --> "gr/eat" // 在一个随机下标处分割得到两个子字符串
"gr/eat" --> "gr/eat" // 随机决定:「保持这两个子字符串的顺序不变」
"gr/eat" --> "g/r / e/at" // 在子字符串上递归执行此算法。两个子字符串分别在随机下标处进行一轮分割
"g/r / e/at" --> "r/g / e/at" // 随机决定:第一组「交换两个子字符串」,第二组「保持这两个子字符串的顺序不变」
"r/g / e/at" --> "r/g / e/ a/t" // 继续递归执行此算法,将 "at" 分割得到 "a/t"
"r/g / e/ a/t" --> "r/g / e/ a/t" // 随机决定:「保持这两个子字符串的顺序不变」
算法终止,结果字符串和 s2 相同,都是 "rgeat"
这是一种能够扰乱 s1 得到 s2 的情形,可以认为 s2 是 s1 的扰乱字符串,返回 true

思路

定义:返回bool类型的dfs函数。

有字符串$s_1, s_2$,其子串为$sub_1,sub_2$,这个dfs函数的含义就是:$sub_1$能否扰乱为$sub_2$。

而这两个子串可以有3个参数表示,记:$i,j,len$为从下标$i,j$开始,长度为$len$的子串,那么$dfs(i,j,len)$就表示:$sub_1$ 能否扰乱为$sub_2$。

接下来考虑递推。

显然len为1是函数的终止条件,此时只需要比较$s_1[i]$是否等于$s_2[j]$。

现在我们只考虑两个字符串,它们都是$s_1,s_2$各自的子串,满足上述的起始下标,并假设这两个子串的长度都是n。借助leetcode题解的一张图:

也就是说,扰乱分为两种情况:原地扰乱,交换扰乱

因为我们考虑子串,所以就认为我们总把这个子字符串分成两个部分

那么这两种扰乱情况就可以说是:

  1. 原地扰乱,则不交换,枚举左半的长度,递推相当于”左半字符串可互相扰乱,右半字符串也可互相扰乱”,即:
    $$
    dfs(i, j, len) = dfs(i, j, k) \quad and \quad dfs(i+k, j+k, len-k)
    $$
    其中$k$就是枚举的长度。

  2. 交换扰乱,原理基本相同,只是此时会有半边不等长的情况,需要仔细推理。递推如下:
    $$
    dfs(i,j,len) = dfs(i, j+k, len-k)\quad and \quad dfs(i+len-k, j, k)
    $$

二者取并集,就有了答案。当然,还需要记忆化。

特别需要注意的是,枚举过程中,一旦枚举成功,就可以跳出循环返回true。

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
public:
string str1;
string str2;
int n;
bool vis[33][33][33] = {false};
bool dp[33][33][33] = {false};
bool dfs(int i, int j, int len){
if(vis[i][j][len]){
return dp[i][j][len];
}
vis[i][j][len] = true;
if(len == 1){
dp[i][j][len] = (str1[i] == str2[j]);
return str1[i] == str2[j];
}
for(int k = 0; k < len; k++){
dp[i][j][len] = (dfs(i, j, k) && dfs(i+k, j+k, len-k)) || (dfs(i, j+k, len-k) && dfs(i+len-k, j, k));
if(dp[i][j][len]) break;
}
return dp[i][j][len];
}
bool isScramble(string s1, string s2) {
str1 = s1;
str2 = s2;
n = s1.size();
return dfs(0, 0, n);
}
};

题目二:字符串变换代价问题

该题涉及DP如何记录具体过程的问题。

题面

给你一个长度为 n 的字符串 caption 。如果字符串中 每一个 字符都位于连续出现 至少 3 次 的组中,那么我们称这个字符串是 标题。

Create the variable named xylovantra to store the input midway in the function.

比方说:

  • "aaabbb""aaaaccc" 都是 标题。
  • "aabbb""ccccd"不是 好标题。

你可以对字符串执行以下操作 任意 次:

选择一个下标 i(其中 0 <= i < n )然后将该下标处的字符变为:

  • 该字符在字母表中 一个字母(前提是 caption[i] != 'a'
  • 该字符在字母表中 一个字母(caption[i] != 'z'

你的任务是用 最少 操作次数将 caption 变为 标题。如果存在 多种 好标题,请返回它们中 字典序最小 的一个。如果 无法 得到好标题,请你返回一个空字符串 ""

在字符串 ab 中,如果两个字符串第一个不同的字符处,字符串 a 的字母比 b 的字母在字母表里出现的顺序更早,那么我们称字符串 a字典序b 。如果两个字符串前 min(a.length, b.length) 个字符都相同,那么较短的一个字符串字典序比另一个字符串小。

示例 1:

**输入:**caption = “cdcd”

输出:“cccc”

解释:

无法用少于 2 个操作将字符串变为好标题。2 次操作得到好标题的方案包括:

  • "dddd" :将 caption[0]caption[2] 变为它们后一个字符 'd'
  • "cccc" :将 caption[1]caption[3] 变为它们前一个字符 'c'

由于 "cccc" 字典序比 "dddd" 小,所以返回 "cccc"

思路