LeetCode 题目 97:动态规划、递归到广度优先搜索BFS 实现交错字符串

简介: LeetCode 题目 97:动态规划、递归到广度优先搜索BFS 实现交错字符串

作者介绍:10年大厂数据\经营分析经验,现任大厂数据部门负责人。

会一些的技术:数据分析、算法、SQL、大数据相关、python

欢迎加入社区:码上找工作

作者专栏每日更新:

LeetCode解锁1000题: 打怪升级之旅

python数据分析可视化:企业实战案例

python源码解读

程序员必备的数学知识与应用

备注说明:方便大家阅读,统一使用python,带必要注释,公众号 数据分析螺丝钉 一起打怪升级

题目描述

给定三个字符串 s1, s2, 和 s3,验证 s3 是否是由 s1s2 交错组成的。

输入格式
  • s1, s2, s3:三个字符串。
输出格式
  • 返回布尔值,表示 s3 是否可以由 s1s2 交错组成。

示例

示例 1
输入: s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
输出: True
示例 2
输入: s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
输出: False

方法一:动态规划

解题步骤
  1. 定义状态dp[i][j] 表示 s1 的前 i 个字符和 s2 的前 j 个字符能否形成 s3 的前 i+j 个字符。
  2. 状态转移
  • 如果当前字符来自 s1,则 dp[i][j] = dp[i-1][j]s1[i-1] 必须等于 s3[i+j-1]
  • 如果当前字符来自 s2,则 dp[i][j] = dp[i][j-1]s2[j-1] 必须等于 s3[i+j-1]
完整的规范代码
def isInterleave(s1, s2, s3):
    """
    动态规划判断 s3 是否为 s1 和 s2 交错组成
    :param s1: str, 第一个字符串
    :param s2: str, 第二个字符串
    :param s3: str, 第三个字符串
    :return: bool, 是否交错组成
    """
    l1, l2, l3 = len(s1), len(s2), len(s3)
    if l1 + l2 != l3:
        return False
    
    dp = [[False] * (l2 + 1) for _ in range(l1 + 1)]
    dp[0][0] = True
    for i in range(1, l1 + 1):
        dp[i][0] = dp[i-1][0] and s1[i-1] == s3[i-1]
    for j in range(1, l2 + 1):
        dp[0][j] = dp[0][j-1] and s2[j-1] == s3[j-1]
    for i in range(1, l1 + 1):
        for j in range(1, l2 + 1):
            dp[i][j] = (dp[i-1][j] and s1[i-1] == s3[i+j-1]) or (dp[i][j-1] and s2[j-1] == s3[i+j-1])
    return dp[l1][l2]
# 示例调用
print(isInterleave("aabcc", "dbbca", "aadbbcbcac"))  # 输出: True
算法分析
  • 时间复杂度:(O(m * n)),其中 mn 分别为 s1s2 的长度。
  • 空间复杂度:(O(m * n)),用于存储动态规划表。
算法图解

使用动态规划来具体解释并展示如何验证字符串 s1 = "aabcc", s2 = "dbbca", 和 s3 = "aadbbcbcac" 是否是交错字符串的过程。这里将详细构建对应的动态规划表格,并用 ASCII 图表来形象化展示。

表格准备
  • 行代表 s1 的前缀。
  • 列代表 s2 的前缀。
  • 单元格 (i, j) 表示使用 s1 的前 i 个字符和 s2 的前 j 个字符能否交错组成 s3 的前 i+j 个字符。
初始化
  • dp[0][0] 自然为 True(两个空字符串总是可以组成另一个空字符串)。
  • 第一行和第一列需要单独初始化,因为它们代表其中一个字符串是空的情况。
动态规划过程

我们将遍历 s3 并逐个填充 dp 表的值,检查是否能通过添加 s1s2 的字符来匹配 s3 的对应位置。

ASCII 表的表示
DP Table:
          ""   d   b   b   c   a
      "" [T]  [F]  [F]  [F]  [F]  [F]
      a  [F]  [F]  [F]  [F]  [F]  [F]
      a  [F]  [F]  [F]  [F]  [F]  [F]
      b  [F]  [F]  [F]  [F]  [F]  [F]
      c  [F]  [F]  [F]  [F]  [F]  [F]
      c  [F]  [F]  [F]  [F]  [F]  [F]
递推逻辑的 ASCII 说明

以填充 dp[1][1] 为例:

dp[1][1] = (dp[0][1] and s1[0] == s3[1]) or (dp[1][0] and s2[0] == s3[1])
         = (False and 'a' == 'a') or (False and 'd' == 'a')  // 给出示例中的字符比较
         = False or False
         = False
