2407. 最长递增子序列 II(困难)题解

题目描述

给你一个整数数组 nums 和一个整数 k 。

找到 nums 中满足以下要求的最长子序列:

  • 子序列 严格递增
  • 子序列中相邻元素的差值 不超过 k 。

请你返回满足上述要求的 最长子序列 的长度。

子序列 是从一个数组中删除部分元素后,剩余元素不改变顺序得到的数组。

示例 1:

输入:nums = [4,2,1,4,3,4,5,8,15], k = 3
输出:5
解释:
满足要求的最长子序列是 [1,3,4,5,8] 。
子序列长度为 5 ,所以我们返回 5 。
注意子序列 [1,3,4,5,8,15] 不满足要求,因为 15 - 8 = 7 大于 3 。

示例 2:

输入:nums = [7,4,5,1,8,12,4,7], k = 5
输出:4
解释:
满足要求的最长子序列是 [4,5,8,12] 。
子序列长度为 4 ,所以我们返回 4 。

示例 3:

输入:nums = [1,5], k = 1
输出:1
解释:
满足要求的最长子序列是 [1] 。
子序列长度为 1 ,所以我们返回 1 。

提示:

  • 1 <= nums.length <= 10^5
  • 1 <= nums[i], k <= 10^5

问题分析

这道题是经典的最长递增子序列(LIS)问题的变种,增加了一个约束条件:相邻元素的差值不能超过 k。

与标准的 LIS 问题相比,这道题的特殊之处在于:

  1. 不仅要求严格递增

  2. 还要求相邻元素差值 ≤ k

  3. 数据规模较大(10^5),需要高效算法

分析:

  • 如果我们知道以某个值结尾的最长子序列长度,那么对于当前元素 nums[i],我们需要找到所有满足条件的前驱元素

  • 前驱元素的值必须在区间 [nums[i] - k, nums[i] - 1]

  • 我们需要在这个区间内找到以某个值结尾的最长子序列长度的最大值


解题思路

方法一:动态规划 + 线段树

由于数据规模较大,朴素的 O(n²) 动态规划会超时。我们需要使用线段树来优化区间查询。

状态定义:

  • dp[v] 表示以值 v 结尾的最长递增子序列的长度

状态转移:

  • 对于当前元素 nums[i],我们需要查询区间 [nums[i] - k, nums[i] - 1] 内的最大 dp

  • 然后更新 dp[nums[i]] = max(dp[nums[i]], maxInRange + 1)

线段树操作:

  • 区间查询最大值

  • 单点更新


算法过程

以示例1为例:nums = [4,2,1,4,3,4,5,8,15], k = 3

  • 初始状态:dp数组全为0,表示以每个值结尾的最长递增子序列长度
  • num = 4
    • 查询区间[1,3],最大值=0
    • 更新dp[4]=1
    • 当前最优子序列:[4]
  • num = 2
    •  查询区间[1,1],最大值=0
    • 更新dp[2]=1
    • 当前最优子序列:[2] 或 [4]
  • num = 1
    • 查询区间[1,0](无效区间),最大值=0
    • 更新dp[1]=1
    • 当前最优子序列:[1]、[2] 或 [4]
  • num = 4
    • 查询区间[1,3],最大值=1(来自dp[1]或dp[2])
    • 更新dp[4]=2
    • 当前最优子序列:[1,4] 或 [2,4]
  • num = 3
    • 查询区间[1,2],最大值=1(来自dp[1]或dp[2])
    • 更新dp[3]=2
    • 当前最优子序列:[1,3]
  • num = 4
    • 查询区间[1,3],最大值=2(来自dp[3])
    • 更新dp[4]=3
    • 当前最优子序列:[1,3,4]
  • num = 5
    • 查询区间[2,4],最大值=3(来自dp[4])
    • 更新dp[5]=4
    • 当前最优子序列:[1,3,4,5]
  • num = 8
    • 查询区间[5,7],最大值=4(来自dp[5])
    • 更新dp[8]=5
    • 当前最优子序列:[1,3,4,5,8]
  • num = 15
    • 查询区间[12,14],最大值=0(区间内无有效值)
    • 更新dp[15]=1
    • 15不能接在8后面,因为15-8=7 > k=3
步骤    当前元素    查询区间    区间最大值    更新后dp状态    最长长度
1       4         [1,3]       0           dp[4]=1        1
2       2         [1,1]       0           dp[2]=1        1  
3       1         [1,0]       0           dp[1]=1        1
4       4         [1,3]       1           dp[4]=2        2
5       3         [1,2]       1           dp[3]=2        2
6       4         [1,3]       2           dp[4]=3        3
7       5         [2,4]       3           dp[5]=4        4
8       8         [5,7]       4           dp[8]=5        5
9       15        [12,14]     0           dp[15]=1       5

