同源字符串检测

标签: 字符串 动态规划

难度: Hard

原字符串由小写字母组成,可以按下述步骤编码:

  • 任意将其 分割 为由若干 非空 子字符串组成的一个 序列
  • 任意选择序列中的一些元素(也可能不选择),然后将这些元素替换为元素各自的长度(作为一个数字型的字符串)。
  • 重新 顺次连接 序列,得到编码后的字符串。

例如,编码 "abcdefghijklmnop" 的一种方法可以描述为:

  • 将原字符串分割得到一个序列:["ab", "cdefghijklmn", "o", "p"]
  • 选出其中第二个和第三个元素并分别替换为它们自身的长度。序列变为 ["ab", "12", "1", "p"]
  • 重新顺次连接序列中的元素,得到编码后的字符串:"ab121p"

给你两个编码后的字符串 s1s2 ,由小写英文字母和数字 1-9 组成。如果存在能够同时编码得到 s1s2 原字符串,返回 true ;否则,返回 false

注意:生成的测试用例满足 s1s2 中连续数字数不超过 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
  • s1s2 仅由数字 1-9 和小写英文字母组成
  • s1s2 中连续数字数不超过 3

Submission

运行时间: 603 ms

内存: 114.8 MB

class Solution:
    def possiblyEquals(self, s1: str, s2: str) -> bool:
        m = len(s1)  # s1的长度
        n = len(s2)  # s2的长度
        memo = dict()  # 用于记忆化递归的字典

        # diff:跟踪两个字符串 s1和s2之间的潜在字符差异。
        # 具体来说,它表示在某个点上s1相对于s2的额外或缺少的字符数量。
        def helper(i, j, diff):
            # 如果两个字符串都遍历完成,并且差异为0,则返回True
            if i == m and j == n:
                return diff == 0
            # 如果当前状态已经计算过,则直接返回之前的结果
            if (i, j, diff) in memo:
                return memo[(i, j, diff)]

            # 如果s1的当前字符是数字
            if i < m and s1[i].isdigit():
                num = 0  # 用于记录数字的变量
                k = i
                # 计算数字的值
                while k < m and s1[k].isdigit():
                    num = num * 10 + int(s1[k])
                    k += 1
                    # 递归检查在考虑这个数字代表的重复次数后,两个字符串是否可能等价
                    if helper(k, j, diff + num):
                        return True
            # 如果s2的当前字符是数字:
            # why elif? especially in a scenario where multiple conditions are being checked sequentially.
            # It ensures that only one branch of the logic is executed for each iteration or recursion
            elif j < n and s2[j].isdigit():
                num = 0
                k = j
                # 计算数字的值
                while k < n and s2[k].isdigit():
                    num = num * 10 + int(s2[k])
                    k += 1
                    # 递归检查在考虑这个数字代表的重复次数后,两个字符串是否可能等价
                    if helper(i, k, diff- num):
                        return True
            # 如果当前没有字符重复的差异
            elif diff == 0:
                # 如果当前字符相同,则继续递归比较下一个字符
                if i < m and j < n and s1[i] == s2[j]:
                    if helper(i+1, j+1, diff):
                        return True
            # 如果s1需要更多字符来匹配s2
            elif diff > 0:
                if j < n:
                    if helper(i, j+1, diff - 1):
                        return True
            # 如果s2需要更多字符来匹配s1
            elif diff < 0:
                if i < m:
                    if helper(i+1, j, diff + 1):
                        return True

            # 如果当前状态不能使两个字符串等价,记录这个结果并返回False
            memo[(i, j, diff)] = False
            return False

        # 从两个字符串的起始位置开始递归检查
        return helper(0, 0, 0)
# s1 = "L3E",
# s2 = "L1E2"
# 初始状态:diff = 0,因为两个字符串初始同步开始。

# 在s1中,数字是3,意味着有3个字符。
# 在s2中,紧接"L"的数字是1,意味着有1个字符,然后是"E",然后是另外2个字符。
# 更新diff = 3 - 1 = 2。

# 由于s2中"E"之后还有2个字符,而在s1中,"E"之后没有更多的字符,这与我们之前计算的diff = 2相匹配,
# 表明在"E"之后,s1和s2可能有相同数量的额外字符。
# 根据diff的最终值和对字符串的分析,我们可以推断s1和s2可能表示同一个原始字符串,即"LXXXE",
# 其中"X"表示任意字符。

