题目:最长重复子数组(给两个整数数组,返回两个数组中公共的、长度最长的子数组的长度)
字数 2321 2025-12-19 01:59:01

好的,根据你的历史记录,我为你随机选择并讲解一个尚未出现过的线性动态规划经典问题。注意,我们不会讲解“任务调度器”相关的算法。

题目:最长重复子数组(给两个整数数组,返回两个数组中公共的、长度最长的子数组的长度)

题目描述:
给定两个整数数组 nums1nums2,你需要找到它们的一个公共的、连续的子数组(也称为子数组),并返回该子数组的最大长度。

例如:

  • 输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
  • 输出:3
  • 解释:长度最长的公共子数组是 [3,2,1],长度为 3。

注意:子数组必须是连续的,这与“子序列”不同。


解题思路与分析

这是一个经典的“最长公共子串”(Longest Common Substring)问题。由于子数组要求连续,状态的定义和转移方程与“最长公共子序列”(LCS)有显著区别。

  1. 暴力枚举的局限性
    最直接的想法是枚举 nums1 的所有子数组,然后在 nums2 中查找相同的子数组。但这样做的时间复杂度极高(O(m² * n * min(m, n))),对于较长的数组(如长度几百到几千)会严重超时。

  2. 动态规划的核心思路

    • 我们用 dp[i][j] 来表示一个状态:nums1 的第 i-1 个元素为结尾,同时以 nums2 的第 j-1 个元素为结尾的公共子数组的长度
    • 为什么是 i-1j-1?这是一种编程技巧,使得我们方便处理边界情况(当 i=0j=0 时,意味着没有字符,长度为0)。dp[i][j] 对应的是 nums1[i-1]nums2[j-1]
    • 这个状态定义天然地保证了“连续性”。因为 dp[i][j] 的值只依赖于 nums1[i-1]nums2[j-1] 是否相等。
  3. 状态转移方程的推导

    • 如果 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
  4. 初始化与边界条件

    • dp[0][j] = 0nums1 为空数组时,与任何 nums2 的子数组公共长度都为 0。
    • dp[i][0] = 0nums2 为空数组时,与任何 nums1 的子数组公共长度都为 0。
  5. 目标答案的获取
    在整个动态规划的表格中,dp[i][j] 的值是“以特定位置结尾”的最长公共子数组长度。而题目要求的是“全局最长”的。因此,我们需要在计算 dp[i][j] 的过程中,用一个变量 ans 来记录所有 dp[i][j] 中的最大值,它就是最终的答案。

  6. 空间复杂度优化(可选)
    观察状态转移方程,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 长度为 mnums2 长度为 n

步骤1:初始化DP表
创建一个二维数组 dp,大小为 (m+1) x (n+1),所有元素初始化为0。额外的第一行和第一列用于处理边界。

步骤2:逐行、逐列填充DP表
我们从 i = 1m,从 j = 1n 遍历。

  • 比较 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] = 2dp[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],从而保证了子数组的连续性质。解题时,牢记“相等则延续,不等则归零”的转移逻辑,并注意在过程中记录全局最大值。

好的,根据你的历史记录,我为你随机选择并讲解一个尚未出现过的线性动态规划经典问题。注意,我们不会讲解“任务调度器”相关的算法。 题目:最长重复子数组(给两个整数数组,返回两个数组中公共的、长度最长的子数组的长度) 题目描述: 给定两个整数数组 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的行/列省略): 我们能看到, dp[5][3] = 3 是最大值,它由 dp[4][2] = 2 和 dp[3][1] = 1 这样“连续相等”的状态推导而来,对应了子数组 [3, 2, 1] 。 代码实现(附注释) 优化后的代码(一维DP,空间复杂度O(n)) 总结 这个问题的核心在于理解 状态定义的连续性 。 dp[i][j] 代表了以特定位置结尾的最长公共子数组长度,这使得状态转移仅依赖于 dp[i-1][j-1] ,从而保证了子数组的连续性质。解题时,牢记“相等则延续,不等则归零”的转移逻辑,并注意在过程中记录全局最大值。