深度解析LeetCode 300:巧妙动态规划揭秘最长递增子序列

231 阅读3分钟

题目解析:LeetCode 300. 最长递增子序列

在探讨算法之前,让我们先深入理解LeetCode 300题的核心——寻找严格递增子序列中的最长长度。请注意,这里的“递增”强调了每个元素都必须大于其前驱,而非大于等于,这一细节在解决此问题时至关重要。

Screenshot 2024-05-30 221834.png

动态规划策略精讲

思维导引:

面对这类子序列问题,核心思想在于考虑数组中每个元素作为子序列终点时,能构建的最长递增序列有多长。通过这种方法,我们逐步累积信息,直至揭示整个序列的最长递增子序列长度。

动态规划框架搭建:

  1. 状态定义:设dp[i]表示以nums[i]结尾的最长递增子序列的长度。这样,最终答案即为所有dp[i]中的最大值。

  2. 状态转移:对于数组中的每个元素nums[i],我们回顾所有之前的元素nums[j](其中j < i),如果nums[j] < nums[i],这意味着nums[i]可以合法地接续在以nums[j]结尾的递增子序列之后,从而形成一个新的更长的递增子序列。因此,我们更新dp[i] = max(dp[i], dp[j] + 1),以确保dp[i]始终代表最理想的递增序列长度。

  3. 初始化与遍历:每个位置的初始递增子序列长度至少为1(因为它自身即是最小的递增子序列)。遍历遵循从前至后的逻辑,确保在计算dp[i]时已充分考虑了所有小于i的位置信息。

  4. 实例推演:

    通过一步步构建dp数组的过程,我们可以直观地观察到如何从局部最优解推导出全局最优解。想象一下逐步填充dp数组的过程,你会看到递增子序列是如何一步步延长并最终确定最长长度的。

Screenshot_20240530_225115_com.newskyer.draw_edit.jpg

代码实现与分析

以下是采用动态规划求解的C++代码示例,简洁明了地体现了上述逻辑:

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
         // 如果数组只有一个或没有元素,最长递增子序列的长度就是数组的长度
        if (nums.size() <= 1) return nums.size();
        // 初始化dp数组,大小与原数组相同,每个元素的初始值为1
        // 因为至少以该位置的数字自身可以构成长度为1的递增子序列
        vector<int> dp(nums.size(), 1);
        int ans = 0;
        // 遍历数组中的每个元素
        for (int i = 1; i < nums.size(); i++) {
             // 再次遍历当前元素之前的每个元素
            for (int j = 0; j < i; j++) {
                // 如果当前元素大于之前的某个元素,并且这个条件成立意味着可以形成更长的递增子序列
                if (nums[i] > nums[j])          
                    // 更新dp[i]的值,取当前值和基于dp[j]加1(即在j的递增子序列基础上加上i元素)中的较大者
                    dp[i] = max(dp[i], dp[j] + 1);
            }
            // 在每次内层循环结束后,检查当前的dp[i]是否大于已知的最大长度,如果是,则更新ans
            if (dp[i] > ans)
                ans = dp[i]; 
        }
        return ans;
    }
};

  • 时间复杂度: O(n^2),因两层循环遍历数组。
  • 空间复杂度: O(n),需额外存储每个位置的最长递增子序列长度。

结语

通过上述解析,我们不仅解决了LeetCode 300题,还深入探索了动态规划解决最长递增子序列问题的精髓。此文章旨在通过清晰的逻辑阐述与实例演示,帮助读者掌握这一经典算法应用。分享给志同道合的学习者,相信这份详尽的解析定能激发更多对算法研究的热情与理解。