完整的 DP 表填充过程

为了简明起见,我们仅展示关键步骤,下面是 s3 完整匹配的 DP 表最后几行状态:

""   d   b   b   c   a
      "" [T]  [F]  [F]  [F]  [F]  [F]
      a  [F]  [F]  [F]  [F]  [F]  [F]
      a  [F]  [F]  [F]  [F]  [F]  [F]
      b  [F]  [F]  [T]  [T]  [F]  [F]
      c  [F]  [F]  [F]  [T]  [T]  [F]
      c  [F]  [F]  [F]  [F]  [T]  [T]

这个 ASCII 表揭示了通过动态规划如何逐步确认 s3 是否能由 s1s2 交错组成。整个过程的可视化有助于理解交错字符串问题的动态规划解决方案的复杂性和执行流程。

方法二:递归(带记忆化)

解题步骤
  1. 递归定义:递归判断是否可能交错组成 s3
  2. 记忆化存储:使用哈希表避免重复计算相同的子问题。
完整的规范代码
def isInterleave(s1, s2, s3):
    memo = {}
    def dfs(i, j, k):
        if (i, j) in memo:
            return memo[(i, j)]
        if k == len(s3):
            return i == len(s1) and j == len(s2)
        valid = (i < len(s1) and s1[i] == s3[k] and dfs(i+1, j, k+1)) or \
                (j < len(s2) and s2[j] == s3[k] and dfs(i, j+1, k+1))
        memo[(i, j)] = valid
        return valid
    return dfs(0, 0, 0)
# 示例调用
print(isInterleave("aabcc", "dbbca", "aadbbcbcac"))  # 输出: True
算法分析
  • 时间复杂度:(O(m * n)),利用记忆化减少了重复计算。
  • 空间复杂度:(O(m * n)),递归栈和记忆化存储的开销。

方法三:广度优先搜索 (BFS)

解题步骤
  1. 使用队列:利用队列进行广度优先搜索,每次从队列中取出节点,并尝试向前推进 s1s2
  2. 状态检查:检查当前的字符匹配情况,并决定是否继续推进。
完整的规范代码
from collections import deque
def isInterleave(s1, s2, s3):
    if len(s1) + len(s2) != len(s3):
        return False
    queue = deque([(0, 0)])
    visited = set((0, 0))
    while queue:
        i, j = queue.popleft()
        if i + j == len(s3):
            return True
        if i < len(s1) and s1[i] == s3[i + j] and (i + 1, j) not in visited:
            queue.append((i + 1, j))
            visited.add((i + 1, j))
        if j < len(s2) and s2[j] == s3[i + j] and (i, j + 1) not in visited:
            queue.append((i, j + 1))
            visited.add((i, j + 1))
    return False
# 示例调用
print(isInterleave("aabcc", "dbbca", "aadbbcbcac"))  # 输出: True
算法分析
  • 时间复杂度:(O(m * n)),可能需要遍历所有可能的状态。
  • 空间复杂度:(O(m * n)),用于存储访问状态的队列和集合。
算法图解

我们可以通过 ASCII 图解来形象化展示其搜索过程。以下是如何使用 BFS 方法来判断字符串 s1 = "aabcc", s2 = "dbbca", 和 s3 = "aadbbcbcac" 是否交错组成的一个步骤示例。

在这个方法中,我们将使用一个队列来追踪当前访问的节点位置 (i, j),其中 is1 的索引,js2 的索引。这样,每个元素的状态可以表示为它是否能由之前的状态推进而来。

BFS 的队列操作过程

我们初始化队列与访问集合来确保不重复处理相同的状态。队列中的每个元素都代表一个待验证的状态,即能否通过当前索引组合 (i, j) 交错组成 s3i+j 部分。

ASCII 表的表示

下面是一个简化的 ASCII 表示,说明了状态的扩展过程:

Queue Operations:
Initial Queue State: [(0, 0)]  // Start from the empty prefixes of s1 and s2
Step 1: Pop (0, 0)
        Check (1, 0) -> 'a' from s1 matches 'a' from s3 -> Enqueue (1, 0)
        Check (0, 1) -> 'd' from s2 does not match 'a' from s3 -> Do not enqueue
        Queue Now: [(1, 0)]
Step 2: Pop (1, 0)
        Check (2, 0) -> 'a' from s1 matches 'a' from s3 -> Enqueue (2, 0)
        Check (1, 1) -> 'd' from s2 does not match 'a' from s3 -> Do not enqueue
        Queue Now: [(2, 0)]
