当前位置: 首页 > news >正文

动态规划解LeetCode 115:不同子序列计数问题

1. 问题背景与理解

第一次看到LeetCode 115题"不同的子序列"时,我盯着题目描述足足看了五分钟。这道题在动态规划分类中属于中等难度,但它的解法思路却让很多初学者感到困惑。题目要求我们计算字符串s中有多少种不同的子序列等于字符串t,这里的子序列指的是在不改变字符顺序的情况下,通过删除某些字符得到的新字符串。

举个例子,如果s = "rabbbit",t = "rabbit",那么有3种方式可以从s中得到t:

  1. rabb b it (删除第二个b)
  2. ra b bbit (删除第三个b)
  3. rab b bit (删除第四个b)

这个例子生动展示了子序列问题的核心特征——顺序必须保持一致,但允许跳过中间字符。理解这一点对解题至关重要。

2. 暴力递归解法分析

2.1 基础递归思路

最直观的解法是使用递归。我们可以定义递归函数count(i,j),表示在s的前i个字符和t的前j个字符中,t的前j个字符作为子序列出现在s的前i个字符中的次数。

递归的终止条件有两种:

  1. 当j=0时,表示t已经匹配完成,返回1
  2. 当i=0但j>0时,表示s已经用完但t还未匹配完,返回0

递归关系也有两种情况:

  1. 如果s[i-1] == t[j-1],可以选择匹配这个字符,也可以选择不匹配
  2. 如果s[i-1] != t[j-1],只能选择不匹配这个字符

这种递归解法虽然直观,但时间复杂度高达O(2^n),在LeetCode上会超时。不过,理解这个基础解法对后续优化至关重要。

2.2 递归代码实现

def numDistinct(s: str, t: str) -> int: def helper(i, j): if j == 0: return 1 if i == 0: return 0 if s[i-1] == t[j-1]: return helper(i-1, j-1) + helper(i-1, j) else: return helper(i-1, j) return helper(len(s), len(t))

这段代码清晰地展现了递归思路,但在实际运行中,对于较长的字符串(比如s长度100+),性能会急剧下降。

3. 动态规划解法优化

3.1 DP状态定义

为了优化时间复杂度,我们引入动态规划。定义dp[i][j]表示s的前i个字符中t的前j个字符作为子序列出现的次数。这个定义与递归解法中的count(i,j)完全对应。

初始化条件:

  • dp[i][0] = 1 (空字符串是任何字符串的子序列)
  • dp[0][j] = 0 (j>0时,空字符串无法包含非空子序列)

状态转移方程:

  • 当s[i-1] == t[j-1]时:dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
  • 当s[i-1] != t[j-1]时:dp[i][j] = dp[i-1][j]

3.2 DP表格填充示例

以s="rabbbit",t="rabbit"为例:

初始化dp表格大小为(8,7)(包含空字符串情况)

填充过程:

  1. 第一行(除dp[0][0]外)全为0
  2. 第一列全为1
  3. 逐步填充其余单元格

最终dp[7][6] = 3,与示例结果一致。

3.3 DP代码实现

def numDistinct(s: str, t: str) -> int: m, n = len(s), len(t) dp = [[0] * (n + 1) for _ in range(m + 1)] for i in range(m + 1): dp[i][0] = 1 for i in range(1, m + 1): for j in range(1, n + 1): if s[i-1] == t[j-1]: dp[i][j] = dp[i-1][j-1] + dp[i-1][j] else: dp[i][j] = dp[i-1][j] return dp[m][n]

这个解法的时间复杂度为O(mn),空间复杂度也是O(mn),已经比递归解法高效很多。

4. 空间优化技巧

4.1 滚动数组优化

观察状态转移方程,我们发现dp[i][j]只依赖于上一行的数据。因此可以使用一维数组来优化空间复杂度:

def numDistinct(s: str, t: str) -> int: m, n = len(s), len(t) dp = [0] * (n + 1) dp[0] = 1 for i in range(1, m + 1): prev = dp.copy() for j in range(1, n + 1): if s[i-1] == t[j-1]: dp[j] = prev[j-1] + prev[j] else: dp[j] = prev[j] return dp[n]

4.2 反向遍历优化

更巧妙的是,我们可以反向遍历j,这样就不需要额外的prev数组:

def numDistinct(s: str, t: str) -> int: m, n = len(s), len(t) dp = [0] * (n + 1) dp[0] = 1 for i in range(1, m + 1): for j in range(n, 0, -1): if s[i-1] == t[j-1]: dp[j] += dp[j-1] return dp[n]

这种优化将空间复杂度降到了O(n),是面试中最推荐的写法。

5. 边界条件与特殊测试用例

