最长递增子序列的变种:带限制的最长递增子序列(每个元素最多出现k次)
字数 1524 2025-11-03 08:34:44

最长递增子序列的变种:带限制的最长递增子序列(每个元素最多出现k次)

题目描述
给定一个整数数组nums和一个整数k,找到最长递增子序列的长度,要求这个子序列中每个元素最多出现k次。注意:子序列不要求连续,但必须保持原数组中的相对顺序。

示例:
输入:nums = [1,2,2,3,3,3,4], k = 2
输出:4
解释:最长递增子序列可以是[1,2,2,3]或[1,2,3,3]等,其中每个数字最多出现2次。

解题思路
这个问题是标准最长递增子序列(LIS)问题的变种,增加了每个元素出现次数不超过k次的限制。我们需要修改传统的动态规划方法来解决这个限制。

详细解题步骤

步骤1:定义状态
我们定义dp[i]为以第i个元素结尾的最长递增子序列的长度。但为了处理出现次数限制,我们需要记录每个值出现的次数。

更准确地说,我们定义:

  • dp[i]:以nums[i]结尾的最长递增子序列长度
  • count[i]:在dp[i]对应的子序列中,nums[i]出现的次数

步骤2:状态转移方程
对于每个位置i,我们需要检查所有j < i:

  1. 如果nums[j] < nums[i],那么可以将nums[i]接在以nums[j]结尾的子序列后面
  2. 但是需要检查添加nums[i]后,整个子序列中nums[i]的出现次数是否超过k

状态转移方程:
dp[i] = max{ dp[j] + 1 },对于所有j < i且满足:

  • nums[j] < nums[i]
  • 在以j结尾的子序列中,nums[i]的出现次数 < k

步骤3:处理出现次数限制
为了跟踪每个值在子序列中的出现次数,我们需要维护一个辅助数组:

  • freq[i][val]:记录在以i结尾的子序列中,值val出现的次数

或者更高效的方法是:对于每个位置i,我们维护一个映射,记录当前子序列中各个数值的出现次数。

步骤4:算法实现
我们可以使用动态规划结合出现次数跟踪:

  1. 初始化dp数组全为1(每个元素单独构成子序列)
  2. 初始化freq数组,记录每个位置对应的子序列中数值出现情况
  3. 遍历数组,对于每个位置i:
    • 初始化dp[i] = 1
    • 初始化freq[i]中,nums[i]出现1次
    • 对于所有j < i:
      • 如果nums[j] < nums[i]:
        • 检查freq[j]中nums[i]的出现次数
        • 如果出现次数 < k,则可以接上:dp[i] = max(dp[i], dp[j] + 1)
        • 更新freq[i]中各个数值的出现次数

步骤5:优化考虑
上述方法的时间复杂度是O(n²),对于较大的n可能效率不高。我们可以考虑优化:

优化思路:使用二分查找+贪心思想,类似LIS的O(n log n)解法,但需要修改以处理出现次数限制。

步骤6:最终解法
更高效的解法是维护多个"桶",每个桶对应不同的子序列长度,对于每个长度,我们记录可能的最小结尾值及其出现次数信息。

具体实现:

  1. 使用一个列表tails,其中tails[i]表示长度为i+1的所有递增子序列的最小结尾值
  2. 对于每个数值,我们需要维护它在当前子序列中的出现次数
  3. 当处理新元素x时:
    • 使用二分查找找到x应该插入的位置
    • 检查插入后x的出现次数是否超过k
    • 如果不超过,则正常插入;如果超过,则需要特殊处理

步骤7:代码框架

def lengthOfLISWithLimit(nums, k):
    if not nums:
        return 0
    
    n = len(nums)
    dp = [1] * n
    # freq[i]是一个字典,记录以i结尾的子序列中各个数值的出现次数
    freq = [{} for _ in range(n)]
    
    for i in range(n):
        freq[i][nums[i]] = 1  # 当前元素至少出现1次
        
    max_len = 1
    
    for i in range(n):
        for j in range(i):
            if nums[j] < nums[i]:
                # 检查nums[i]在以j结尾的子序列中的出现次数
                current_count = freq[j].get(nums[i], 0)
                if current_count < k:
                    if dp[j] + 1 > dp[i]:
                        dp[i] = dp[j] + 1
                        # 更新频率信息
                        freq[i] = freq[j].copy()
                        freq[i][nums[i]] = freq[i].get(nums[i], 0) + 1
        max_len = max(max_len, dp[i])
    
    return max_len