最终dp数组状态:

dp[1] = 1   (子序列: [1])
dp[2] = 1   (子序列: [2])  
dp[3] = 2   (子序列: [1,3])
dp[4] = 3   (子序列: [1,3,4])
dp[5] = 4   (子序列: [1,3,4,5])
dp[8] = 5   (子序列: [1,3,4,5,8])
dp[15] = 1  (子序列: [15])

代码实现

Java 实现 - 线段树

/**
 * 2407. 最长递增子序列 II
 * 动态规划,线段树
 */
class Solution {
    /**
     * 计算满足条件的最长递增子序列长度
     * 对于子序列中相邻元素的差值有约束:后一个元素减去前一个元素的差值不能超过k
     *
     * @param nums 输入的整数数组
     * @param k 差值约束条件,相邻元素差值不能超过k
     * @return 满足条件的最长递增子序列长度
     */
    public int lengthOfLIS(int[] nums, int k) {
        // 找到数组中的最大值,确定线段树的范围
        int maxVal = 0;
        for (int num : nums) {
            maxVal = Math.max(maxVal, num);
        }

        // 创建线段树
        SegmentTree segmentTree = new SegmentTree(maxVal);
        int res = 0;

        // 遍历数组中的每个元素,动态维护以当前元素结尾的最长递增子序列长度
        for (int num:nums){
            // 查询区间 [num - k, num - 1] 内的最大值,确保差值约束条件
            int left = Math.max(1, num - k);
            int right = num - 1;

            int maxLen = 0;
            if(left<=right){
                maxLen = segmentTree.query(left, right);
            }

            // 更新以 num 结尾的最长子序列长度
            int newLen = maxLen+1;
            segmentTree.update(num, newLen);

            // 更新全局最大值
            res = Math.max(res, newLen);
        }

        return res;
    }

}

/**
 * 线段树类,用于高效处理区间查询和单点更新操作
 * 支持区间最大值查询和单点最大值更新操作
 */
class SegmentTree {
    private int[] tree;
    private int n;

    /**
     * 构造函数,初始化线段树
     * @param size 线段树覆盖的区间大小
     */
    public SegmentTree(int size) {
        n = size;
        tree = new int[4 * n];
    }

    /**
     * 更新指定位置的值
     * @param pos 要更新的位置(1-indexed)
     * @param val 要更新的值,实际存储的是当前值与新值的最大值
     */
    public void update(int pos, int val) {
        update(1, 1, n, pos, val);
    }

    /**
     * 递归更新线段树节点
     * @param node 当前节点在tree数组中的索引
     * @param start 当前节点所代表区间的起始位置
     * @param end 当前节点所代表区间的结束位置
     * @param pos 要更新的位置
     * @param val 要更新的值
     */
    private void update(int node, int start, int end, int pos, int val) {
        // 到达叶子节点,直接更新值
        if (start == end) {
            tree[node] = Math.max(tree[node], val);
        } else {
            // 非叶子节点,递归更新子节点
            int mid = (start + end) / 2;
            if (pos <= mid) {
                update(2 * node, start, mid, pos, val);
            } else {
                update(2 * node + 1, mid + 1, end, pos, val);
            }
            // 更新当前节点为子节点的最大值
            tree[node] = Math.max(tree[2 * node], tree[2 * node + 1]);
        }
    }

    /**
     * 查询指定区间的最大值
     * @param left 查询区间的左边界(1-indexed)
     * @param right 查询区间的右边界(1-indexed)
     * @return 区间[left, right]内的最大值,如果区间无效则返回0
     */
    public int query(int left, int right) {
        if (left > right) return 0;
        return query(1, 1, n, left, right);
    }

    /**
     * 递归查询线段树区间最大值
     * @param node 当前节点在tree数组中的索引
     * @param start 当前节点所代表区间的起始位置
     * @param end 当前节点所代表区间的结束位置
     * @param left 查询区间的左边界
     * @param right 查询区间的右边界
     * @return 区间[left, right]与当前节点区间交集内的最大值
     */
    private int query(int node, int start, int end, int left, int right) {
        // 查询区间与当前节点区间无交集
        if (right < start || end < left) {
            return 0;
        }
        // 当前节点区间完全包含在查询区间内
        if (left <= start && end <= right) {
            return tree[node];
        }
        // 部分重叠,递归查询子节点
        int mid = (start + end) / 2;
        return Math.max(
                query(2 * node, start, mid, left, right),
                query(2 * node + 1, mid + 1, end, left, right)
        );
    }
}