5.1 空字符串处理

  • s为空,t不为空:返回0
  • t为空:返回1(空字符串是任何字符串的子序列)
  • 两者都为空:返回1

5.2 大数溢出问题

当结果很大时(比如s和t都是相同的长字符串),结果可能超过普通整型范围。在Python中这不是问题,但在其他语言如C++中需要考虑使用长整型。

5.3 性能极限测试

对于s="a"*1000,t="a"*100的情况,即使使用DP解法也需要处理较大的计算量。在实际编码中,可以提前判断:

  • 如果len(t) > len(s),直接返回0
  • 如果t为空,直接返回1

6. 类似题目与举一反三

6.1 LeetCode 392. 判断子序列

这道简单题可以看作是本题的简化版,只需要判断是否存在子序列,而不需要计数。

6.2 LeetCode 72. 编辑距离

虽然题目不同,但状态定义和转移思路有相似之处,都是基于两个字符串的匹配。

6.3 LeetCode 1143. 最长公共子序列

LCS问题与子序列计数问题有异曲同工之妙,都是动态规划的经典应用。

7. 面试技巧与常见错误

7.1 面试官可能问的问题

  1. 为什么初始条件是dp[i][0]=1?
  2. 如何从递归解法推导出DP解法?
  3. 空间优化思路是什么?
  4. 如果字符串包含Unicode字符,解法需要修改吗?

7.2 常见错误点

  1. 混淆子序列和子串的概念
  2. 初始化条件设置错误
  3. 索引处理不当(字符串从0开始但dp表从1开始)
  4. 在大数情况下忘记考虑溢出

7.3 代码调试技巧

在实现DP解法时,可以:

  1. 先写出递归解法确保逻辑正确
  2. 打印出完整的DP表格验证中间结果
  3. 用小的测试用例手动计算核对

8. 实际应用场景

虽然这看起来是一道纯算法题,但子序列计数在实际中有重要应用:

  1. DNA序列比对:在生物信息学中,比较基因序列的相似性
  2. 版本控制系统:比较代码文件的变化
  3. 拼写检查:计算单词之间的相似度
  4. 自然语言处理:评估句子相似性

理解子序列问题的解法,可以帮助我们在这些领域设计更高效的算法。

http://www.cnnetsun.cn/news/3944012.html

相关文章:

  • UE5蓝图Tab切换:管理者模式实现UI状态管理与组件复用
  • python hot 100——2 栈(自存)
  • 5个关键问题解决:XUnity.AutoTranslator如何让你的游戏实现零门槛多语言支持
  • 工业物联网时序数据库选型与实践指南
  • 贵阳专业网站建设公司如何打造高效转化网站的全攻略指南
  • 具身智能TVA-World抽象概念学习与知识迁移机制
  • 09 字面量
  • 智慧城市数字孪生IOC的智能体时刻:从数据可视化到自主决策的架构演进
  • MySQL连接问题排查与网络配置优化
  • Python开发环境搭建与PyCharm配置全攻略:从零到高效编程
  • DeepSeek LeetCode 3855. 给定范围内 K 位数字之和 Rust实现
  • 北滘网站建设公司哪家强?揭秘2024年本土企业官网搭建避坑指南与真实案例解析
  • Cloudflare Kitesurf:边缘计算中的轻量级浏览器自动化新方案
  • OpenAI Astra网络能力升级下的智能体安全开发实战指南
  • Cesium与虚幻引擎蓝图UI集成实战:地理可视化交互开发指南
  • Windows C++网络编程:Boost.Asio从环境配置到TCP/UDP实战
  • Java多线程同步:synchronized原理与最佳实践
  • 408计算机组成原理:微程序控制器——概念串联记忆版
  • Python招聘大数据分析系统:从爬虫到可视化全流程解析
  • 揭秘2024年网站建设哪家好xm37真相:老板必看避坑指南与实战建议
  • 显卡驱动彻底清理终极指南:Display Driver Uninstaller 完全解决方案
  • Python类型提示详解:从基础到高级应用
  • 程序员段子背后的实战智慧:从经典梗到云原生避坑指南
  • 从拼错一个单词到命中正确业务数据,深入理解 SAP HANA 与 ABAP CDS 的 Fuzzy Search
  • SpringBoot+MySQL实现大学图书借阅管理系统
  • 如何快速构建现代化WinForm应用:SunnyUI终极控件库完全指南
  • NCM格式解密实战:突破网易云音乐限制的完全攻略
  • C语言高级语法:内存管理与数据结构实战
  • 深入解析吉林市建设局网站功能与民生服务价值,市民必看的权威资讯平台指南
  • 容度原理终极推演:月球上的“反物质矿藏”及其千万亿美元级价值