步骤8:复杂度分析

  • 时间复杂度:O(n²)
  • 空间复杂度:O(n²)(因为每个位置需要存储一个频率字典)

这个解法虽然不如标准LIS的O(n log n)解法高效,但能正确处理出现次数限制,并且思路清晰易懂。

最长递增子序列的变种:带限制的最长递增子序列(每个元素最多出现k次) 题目描述 给定一个整数数组nums和一个整数k,找到最长递增子序列的长度,要求这个子序列中每个元素最多出现k次。注意:子序列不要求连续,但必须保持原数组中的相对顺序。 示例: 输入:nums = [ 1,2,2,3,3,3,4 ], k = 2 输出:4 解释:最长递增子序列可以是[ 1,2,2,3]或[ 1,2,3,3 ]等,其中每个数字最多出现2次。 解题思路 这个问题是标准最长递增子序列(LIS)问题的变种,增加了每个元素出现次数不超过k次的限制。我们需要修改传统的动态规划方法来解决这个限制。 详细解题步骤 步骤1:定义状态 我们定义dp[ i ]为以第i个元素结尾的最长递增子序列的长度。但为了处理出现次数限制,我们需要记录每个值出现的次数。 更准确地说,我们定义: dp[ i]:以nums[ i ]结尾的最长递增子序列长度 count[ i]:在dp[ i]对应的子序列中,nums[ i ]出现的次数 步骤2:状态转移方程 对于每个位置i,我们需要检查所有j < i: 如果nums[ j] < nums[ i],那么可以将nums[ i]接在以nums[ j ]结尾的子序列后面 但是需要检查添加nums[ i]后,整个子序列中nums[ i ]的出现次数是否超过k 状态转移方程: dp[ i] = max{ dp[ j] + 1 },对于所有j < i且满足: nums[ j] < nums[ i ] 在以j结尾的子序列中,nums[ i]的出现次数 < k 步骤3:处理出现次数限制 为了跟踪每个值在子序列中的出现次数,我们需要维护一个辅助数组: freq[ i][ val ]:记录在以i结尾的子序列中,值val出现的次数 或者更高效的方法是:对于每个位置i,我们维护一个映射,记录当前子序列中各个数值的出现次数。 步骤4:算法实现 我们可以使用动态规划结合出现次数跟踪: 初始化dp数组全为1(每个元素单独构成子序列) 初始化freq数组,记录每个位置对应的子序列中数值出现情况 遍历数组,对于每个位置i: 初始化dp[ i ] = 1 初始化freq[ i]中,nums[ i ]出现1次 对于所有j < i: 如果nums[ j] < nums[ i ]: 检查freq[ j]中nums[ i ]的出现次数 如果出现次数 < k,则可以接上:dp[ i] = max(dp[ i], dp[ j ] + 1) 更新freq[ i ]中各个数值的出现次数 步骤5:优化考虑 上述方法的时间复杂度是O(n²),对于较大的n可能效率不高。我们可以考虑优化: 优化思路:使用二分查找+贪心思想,类似LIS的O(n log n)解法,但需要修改以处理出现次数限制。 步骤6:最终解法 更高效的解法是维护多个"桶",每个桶对应不同的子序列长度,对于每个长度,我们记录可能的最小结尾值及其出现次数信息。 具体实现: 使用一个列表tails,其中tails[ i ]表示长度为i+1的所有递增子序列的最小结尾值 对于每个数值,我们需要维护它在当前子序列中的出现次数 当处理新元素x时: 使用二分查找找到x应该插入的位置 检查插入后x的出现次数是否超过k 如果不超过,则正常插入;如果超过,则需要特殊处理 步骤7:代码框架 步骤8:复杂度分析 时间复杂度:O(n²) 空间复杂度:O(n²)(因为每个位置需要存储一个频率字典) 这个解法虽然不如标准LIS的O(n log n)解法高效,但能正确处理出现次数限制,并且思路清晰易懂。