☰
动态规划解LeetCode 1458:两子序列最大点积问题
2026/10/27 15:22:02 网站建设 项目流程

1. 问题背景与核心概念解析

leetcode 1458题"两个子序列的最大点积"是一道典型的动态规划问题,难度标记为"困难"。这道题要求我们从两个整数数组nums1和nums2中分别选出两个长度相同的非空子序列,使得它们的点积最大化。

子序列是指通过删除一些元素(可能不删除)而不改变剩余元素的相对顺序所得到的新序列。例如,对于数组[1,2,3],[1,3]和[2]都是它的子序列。点积则是两个长度相同的序列对应位置元素相乘后求和的结果。

这道题之所以被标记为困难,是因为它结合了子序列选择和点积计算两个概念,并且需要在所有可能的子序列组合中找到最优解。暴力解法的时间复杂度会非常高,因此需要使用动态规划来优化。

2. 动态规划解法思路拆解

2.1 状态定义

对于这类两个序列的动态规划问题,通常定义dp[i][j]表示考虑nums1前i个元素和nums2前j个元素时能获得的最大点积。这里的i和j分别表示两个数组的索引位置。

但本题的特殊之处在于,我们需要确保选取的子序列长度相同且非空。因此,我们需要额外考虑是否已经选取了元素的情况。更准确的状态定义应该是:

dp[i][j]表示考虑nums1前i个元素和nums2前j个元素时,已经选取了至少一个元素的子序列对的最大点积。

2.2 状态转移方程

状态转移需要考虑以下几种情况:

  1. 选择nums1[i]和nums2[j]作为当前子序列对的最后一个元素:

    • 如果之前已经选择了子序列对,可以接在它们后面:dp[i-1][j-1] + nums1[i-1]*nums2[j-1]
    • 如果之前没有选择任何元素,这就是第一个选择:nums1[i-1]*nums2[j-1]
  2. 不选择nums1[i]:dp[i-1][j]

  3. 不选择nums2[j]:dp[i][j-1]

  4. 既不选择nums1[i]也不选择nums2[j]:dp[i-1][j-1]

因此,完整的转移方程为: dp[i][j] = max( nums1[i-1]*nums2[j-1], # 新开始一个子序列对 dp[i-1][j-1] + nums1[i-1]*nums2[j-1], # 延续之前的子序列对 dp[i-1][j], # 忽略nums1[i] dp[i][j-1], # 忽略nums2[j] dp[i-1][j-1] # 两者都忽略 )

2.3 初始化与边界条件

初始化时,dp[0][j]和dp[i][0]都应该设置为负无穷(或一个非常小的数),因为无法从一个空数组和一个非空数组中选择长度相同的子序列对。

dp[0][0]可以初始化为0,表示两个空数组的情况。

3. 完整代码实现与解析

3.1 Python实现

def maxDotProduct(nums1, nums2): m, n = len(nums1), len(nums2) dp = [[-float('inf')] * (n + 1) for _ in range(m + 1)] for i in range(1, m + 1): for j in range(1, n + 1): current = nums1[i-1] * nums2[j-1] dp[i][j] = max( current, # 新子序列对 dp[i-1][j-1] + current, # 延续之前的 dp[i-1][j], # 忽略nums1[i] dp[i][j-1], # 忽略nums2[j] dp[i-1][j-1] # 两者都忽略 ) return dp[m][n]

3.2 代码解析

  1. 初始化一个(m+1)×(n+1)的dp数组,所有值初始化为负无穷,表示初始状态下无法形成有效的子序列对。
  2. 双重循环遍历所有可能的i和j组合。
  3. 计算当前元素的点积current = nums1[i-1] * nums2[j-1]。
  4. 更新dp[i][j]为五种可能情况中的最大值。
  5. 最终返回dp[m][n],即考虑全部元素时的最大点积。

3.3 时间复杂度分析

该解法使用了双重循环遍历所有可能的i和j组合,因此时间复杂度为O(mn),其中m和n分别是两个数组的长度。空间复杂度也是O(mn),用于存储dp数组。

4. 优化与变种思考

4.1 空间优化

注意到dp[i][j]只依赖于dp[i-1][j], dp[i][j-1]和dp[i-1][j-1],因此可以将空间复杂度优化到O(n):

def maxDotProduct(nums1, nums2): m, n = len(nums1), len(nums2) prev = [-float('inf')] * (n + 1) curr = [-float('inf')] * (n + 1) for i in range(1, m + 1): for j in range(1, n + 1): current = nums1[i-1] * nums2[j-1] curr[j] = max( current, prev[j-1] + current, prev[j], curr[j-1], prev[j-1] ) prev, curr = curr, [-float('inf')] * (n + 1) return prev[n]

4.2 变种问题思考

  1. 最小点积:如果我们要求最小点积而非最大点积,只需要将max改为min,并相应调整初始值。
  2. 固定长度子序列:如果要求子序列长度必须为k,我们需要增加一维状态来记录当前子序列长度。
  3. 非连续子序列:本题已经处理了非连续情况,但如果要求子序列必须是连续的,解法会有所不同。

5. 常见错误与调试技巧

5.1 常见错误

  1. 初始化错误:忘记将dp[0][j]和dp[i][0]初始化为负无穷,导致错误结果。
  2. 索引错误:混淆nums的0-based索引和dp的1-based索引,导致取错元素。
  3. 状态转移遗漏:忘记考虑所有可能的转移情况,特别是dp[i-1][j-1]的情况。

5.2 调试技巧

  1. 打印dp表:对于小规模输入,打印完整的dp表可以帮助理解算法执行过程。
  2. 边界测试:测试包含负数、全正数、全负数的输入,以及一个数组比另一个长很多的情况。
  3. 逐步验证:手动计算几个小的测试用例,与程序输出对比。

提示:当遇到难以理解的动态规划问题时,尝试从最小的子问题开始,逐步构建解决方案,并记录每个子问题的解。

6. 实际应用场景

虽然这个问题看起来是纯理论的算法题,但它实际上有一些实际应用:

  1. 生物信息学:在DNA序列比对中,类似的算法可以用于寻找相似子序列。
  2. 自然语言处理:比较两个句子的相似度时,可能需要找到最能匹配的子序列。
  3. 金融分析:寻找两个时间序列数据中最相关的子段。

理解这个问题的解法可以帮助我们在这些实际应用中设计更高效的算法。

7. 与其他DP问题的关联

这个问题与几个经典的动态规划问题有密切联系:

  1. 最长公共子序列(LCS):都是处理两个序列的子序列问题,但优化目标不同。
  2. 最大子数组和:可以看作是本题在一维情况下的简化版本。
  3. 编辑距离:状态定义和转移方式有相似之处。

理解这些问题的共性和差异,可以帮助我们更好地掌握动态规划解决序列问题的通用模式。

8. 个人解题心得

在实际解决这个问题时,我最初尝试了暴力解法,但很快发现其时间复杂度不可接受。转而思考动态规划解法时,最关键的突破点是意识到需要记录是否已经选择了元素这一状态。这提示我们,当标准的状态定义不足以解决问题时,可能需要增加额外的状态维度。

另一个重要的体会是,动态规划问题的边界条件处理往往决定了算法的正确性。在这个问题中,正确处理dp[0][j]和dp[i][0]的初始化是避免错误的关键。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询