优化探索:从O(n²)到O(nlogn)的飞跃 —— 贪心与二分法优化最长上升子序列

197 阅读3分钟

在先前讨论的动态规划解法(深度解析LeetCode 300:巧妙动态规划揭秘最长递增子序列)基础上,我们追求效率的极致,如何将算法复杂度从O(n²)降至O(nlogn)?本文聚焦贪心策略结合二分查找的高效解法,为解决最长上升子序列问题开辟新径。

核心洞察:对于一个上升子序列,显然其结尾元素越小,越有利于在后面接其他的元素,也就越可能变得更长,这就是最长上升子序列的贪心思路。

核心思路

  • 初始化 low 数组:创建一个 low 数组,其中 low[i] 表示长度为 i最长递增子序列(LIS)的最小末尾元素。我们的焦点集中于维护此 low 数组,以动态跟踪最长递增路径。
  • 关键操作:遍历每个元素 a[i],若它大于 low 中表示当前最长LIS长度的值,则直接延伸此LIS,即 low[++当前最长LIS长度] = a[i]
  • 维护 low 的策略:面对每个 a[i],尝试将其接续至LIS尾部;若不可行,利用 a[i] 更新 low 数组。实现细节涉及识别 low[j](首个≥a[i]的元素),并用 a[i] 替换之。
  • 性能瓶颈与优化:原始逐一查找导致的O(n) 时间复杂度在 low 数组上执行,维持整体算法于 O(n^2)二分查找的引入成为转折点,针对有序 low 数组的查询时间骤减至 O(logn) ,整体算法效率跃升至 O(nlogn)

实战演练

有以下序列A = [3 1 2 6 4 5],求LIS长度。我们设B来存储LIS序列,(i从1开始)

  1. 初始化步骤:A[1] = 3,置入B,B[1] = 3,同时low[1] = 3

  2. 调整与增长

    • 遇到A[2] = 1,比B末尾小,替换B[1] = 1low[1] = 1
    • A[3] = 2,大于B末尾,追加B=[1,2]low[2] = 2
    • A[4] = 6,继续追加B=[1,2,6]low[3]=6
    • A[5] = 4,二分查找更新:找到B[3]=6,更新为4,得到B=[1,2,4]low[3]=4
    • A[6] = 5,再次追加B=[1,2,4,5]low[4]=5

关键提示B数组展示的是构造过程,不代表实际LIS,仅保证长度正确

例如有以下序列A = [1 4 7 2],求LIS长度。我们设B来存储LIS序列,(i从1开始)

  • 流程类似,最终B调整为[1,2,7]low[2]=2,显示LIS长度为3。
  • 重要提醒:尽管B序列结果为[1,2,7],实际最长递增子序列并非如此,强调B序列仅用于长度指示,非精确解。

代码实践

int lengthOfLIS(vector<int>& nums) {
    int low[2510] = {INT_MAX};
    int ans = 1;
    low[1] = nums[1];
    for (int i = 2; i < nums.size(); i++) {
        if (nums[i] > low[ans])
            low[++ans] = nums[i];
        else {
            int ind = lower_bound(low + 1, low + ans, nums[i]) - low;
            low[ind] = nums[i];
        }
    }
    return ans;
}

结语

总结而言,通过对LeetCode 300题“最长递增子序列”的探讨与实践,我们不仅掌握了一种从基本动态规划到贪心策略结合二分查找的优化方法,更深刻理解了算法优化背后的思想力量。本篇解构不仅提供了具体的解题之道,更重要的是激发了对算法世界深入探索的兴趣与灵感。带着这些宝贵的认知,让我们在编码之旅上继续前行,以算法为匙,开启更多知识宝库的大门。