在先前讨论的动态规划解法(深度解析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开始)
-
初始化步骤:A[1] = 3,置入B,
B[1] = 3,同时low[1] = 3。 -
调整与增长:
- 遇到A[2] = 1,比B末尾小,替换:
B[1] = 1,low[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。
- 遇到A[2] = 1,比B末尾小,替换:
关键提示: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题“最长递增子序列”的探讨与实践,我们不仅掌握了一种从基本动态规划到贪心策略结合二分查找的优化方法,更深刻理解了算法优化背后的思想力量。本篇解构不仅提供了具体的解题之道,更重要的是激发了对算法世界深入探索的兴趣与灵感。带着这些宝贵的认知,让我们在编码之旅上继续前行,以算法为匙,开启更多知识宝库的大门。