Explain

此题解采用递归回溯的方式配合记忆化搜索解决问题。核心思想是通过递归函数探索两个编码字符串s1和s2是否可能由同一原字符串通过不同的编码方式得到。在递归函数中,使用三个主要参数i, j, diff分别表示当前在s1和s2中的位置以及两者之间的潜在字符差异(diff表示s1相对于s2可能多出或少的字符数)。当遇到数字时,递归探索该数字可能表示的字符数,同时更新diff值。当字符直接相同时,直接比较下一位置。最终,若两字符串能够同时遍历完毕且diff为0,则表示可能由同一原字符串编码得到。

时间复杂度: O(m * n * diff_range)

空间复杂度: O(m * n * diff_range)

class Solution:
    def possiblyEquals(self, s1: str, s2: str) -> bool:
        m = len(s1)
        n = len(s2)
        memo = dict()

        def helper(i, j, diff):
            if i == m and j == n:
                return diff == 0
            if (i, j, diff) in memo:
                return memo[(i, j, diff)]

            if i < m and s1[i].isdigit():
                num = 0
                k = i
                while k < m and s1[k].isdigit():
                    num = num * 10 + int(s1[k])
                    k += 1
                    if helper(k, j, diff + num):
                        return True
            elif j < n and s2[j].isdigit():
                num = 0
                k = j
                while k < n and s2[k].isdigit():
                    num = num * 10 + int(s2[k])
                    k += 1
                    if helper(i, k, diff - num):
                        return True
            elif diff == 0:
                if i < m and j < n and s1[i] == s2[j]:
                    if helper(i+1, j+1, diff):
                        return True
            elif diff > 0:
                if j < n:
                    if helper(i, j+1, diff - 1):
                        return True
            elif diff < 0:
                if i < m:
                    if helper(i+1, j, diff + 1):
                        return True

            memo[(i, j, diff)] = False
            return False

        return helper(0, 0, 0)

Explore

在递归函数中,`diff` 参数表示两个字符串 s1 和 s2 在当前位置的潜在字符数差异。如果 `diff` 为正,表示 s1 比 s2 长,即 s1 有更多未匹配的字符;如果 `diff` 为负,表示 s2 比 s1 长,即 s2 有更多未匹配的字符。当 `diff` 为正时,递归逻辑会尝试通过增加 s2 的位置索引 `j`(即调用 `helper(i, j+1, diff-1)`)来平衡差异,消耗 s2 的字符以匹配 s1 的额外字符。当 `diff` 为负时,递归逻辑会尝试通过增加 s1 的位置索引 `i`(即调用 `helper(i+1, j, diff+1)`)来平衡差异,消耗 s1 的字符以匹配 s2 的额外字符。这种方法确保了无论字符数差异如何,都可以通过适当调整两个字符串的索引来尝试匹配两者。

当遇到数字时,算法通过解析数字并将其视为可能的字符长度来探索不同的匹配情况。具体实现为,如果在 s1 或 s2 中的当前位置发现数字,会从该位置开始解析出完整的数字(可能是多位数),然后递归探索将该数字视为字符串长度的所有情况。例如,如果 `s1[i]` 是数字,会计算从 `i` 开始的全数字,然后尝试不同的长度从 1 到该数字值,逐一调用递归函数 `helper(k, j, diff + num)`,其中 `k` 是数字后的下一个位置,`num` 是当前探索的长度。这样可以确保覆盖从该位置开始所有可能的字符长度。通过递归地探索每一种可能的长度,可以确保所有情况都被考虑到。

在字符直接相同时,可以直接比较下一位置的原因是,如果两个字符相同,则它们可能来自同一个原字符,因此无需进一步的解码或调整,直接移动到下一个字符继续比较即可。这种假设通常在没有涉及数字编码变化或其他复杂编码情况时成立。然而,这种假设可能不成立的情况包括数字解析后的长度可能与实际字符匹配不一致。例如,如果一个字符串在一个数字后直接跟随相同字符,而另一个字符串中该数字表示不同的字符数,即使当前字符相同,后续的字符和长度处理可能导致不匹配。因此,在处理含有数字的字符串时,需要小心处理每个数字可能引入的复杂性。