在数组类面试题中,「最长递增子序列(Longest Increasing Subsequence, LIS)」是当之无愧的“高频考点+思路模板”——它不仅本身经常被考察,其解题思想(动态规划、贪心+二分)还能迁移到数十种变种问题中。本文将从题目背景出发,拆解两种解法的“思路诞生过程”,解答新手最易困惑的核心疑问,对比两种方法的优劣,并给出“看到题目就知道用什么解法”的触发技巧,帮你彻底掌握这类问题。

一、题目背景:什么是最长递增子序列?

1. 问题定义

给定一个长度为 n 的整数数组 nums,找出其中不要求连续严格递增的子序列的最长长度。

  • 「不连续」:子序列的元素可以从原数组中任意挑选,无需相邻;
  • 「严格递增」:子序列中后一个元素必须大于前一个元素(若题目允许相等,则为“最长不递减子序列”,思路类似)。

2. 示例理解

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列为 [2,3,7,101][2,3,7,18],长度均为 4。

3. 为什么这道题重要?

  • 面试高频:字节、阿里、谷歌等大厂常考,且常作为中等难度题的“核心子问题”;
  • 思路模板:动态规划的“状态定义+转移”、贪心+二分的“优化思想”,可迁移到数十种变种问题;
  • 实际应用:可用于解决“最长上升子序列匹配”“最少拦截系统”“俄罗斯套娃信封”等实际场景问题。

二、两种解题思路:从“直观”到“最优”的演变

思路1:动态规划(O(n²))—— 从“每个元素结尾”切入

1. 思路是怎么产生的?

面对“找最长子序列”的问题,最直观的想法是:逐个分析每个元素,看看以它为结尾的最长递增子序列能有多长

  • 比如对于 nums = [2,5,3,7]
    • 2 结尾的LIS:[2] → 长度 1;
    • 5 结尾的LIS:[2,5] → 长度 2(因为 5>2);
    • 3 结尾的LIS:[2,3] → 长度 2(因为 3>2,但 3<5,不能接在 5 后面);
    • 7 结尾的LIS:[2,5,7][2,3,7] → 长度 3。

基于这个直观观察,我们可以定义状态 dp[i] 表示“以 nums[i] 结尾的最长递增子序列长度”,再推导转移逻辑。

2. 核心逻辑与代码实现

class Solution:def lengthOfLIS(self, nums):if not nums:return 0n = len(nums)# dp[i]:以nums[i]结尾的最长递增子序列长度dp = [1] * n  # 初始化:每个元素自身就是长度为1的子序列for i in range(1, n):  # 从第2个元素开始(索引1)for j in range(i):  # 遍历i前面所有元素jif nums[j] < nums[i]:  # 满足递增条件,可接在j后面dp[i] = max(dp[i], dp[j] + 1)  # 更新最长长度return max(dp)  # 所有dp[i]的最大值就是LIS长度

3. 特殊代码的作用

  • dp = [1] * n:每个元素自身可独立构成长度为 1 的子序列,这是所有状态的“基础值”,不能初始化为 0(否则会导致长度计算错误);
  • 双重循环 iji 遍历每个“结尾元素”,j 遍历“可能接在前面的元素”,通过 nums[j] < nums[i] 判断是否能形成更长子序列;
  • max(dp[i], dp[j] + 1):确保 dp[i] 始终记录“以 nums[i] 结尾的最长长度”,避免被 shorter 的子序列覆盖。

思路2:贪心+二分(O(nlogn))—— 为“效率”优化的贪心思想

1. 思路是怎么产生的?

动态规划的 O(n²) 时间复杂度,在 n > 10^4 时会超时(比如 nums 长度为 1e5)。此时需要思考:能否不记录每个元素结尾的LIS,只关注“如何让子序列更长”?

核心洞察:要让递增子序列尽可能长,需要让子序列的“尾部元素尽可能小”—— 尾部越小,后续遇到的元素就越容易比它大,从而能延长子序列。
比如:长度为 2 的子序列,[2,3][2,5] 更好,因为 3 更小,后续的 4 能接在 [2,3] 后面形成长度 3 的子序列,而 4 不能接在 [2,5] 后面。

基于这个贪心思想,我们引入 tails 数组:tails[k] 表示“长度为 k+1 的递增子序列的最小尾部元素”。通过维护 tails,我们可以快速判断新元素能否延长子序列,或优化已有长度的子序列尾部。

2. 核心逻辑与代码实现

class Solution:def lengthOfLIS(self, nums):if not nums:return 0tails = []  # tails[k]:长度为k+1的LIS的最小尾部元素for x in nums:# 情况1:x比tails最后一个元素大,直接加入,延长LISif not tails or x > tails[-1]:tails.append(x)# 情况2:x不大于tails最后一个元素,找替换位置else:# 二分查找:找到tails中第一个 >= x 的元素索引left, right = 0, len(tails) - 1while left < right:mid = (left + right) // 2if tails[mid] >= x:right = midelse:left = mid + 1# 用x替换该位置,更新最小尾部tails[left] = xreturn len(tails)  # tails的长度就是LIS的长度

3. 特殊代码的作用

  • tails 数组:核心是“最小尾部”,而非真实的LIS(比如示例中最终 tails = [2,3,7,18],长度正确,但不是唯一的LIS);
  • x > tails[-1]:判断能否延长LIS,直接加入即可,因为 tails 是严格递增的(后续会证明);
  • 二分查找:因为 tails 严格递增,所以用二分在 O(logn) 时间内找到“第一个 >= x 的元素”,避免遍历(否则时间退化为 O(n²));
  • tails[left] = x:替换后,该长度的LIS尾部更小,为后续元素留更多延长空间(比如用 3 替换 5 后,tails = [2,3],后续 7 能直接加入)。