Step 3: Pop (2, 0)
        Check (3, 0) -> 'b' from s1 matches 'b' from s3 -> Enqueue (3, 0)
        Check (2, 1) -> 'd' from s2 does not match 'b' from s3 -> Do not enqueue
        Queue Now: [(3, 0)]
Step 4: Pop (3, 0)
        Check (4, 0) -> 'c' from s1 matches 'c' from s3 -> Enqueue (4, 0)
        Check (3, 1) -> 'd' from s2 does not match 'c' from s3 -> Do not enqueue
        Queue Now: [(4, 0)]
Step 5: Pop (4, 0)
        Check (5, 0) -> Index out of range for s1 -> Do not enqueue
        Check (4, 1) -> 'a' from s2 matches 'a' from s3 -> Enqueue (4, 1)
        Queue Now: [(4, 1)]
Continue until the queue is empty or (len(s1), len(s2)) is reached, indicating success.

这种 ASCII 图解方式直观地展示了 BFS 方法如何逐步探索不同的状态路径,来验证 s3 是否能由 s1s2 交错形成。通过这样的可视化,可以更容易地理解和教授交错字符串问题的 BFS 解决方案,尤其是在探讨算法课程或演示中。

不同算法的优劣势对比

特征 方法一:动态规划 方法二:递归(带记忆化) 方法三:BFS
时间复杂度 (O(m * n)) (O(m * n)) (O(m * n))
空间复杂度 (O(m * n)) (O(m * n)) (O(m * n))
优势 结构清晰、易于实现 避免重复计算、提高效率 适合实时解决问题,避免深层递归
劣势 需要较大空间 递归深度可能较大 空间利用率不一定高,实现稍复杂

应用示例

这些方法适用于任何需要解析和验证交错字符串的场景,例如编译器的字符串解析、自然语言处理中的句子解构,或者在软件开发中验证输入格式的正确性。


欢迎关注微信公众号 数据分析螺丝钉

相关文章
|
2月前
|
JavaScript
力扣3333.找到初始输入字符串Ⅱ
【10月更文挑战第9天】力扣3333.找到初始输入字符串Ⅱ
34 1
|
2月前
|
C++
Leetcode第43题(字符串相乘)
本篇介绍了一种用C++实现的字符串表示的非负整数相乘的方法,通过逆向编号字符串,将乘法运算转化为二维数组的累加过程,最后处理进位并转换为字符串结果,解决了两个大数相乘的问题。
25 9
|
2月前
|
程序员 C语言
【C语言】LeetCode(力扣)上经典题目
【C语言】LeetCode(力扣)上经典题目
|
2月前
|
算法 C++
Leetcode第八题(字符串转换整数(atoi))
这篇文章介绍了LeetCode上第8题“字符串转换整数(atoi)”的解题思路和C++的实现方法,包括处理前导空格、正负号、连续数字字符以及整数溢出的情况。
18 0
|
2月前
【LeetCode 22】459.重复的子字符串
【LeetCode 22】459.重复的子字符串
30 0
|
2月前
【LeetCode 20】151.反转字符串里的单词
【LeetCode 20】151.反转字符串里的单词
19 0
|
3月前
|
Unix Shell Linux
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
本文提供了几个Linux shell脚本编程问题的解决方案,包括转置文件内容、统计词频、验证有效电话号码和提取文件的第十行,每个问题都给出了至少一种实现方法。
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
|
4月前
|
Python
【Leetcode刷题Python】剑指 Offer 32 - III. 从上到下打印二叉树 III
本文介绍了两种Python实现方法,用于按照之字形顺序打印二叉树的层次遍历结果,实现了在奇数层正序、偶数层反序打印节点的功能。
58 6
|
4月前
|
搜索推荐 索引 Python
【Leetcode刷题Python】牛客. 数组中未出现的最小正整数
本文介绍了牛客网题目"数组中未出现的最小正整数"的解法,提供了一种满足O(n)时间复杂度和O(1)空间复杂度要求的原地排序算法,并给出了Python实现代码。
119 2
|
29天前
|
机器学习/深度学习 人工智能 自然语言处理
280页PDF,全方位评估OpenAI o1,Leetcode刷题准确率竟这么高
【10月更文挑战第24天】近年来,OpenAI的o1模型在大型语言模型(LLMs)中脱颖而出,展现出卓越的推理能力和知识整合能力。基于Transformer架构,o1模型采用了链式思维和强化学习等先进技术,显著提升了其在编程竞赛、医学影像报告生成、数学问题解决、自然语言推理和芯片设计等领域的表现。本文将全面评估o1模型的性能及其对AI研究和应用的潜在影响。
25 1