2060-同源字符串检测
原字符串由小写字母组成,可以按下述步骤编码:
- 任意将其 分割 为由若干 非空 子字符串组成的一个 序列 。
- 任意选择序列中的一些元素(也可能不选择),然后将这些元素替换为元素各自的长度(作为一个数字型的字符串)。
- 重新 顺次连接 序列,得到编码后的字符串。
例如,编码 "abcdefghijklmnop"
的一种方法可以描述为:
- 将原字符串分割得到一个序列:
["ab", "cdefghijklmn", "o", "p"]
。 - 选出其中第二个和第三个元素并分别替换为它们自身的长度。序列变为
["ab", "12", "1", "p"]
。 - 重新顺次连接序列中的元素,得到编码后的字符串:
"ab121p"
。
给你两个编码后的字符串 s1
和 s2
,由小写英文字母和数字 1-9
组成。如果存在能够同时编码得到 s1
和 s2
原字符串,返回true
;否则,返回 false
。
注意: 生成的测试用例满足 s1
和 s2
中连续数字数不超过 3
。
示例 1:
**输入:** s1 = "internationalization", s2 = "i18n"
**输出:** true
**解释:** "internationalization" 可以作为原字符串
- "internationalization"
-> 分割: ["internationalization"]
-> 不替换任何元素
-> 连接: "internationalization",得到 s1
- "internationalization"
-> 分割: ["i", "nternationalizatio", "n"]
-> 替换: ["i", "18", "n"]
-> 连接: "i18n",得到 s2
示例 2:
**输入:** s1 = "l123e", s2 = "44"
**输出:** true
**解释:** "leetcode" 可以作为原字符串
- "leetcode"
-> 分割: ["l", "e", "et", "cod", "e"]
-> 替换: ["l", "1", "2", "3", "e"]
-> 连接: "l123e",得到 s1
- "leetcode"
-> 分割: ["leet", "code"]
-> 替换: ["4", "4"]
-> 连接: "44",得到 s2
示例 3:
**输入:** s1 = "a5b", s2 = "c5b"
**输出:** false
**解释:** 不存在这样的原字符串
- 编码为 s1 的字符串必须以字母 'a' 开头
- 编码为 s2 的字符串必须以字母 'c' 开头
示例 4:
**输入:** s1 = "112s", s2 = "g841"
**输出:** true
**解释:** "gaaaaaaaaaaaas" 可以作为原字符串
- "gaaaaaaaaaaaas"
-> 分割: ["g", "aaaaaaaaaaaa", "s"]
-> 替换: ["1", "12", "s"]
-> 连接: "112s",得到 s1
- "gaaaaaaaaaaaas"
-> 分割: ["g", "aaaaaaaa", "aaaa", "s"]
-> 替换: ["g", "8", "4", "1"]
-> 连接 "g841",得到 s2
示例 5:
**输入:** s1 = "ab", s2 = "a2"
**输出:** false
**解释:** 不存在这样的原字符串
- 编码为 s1 的字符串由两个字母组成
- 编码为 s2 的字符串由三个字母组成
提示:
1 <= s1.length, s2.length <= 40
s1
和s2
仅由数字1-9
和小写英文字母组成s1
和s2
中连续数字数不超过3
方法一:动态规划
思路与算法
我们可以使用动态规划来解决本题。
记 f[i][j][\textit{which}][\textit{rest}] 表示是否存在一种解码方案,使得字符串 s_1 的第 0, 1, \cdots, i 个字符可以与 s_2 的第 0, 1, \cdots, j 个字符相匹配,并且:
如果 which} = 0,那么字符串 s_1 还多出了 rest 个任意字符;
如果 which} = 1,那么字符串 s_2 还多出了 rest 个任意字符。
这里的任意字符指的是:当遇到字符串中的数字时,我们先将对应的数量存下来,便于进行后续的匹配。
在进行状态转移时,我们可以考虑 which} = 0 时的若干种情况:
- 如果 rest} = 0 并且 s_1[i+1] 和 s_2[j+1] 都是字母,那么必须有 s_1[i+1] = s_2[j+1],即:
f[i][j][0][0] \to f[i+1][j+1][0][0], \quad s_1[i+1] = s_2[j+1]
如果 s_2[j+1] 是字母(但 rest} = 0 和 s_2[j+1] 是字母不同时满足),那么需要分 rest} \geq 1 和 rest} = 0 两种情况进行讨论:
- 如果 rest} \geq 1,那么 s_2[j+1] 会消耗一个 s_1 多出的任意字符,即:
f[i][j][0][\textit{rest}] \to f[i][j+1][0][\textit{rest}-1]
- 如果 rest} = 0,那么 s_2[j+1] 需要未来 s_1 的一个任意字符去匹配,即:
f[i][j][0][0] \to f[i][j+1][1][0]
如果 s_2[j+1] 是数字,那么我们取出 s_2[j+1] 到 s_2[j+k] 所表示的数(根据题目描述,1 \leq k \leq 3),记为 x,那么需要分 rest} \geq x 和 rest} < x 两种情况进行讨论:
- 如果 rest} \geq x,那么 s_2[j+1] 到 s_2[j+k] 会消耗 x 个多出的任意字符,即:
f[i][j][0][\textit{rest}] \to f[i][j+k+1][0][\textit{rest}-x]
- 如果 rest} < x,那么在消耗完 x 个多出的任意字符后,s_2[j+1] 到 s_2[j+k] 还需要未来 s_1 的 x - \textit{rest 个任意字符去匹配,即:
f[i][j][0][\textit{rest}] \to f[i][j+k+1][1][x-\textit{rest}]
如果 j = |s_2| - 1,即 s_2 中的所有字符均已解码完成,那么 rest 必须为 0 并且 i = |s_1| - 1,否则 s_1 中剩余的每一个字符至少能解码出一个字符,s_1 和 s_2 就不可能匹配了,即:
f[i][|s_2|-1][0][\textit{rest}] = \begin{cases}
\text{True}, & \quad i=|s_1|-1 \text{and} \textit{rest} = 0 \
\text{False}, & \quad \text{otherwise}
\end{cases}
我们发现,当 which} = 0 时,除了 s_1[i+1] 和 s_2[j+1] 均为字母的情况外,我们只会在字符串 s_2 中选择从位置 j+1 开始的若干字符进行匹配,而暂不考虑 s_1。这样做的好处在于,我们总是选择「当前解码长度较小的字符串」,由于连续的数字最多可以对应 999 个字符,那么 rest 的值也永远不会超过 999。
当 which} = 1 时,我们考虑的若干种情况是类似的。
细节
我们可以根据上述状态转移方程编写自顶向下的记忆化搜索的代码,从初始状态 f[-1][-1][0][0] 开始搜索,比普通的动态规划更加直观。由于大部分语言中并不能将负数 -1 作为数组的索引,因此下面的代码中将 i 和 j 对应的索引全部增加 1。
代码
1 | class Solution { |
1 | class Solution: |
复杂度分析
时间复杂度:O(mnd \cdot 10^d),其中 m 是字符串 s_1 的长度,n 是字符串 s_2 的长度,d 是连续数字的最大位数,本题中 d=3。动态规划中的状态一共有 i, j, \textit{which}, \textit{rest 四个维度,它们的范围分别为 O(m), O(n), O(1), O(10^d),而每个状态 f[i][j][\textit{which}][\textit{rest}] 需要 O(d) 的时间进行计算,因此总时间复杂度为 O(mnd \cdot 10^d)。
空间复杂度:O(mn \cdot 10^d),记为动态规划中存储所有状态 f 需要的空间。