好的,根据你的历史记录,我为你随机选择并讲解一个尚未出现过的线性动态规划经典问题。注意,我们不会讲解“任务调度器”相关的算法。
题目:最长重复子数组(给两个整数数组,返回两个数组中公共的、长度最长的子数组的长度)
题目描述:
给定两个整数数组 nums1 和 nums2,你需要找到它们的一个公共的、连续的子数组(也称为子数组),并返回该子数组的最大长度。
例如:
- 输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
- 输出:3
- 解释:长度最长的公共子数组是
[3,2,1],长度为 3。
注意:子数组必须是连续的,这与“子序列”不同。
解题思路与分析
这是一个经典的“最长公共子串”(Longest Common Substring)问题。由于子数组要求连续,状态的定义和转移方程与“最长公共子序列”(LCS)有显著区别。
-
暴力枚举的局限性:
最直接的想法是枚举nums1的所有子数组,然后在nums2中查找相同的子数组。但这样做的时间复杂度极高(O(m² * n * min(m, n))),对于较长的数组(如长度几百到几千)会严重超时。 -
动态规划的核心思路:
- 我们用
dp[i][j]来表示一个状态:以nums1的第i-1个元素为结尾,同时以nums2的第j-1个元素为结尾的公共子数组的长度。 - 为什么是
i-1和j-1?这是一种编程技巧,使得我们方便处理边界情况(当i=0或j=0时,意味着没有字符,长度为0)。dp[i][j]对应的是nums1[i-1]和nums2[j-1]。 - 这个状态定义天然地保证了“连续性”。因为
dp[i][j]的值只依赖于nums1[i-1]和nums2[j-1]是否相等。
- 我们用
-
状态转移方程的推导:
- 如果
nums1[i-1] == nums2[j-1]:
这意味着当前这两个字符匹配上了。那么,以它们为结尾的公共子数组,其长度应该是“在它们各自前一个字符的基础上,接上这个字符”形成的。即:
dp[i][j] = dp[i-1][j-1] + 1 - 如果
nums1[i-1] != nums2[j-1]:
如果这两个字符不相等,那么以它们为结尾不可能构成任何公共子数组。因此,长度直接归零:
dp[i][j] = 0
- 如果
-
初始化与边界条件:
dp[0][j] = 0:nums1为空数组时,与任何nums2的子数组公共长度都为 0。dp[i][0] = 0:nums2为空数组时,与任何nums1的子数组公共长度都为 0。
-
目标答案的获取:
在整个动态规划的表格中,dp[i][j]的值是“以特定位置结尾”的最长公共子数组长度。而题目要求的是“全局最长”的。因此,我们需要在计算dp[i][j]的过程中,用一个变量ans来记录所有dp[i][j]中的最大值,它就是最终的答案。 -
空间复杂度优化(可选):
观察状态转移方程,dp[i][j]只依赖于它左上角(i-1, j-1)的值,与当前行的其他值或上一行的其他值无关。因此,我们可以用一个一维数组dp来压缩空间。- 但是,如果直接用一个一维数组从左到右更新,在更新
dp[j]时,它所依赖的dp[j-1]已经是这一行更新过的值(对应于dp[i][j-1]),而不是上一行的dp[i-1][j-1]。 - 解决方案是从右向左更新。这样,当我们计算
dp[j]时,dp[j-1]存储的还是上一行的旧值(即dp[i-1][j-1]),符合要求。 - 或者,我们也可以用一个临时变量
prev来保存左上角的值。
- 但是,如果直接用一个一维数组从左到右更新,在更新
具体解题步骤(二维DP清晰版)
假设 nums1 长度为 m,nums2 长度为 n。
步骤1:初始化DP表
创建一个二维数组 dp,大小为 (m+1) x (n+1),所有元素初始化为0。额外的第一行和第一列用于处理边界。
步骤2:逐行、逐列填充DP表
我们从 i = 1 到 m,从 j = 1 到 n 遍历。
- 比较
nums1[i-1]和nums2[j-1]。 - 如果相等:
dp[i][j] = dp[i-1][j-1] + 1 - 如果不相等:
dp[i][j] = 0
步骤3:实时更新答案
在每次计算 dp[i][j] 后,将其与当前记录的最大值 ans 比较,如果更大,则更新 ans。
步骤4:返回答案
遍历完成后,ans 即为所求。
举例说明
以 nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7] 为例,构建 dp 表(下标为0的行/列省略):
nums2 -> 3 2 1 4 7
nums1↓
1 [0, 0, 1, 0, 0]
2 [0, 1, 0, 0, 0]
3 [1, 0, 0, 0, 0]
2 [0, 2, 0, 0, 0]
1 [0, 0, 3, 0, 0]
我们能看到,dp[5][3] = 3 是最大值,它由 dp[4][2] = 2 和 dp[3][1] = 1 这样“连续相等”的状态推导而来,对应了子数组 [3, 2, 1]。
代码实现(附注释)
def findLength(nums1, nums2):
m, n = len(nums1), len(nums2)
# 1. 初始化DP表,大小为 (m+1) x (n+1)
dp = [[0] * (n + 1) for _ in range(m + 1)]
ans = 0 # 用于记录全局最大值
# 2. 填充DP表
for i in range(1, m + 1):
for j in range(1, n + 1):
if nums1[i-1] == nums2[j-1]:
# 字符相等,在之前连续匹配的长度基础上+1
dp[i][j] = dp[i-1][j-1] + 1
# 3. 更新答案
ans = max(ans, dp[i][j])
# 如果不相等,dp[i][j] 保持为0,这是初始化已经完成的
# 4. 返回答案
return ans
优化后的代码(一维DP,空间复杂度O(n))
def findLength_optimized(nums1, nums2):
m, n = len(nums1), len(nums2)
# 1. 初始化一维DP数组,大小为 n+1
dp = [0] * (n + 1)
ans = 0
# 2. 遍历nums1的每个元素
for i in range(1, m + 1):
# 关键:从右向左更新,以避免覆盖左上角的值
for j in range(n, 0, -1):
if nums1[i-1] == nums2[j-1]:
dp[j] = dp[j-1] + 1 # dp[j-1] 是上一行i-1时的值
ans = max(ans, dp[j])
else:
dp[j] = 0 # 不相等,必须显式置零
return ans
总结
这个问题的核心在于理解状态定义的连续性。dp[i][j] 代表了以特定位置结尾的最长公共子数组长度,这使得状态转移仅依赖于 dp[i-1][j-1],从而保证了子数组的连续性质。解题时,牢记“相等则延续,不等则归零”的转移逻辑,并注意在过程中记录全局最大值。