最长递增子序列的变种:带限制的最长递增子序列(限制相邻元素差值在一定范围内)
题目描述
给定一个整数数组 nums,以及一个正整数 d。要求找出最长递增子序列(LIS)的长度,但增加一个限制条件:子序列中相邻两个元素在原数组中的下标差的绝对值不超过 d。
注意:这里的“相邻”指的是在子序列中是相邻的,但在原数组中这两个元素的下标位置的距离(索引差)不能超过 d。
例如:
nums = [3, 5, 2, 4, 6, 1, 7],d = 3。
子序列 [3, 5, 6, 7] 是不满足限制的,因为 5 和 6 在原数组中下标差为 4(假设 5 下标 1,6 下标 5),超过了 d=3。
而子序列 [3, 5, 6] 中,相邻元素在原数组中的下标差都在 3 以内,则合法。
你需要返回满足此条件的最长递增子序列的长度。
解题思路
这是一个经典 LIS 问题的变种,增加了“相邻元素在原数组中的下标差不超过 d”的约束。
经典的 LIS 动态规划定义为:
dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度,转移为:
\[dp[i] = \max_{j < i \text{ 且 } nums[j] < nums[i]} \{ dp[j] + 1 \} \]
但现在增加条件:i - j <= d,即我们只能从前面距离不超过 d 的下标 j 转移过来。
步骤 1:定义状态
dp[i] 表示以 nums[i] 结尾的,并且满足限制条件的最长递增子序列的长度。
初始化:dp[i] = 1(每个元素自己构成一个长度为 1 的子序列)。
步骤 2:状态转移
对于每个 i,我们需要查看前面最多 d 个位置(下标从 i-d 到 i-1),但必须满足:
j >= 0(不越界)nums[j] < nums[i](保证递增)i - j <= d(这是我们的额外约束,其实在遍历时控制j范围即可)
所以转移方程:
\[dp[i] = \max_{j = \max(0, i-d)}^{i-1} \{ dp[j] + 1 \} \quad \text{当 } nums[j] < nums[i] \]
如果前面没有满足的 j,则 dp[i] 保持为 1。
步骤 3:最终答案
答案就是所有 dp[i] 的最大值。
举例说明
nums = [7, 6, 5, 4, 3, 2, 1],d = 3
因为数组是严格递减的,任何长度大于 1 的递增子序列都不存在,所以答案是 1。
nums = [3, 1, 2, 4, 5, 2],d = 2
- i=0: dp[0]=1
- i=1: 看前面最多 2 个位置(j=0),但 nums[0]=3 > nums[1]=1,不递增,dp[1]=1
- i=2: 看 j=0,1
- j=0: nums[0]=3 > 2,不行
- j=1: nums[1]=1 < 2 且 i-j=1<=d,dp[2]=dp[1]+1=2
- i=3: 看 j=1,2
- j=1: nums[1]=1<4,dp[3]=dp[1]+1=2
- j=2: nums[2]=2<4,dp[3]=dp[2]+1=3(更大)
所以 dp[3]=3
- i=4: 看 j=2,3
- j=2: 2<5,dp[4]=dp[2]+1=3
- j=3: 4<5,dp[4]=dp[3]+1=4(更大)
所以 dp[4]=4
- i=5: 看 j=3,4
- j=3: 4>2 不行
- j=4: 5>2 不行
dp[5]=1
最终答案是 max(dp)=4,对应的子序列可以是 [1,2,4,5](在原数组中的下标分别是 1,2,3,4,相邻下标差 ≤2)。
步骤 4:时间复杂度优化
直接按上述方法计算,对于每个 i,需要检查最多 d 个前面的 j,总时间复杂度 O(n*d),在 n 和 d 都很大时可能较慢。
我们可以用线段树或树状数组来优化:
- 将 nums 的值离散化(因为值范围可能很大)
- 用数据结构维护“以值 v 结尾的满足条件的最长递增子序列长度”
- 对于每个 i,我们查询值在 [1, nums[i]-1] 范围内的最大值,这个最大值就是从前面某个满足 nums[j] < nums[i] 且 i-j<=d 的 j 中得到的最大 dp[j]
- 但还需要保证 j 的索引满足 i-j<=d,所以当我们处理到 i 时,需要“移除”下标小于 i-d 的元素,因为它们已经超出了可转移的范围。
具体操作:
- 离散化 nums 的所有值,假设值范围映射到 1..m。
- 用线段树 tree,tree[x] 表示以离散化值 x 结尾的满足当前“在窗口内”条件的最大 dp 值。
- 遍历 i=0 到 n-1:
- 查询:在树中查询 [1, rank[nums[i]]-1] 的最大值,记为 best,则 dp[i] = best + 1(如果 best=0 则 dp[i]=1)
- 更新:将树中 rank[nums[i]] 位置更新为 max(当前值, dp[i])
- 维护窗口:当 i >= d 时,需要将 nums[i-d] 从树中“移除”,因为我们即将进入下一个 i+1,对于 i+1 来说,j 的最小下标是 (i+1)-d,所以下标 i-d 就不在窗口内了。移除操作就是将树中 rank[nums[i-d]] 位置的值改为 0(如果这个位置在之前被 nums[i-d] 更新过)。
这样每个 i 只需要 O(log m) 的时间,总复杂度 O(n log n)。
最后
这个问题的核心是 LIS 加上位置窗口约束,通过线段树优化可以将复杂度从 O(nd) 降到 O(n log n),适用于 n 较大、d 较大的情况。
答案就是所有 dp[i] 的最大值。