洞察 LeetCode:化繁为简,攻克「792. 匹配子序列的单词数」
2023-12-23 10:57:29
算法竞赛:LeetCode 792 题解——匹配子序列的单词数
前言
在算法竞赛领域,LeetCode 作为领军者,汇聚了众多引人入胜的难题,激发着无数开发者的求知欲和挑战精神。今天,让我们深入剖析 LeetCode 的一道中等难度题目——「792. 匹配子序列的单词数」。
问题概述
「匹配子序列的单词数」要求我们找出给定单词数组 words
中有多少个单词是源字符串 s
的子序列。子序列是指从原始字符串中删除任意数量字符(包括不删除任何字符)而形成的新字符串。
例如,字符串 "abc" 是 "abac" 的子序列,"ab" 也是 "abac" 的子序列。
直观解法
最直接的解法是逐个比较 s
和 words
中的每个单词。对于每个单词 word
,我们可以遍历 s
,检查 word
中的每个字符是否按顺序出现在 s
中。如果所有字符都存在,则 word
是 s
的子序列,我们增加计数器。
虽然直观易懂,但这种方法的时间复杂度为 O(s * n * m)
,其中 s
是 s
的长度,n
是 words
中单词的数量,m
是 word
的平均长度。对于海量数据,这种方法的效率会非常低下。
动态规划解法
为了提高效率,我们可以采用动态规划的方法。我们定义一个二维数组 dp
,其中 dp[i][j]
表示 s
的前 i
个字符和 word
的前 j
个字符的匹配情况。
初始化 dp
数组:
dp[0][0] = 1 # 空串与空串匹配
递推公式:
dp[i][j] = dp[i - 1][j] # s[i] 与 word[j] 不匹配,跳过 s[i]
if s[i] == word[j]:
dp[i][j] += dp[i - 1][j - 1] # s[i] 与 word[j] 匹配
最终结果为 dp[s.length][word.length]
。
代码实现
def numMatchingSubseq(s, words):
dp = [[0] * (len(word) + 1) for word in words]
for i in range(len(s)):
for j, word in enumerate(words):
if not word:
dp[j][0] = 1
elif s[i] == word[0]:
dp[j][1] += dp[j][0]
else:
dp[j][1] = dp[j][0]
for k in range(1, len(word)):
if s[i] == word[k]:
dp[j][k + 1] += dp[j][k]
return [dp[i][-1] for i in range(len(words))]
复杂度分析
动态规划解法的时间复杂度仍为 O(s * n * m)
,但由于避免了不必要的比较,因此常数项更小,效率更高。
总结
通过解决这道 LeetCode 难题,我们深入理解了子序列的概念,掌握了动态规划这一重要的算法思想。面对算法问题,我们需要灵活运用合适的解法,不断提升自己的算法技能和编程素养。愿每一位算法爱好者都能在 LeetCode 的海洋中乘风破浪,所向披靡!
常见问题解答
1. 如何判断一个单词是否是子序列?
一个单词是子序列,当且仅当它可以从另一个字符串中删除任意数量的字符(包括不删除任何字符)而得到。
2. 动态规划解法是如何工作的?
动态规划解法通过构造二维数组 dp
,其中 dp[i][j]
表示 s
的前 i
个字符和 word
的前 j
个字符的匹配情况,从而避免不必要的比较,提高效率。
3. 动态规划解法的递推公式是如何推导的?
递推公式 dp[i][j]
的推导基于这样的思考:如果 s[i]
与 word[j]
匹配,那么 dp[i][j]
等于 dp[i - 1][j - 1]
(s[i]
匹配 word[j]
)加上 dp[i - 1][j]
(s[i]
不匹配 word[j]
);如果 s[i]
不与 word[j]
匹配,那么 dp[i][j]
等于 dp[i - 1][j]
(s[i]
跳过)。
4. 直观解法和动态规划解法的主要区别是什么?
直观解法逐个比较 s
和 words
中的每个单词,而动态规划解法通过构造 dp
数组,避免不必要的比较,提高效率。
5. 这道题目在算法竞赛中应用广泛吗?
是的,这道题目作为一道中等难度的 LeetCode 题,经常出现在算法竞赛中,考察选手的算法思维和动态规划技巧。