C# 实现 - 线段树

public class Solution
{
    public int LengthOfLIS(int[] nums, int k)
    {
        // 找到数组中的最大值,确定线段树的范围
        int maxVal = 0;
        foreach (int num in nums)
        {
            maxVal = Math.Max(maxVal, num);
        }

        // 创建线段树
        SegmentTree segTree = new SegmentTree(maxVal);
        int result = 0;

        // 遍历数组中的每个元素
        for (int i = 0; i < nums.Length; i++)
        {
            int num = nums[i];
            // 查询区间 [num - k, num - 1] 内的最大值
            int left = Math.Max(1, num - k);
            int right = num - 1;

            int maxLen = 0;
            if (left <= right)
            {
                maxLen = segTree.Query(left, right);
            }

            // 更新以 num 结尾的最长子序列长度
            int newLen = maxLen + 1;
            segTree.Update(num, newLen);

            // 更新全局最大值
            result = Math.Max(result, newLen);
        }

        return result;
    }
}

/// <summary>
/// 线段树类,用于高效处理区间最大值查询和单点更新操作。
/// </summary>
public class SegmentTree
{
    private int[] tree;
    private int n;

    /// <summary>
    /// 初始化线段树。
    /// </summary>
    /// <param name="size">线段树所维护的区间大小。</param>
    public SegmentTree(int size)
    {
        n = size;
        tree = new int[4 * n];
    }

    /// <summary>
    /// 更新指定位置的值为给定值(取最大值)。
    /// </summary>
    /// <param name="pos">要更新的位置(从1开始)。</param>
    /// <param name="val">要更新的值。</param>
    public void Update(int pos, int val)
    {
        Update(1, 1, n, pos, val);
    }

    /// <summary>
    /// 递归实现线段树节点更新。
    /// </summary>
    /// <param name="node">当前节点在树中的索引。</param>
    /// <param name="start">当前节点所代表区间的起始位置。</param>
    /// <param name="end">当前节点所代表区间的结束位置。</param>
    /// <param name="pos">要更新的位置。</param>
    /// <param name="val">要更新的值。</param>
    private void Update(int node, int start, int end, int pos, int val)
    {
        // 如果是叶子节点,则直接更新
        if (start == end)
        {
            tree[node] = Math.Max(tree[node], val);
        }
        else
        {
            int mid = (start + end) / 2;
            // 根据位置决定更新左子树还是右子树
            if (pos <= mid)
            {
                Update(node * 2, start, mid, pos, val);
            }
            else
            {
                Update(node * 2 + 1, mid + 1, end, pos, val);
            }

            // 更新当前节点的值为其子节点的最大值
            tree[node] = Math.Max(tree[node * 2], tree[node * 2 + 1]);
        }
    }

    /// <summary>
    /// 查询指定区间的最大值。
    /// </summary>
    /// <param name="left">查询区间的左边界(从1开始)。</param>
    /// <param name="right">查询区间的右边界(从1开始)。</param>
    /// <returns>区间 [left, right] 内的最大值;若区间无效则返回0。</returns>
    public int Query(int left, int right)
    {
        if (left > right) return 0;
        return Query(1, 1, n, left, right);
    }

    /// <summary>
    /// 递归实现线段树区间最大值查询。
    /// </summary>
    /// <param name="node">当前节点在树中的索引。</param>
    /// <param name="start">当前节点所代表区间的起始位置。</param>
    /// <param name="end">当前节点所代表区间的结束位置。</param>
    /// <param name="left">查询区间的左边界。</param>
    /// <param name="right">查询区间的右边界。</param>
    /// <returns>当前区间与查询区间交集内的最大值。</returns>
    private int Query(int node, int start, int end, int left, int right)
    {
        // 当前区间与查询区间无交集
        if (left > end || right < start)
        {
            return 0;
        }
        // 当前区间完全包含于查询区间
        else if (left <= start && right >= end)
        {
            return tree[node];
        }
        else
        {
            int mid = (start + end) / 2;
            // 分别查询左右子树并返回最大值
            return Math.Max(Query(node * 2, start, mid, left, right),
                Query(node * 2 + 1, mid + 1, end, left, right));
        }
    }
}

复杂度分析

  • 时间复杂度:O(n log maxVal),其中n是数组长度,maxVal是数组中的最大值

    • 每个元素需要进行一次查询和一次更新操作

    • 线段树的查询和更新操作都是O(log maxVal)

  • 空间复杂度:O(maxVal),需要维护大小为maxVal的线段树

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值