三、关键疑问解答:新手最易踩的坑

1. 贪心+二分中,tails 为什么是严格递增的?

这是二分查找能生效的前提!用反证法证明:
假设 tails[k] >= tails[k+1]

  • tails[k] 是“长度为 k+1 的LIS的最小尾部”;
  • tails[k+1] 是“长度为 k+2 的LIS的最小尾部”,而长度为 k+2 的LIS必然包含一个长度为 k+1 的子序列,其尾部 <= tails[k+1]
    tails[k] >= tails[k+1],则“长度为 k+1 的最小尾部” >= “长度为 k+2 的最小尾部”,矛盾!因此 tails 必须严格递增。

2. 为什么替换 tails[left] 而不是 tails[right]

循环结束后 left == right,两者指向同一个位置——“第一个 >= x 的元素索引”,替换哪个都等价!
选择 left 是习惯:二分查找中,left 是“主动向目标靠近”的指针(比如 tails[mid] < x 时,left = mid + 1),逻辑更统一,且避免额外判断。

3. 贪心+二分只能得到长度,怎么输出真实的LIS?

tails 不是真实的LIS,若需输出具体序列,需额外记录:

  • parent 数组记录每个元素在LIS中的前一个元素;
  • len_list 数组记录每个元素结尾的LIS长度;
  • 最后通过 max(len_list) 找到LIS的最后一个元素,回溯 parent 数组得到完整序列(具体实现可参考“动态规划+回溯”)。

4. 动态规划的 O(n²) 真的不能优化吗?

对于“仅求长度”的场景,贪心+二分是最优解(O(nlogn));但如果题目要求“输出所有LIS”“带权重的LIS”(比如每个元素有分值,求总分最大的LIS),动态规划仍是更直接的选择,此时需接受 O(n²) 时间。

四、两种方法的全面对比

对比维度 动态规划(O(n²)) 贪心+二分(O(nlogn))
时间复杂度 O(n²) O(nlogn)
空间复杂度 O(n)(dp数组) O(n)(tails数组)
核心优势 思路直观,可输出具体LIS,支持带权重变种 效率极高,适合大数据量(n>1e4),面试首选
核心劣势 大数据量超时 无法直接输出具体LIS,贪心思想需理解
适用场景 小规模数据、需输出LIS、带权重LIS 大规模数据、仅需LIS长度、面试最优解
状态定义 dp[i]:以nums[i]结尾的LIS长度 tails[k]:长度为k+1的LIS的最小尾部

五、类似问题扩展:LIS思路的迁移

掌握LIS的两种解法后,可快速解决以下变种问题(核心思路不变,仅需微调条件):

1. 最长不递减子序列(允许元素相等)

  • 改动:nums[j] < nums[i]nums[j] <= nums[i](动态规划);x > tails[-1]x >= tails[-1],二分查找“第一个 > x 的元素”(贪心+二分)。

2. 最长严格递减子序列

  • 改动:nums[j] < nums[i]nums[j] > nums[i](动态规划);x > tails[-1]x < tails[-1],二分查找“第一个 <= x 的元素”(贪心+二分)。

3. 带权重的LIS(每个元素有分值,求总分最大的LIS)

  • 思路:动态规划,dp[i] = max(dp[j] + score[i])nums[j] < nums[i])。

4. 二维LIS(比如“俄罗斯套娃信封”问题)

  • 思路:先按宽度升序排序(宽度相等时按高度降序),再对高度求LIS(贪心+二分)。

5. 最少拦截系统(导弹拦截问题)

  • 转化:本质是求“最长不递增子序列的长度”(贪心+二分)。

六、解题思路Trigger:看到题目就知道用什么方法

遇到“子序列递增/递减”相关问题时,按以下步骤快速决策:

1. 第一步:判断问题核心

  • 若题目要求“最长递增/递减子序列”(无论是否连续)→ 核心是LIS问题;
  • 若题目有“最多”“最少”“最长”等关键词,且涉及“元素大小关系+子序列”→ 优先联想LIS思路。

2. 第二步:根据数据规模选方法

  • n <= 1e3 → 动态规划(思路简单,不易出错);
  • n > 1e4 → 贪心+二分(必须优化时间,否则超时);
  • 若要求“输出具体子序列”→ 动态规划+回溯(贪心+二分无法直接实现)。

3. 第三步:根据变种条件微调

  • 允许相等 → 调整比较符号(<<=>>=);
  • 递减子序列 → 反转比较符号(<>);
  • 二维问题 → 排序后转化为一维LIS。

核心口诀

  • 见“最长子序列+递增/递减”→ 想LIS;
  • 见“大数据量”→ 贪心+二分;
  • 见“输出具体序列”→ 动态规划;
  • 见“二维/带权重”→ 动态规划或排序后LIS。

总结

最长递增子序列的两种解法,代表了“直观思路优化”的典型过程:动态规划从“每个元素的局部状态”切入,保证正确性但效率一般;贪心+二分从“全局优化目标”(最小尾部)出发,用二分法提升效率,是面试中的最优解。

掌握这道题的关键在于:

  1. 理解 dp[i]tails 的核心定义(状态定义是动态规划和贪心的基础);
  2. 搞懂 tails 严格递增的原因(二分查找的前提);
  3. 能根据题目条件快速迁移思路(变种问题的核心是“微调比较条件”)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/957646.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!