最长递增子序列(LIS)问题详解

最长递增子序列LIS问题详解

    • 一、问题定义与核心特征
      • 1.1 问题描述
      • 1.2 核心特征
    • 二、基础解法:动态规划(DP)
      • 2.1 解法思路
      • 2.2 Java代码实现
      • 2.3 复杂度分析
    • 三、优化解法:二分查找+贪心
      • 3.1 核心思路
      • 3.2 二分查找的作用
      • 3.3 Java代码实现
        • 代码说明:
      • 3.4 复杂度分析
    • 四、变种问题与拓展
      • 4.1 最长非递减子序列
      • 4.2 输出具体的最长递增子序列
      • 4.3 二维LIS问题(信封嵌套)
    • 五、LIS的实际应用场景
    • 六、两种解法的对比与选择
    • 总结

最长递增子序列(Longest Increasing Subsequence,简称LIS)是动态规划领域的经典问题,它看似简单——在一个无序数组中找到最长的严格递增子序列(子序列无需连续),但背后隐藏着从暴力到高效的多种解法思路。本文我将系统解析LIS问题的核心逻辑,从基础的动态规划到优化的“二分查找+贪心”解法,并结合代码实现、复杂度分析及变种拓展全面解读。

一、问题定义与核心特征

1.1 问题描述

给定一个整数数组nums,找到其中最长的严格递增子序列的长度。

  • 子序列:由数组中部分元素组成,元素顺序与原数组一致(无需连续)。
  • 严格递增:子序列中每个元素都大于前一个元素(如[2,5,7]是递增子序列,[2,2,3]不是)。

示例:

  • 输入:nums = [10,9,2,5,3,7,101,18]
  • 输出:4(最长递增子序列为[2,3,7,18][2,5,7,101]

1.2 核心特征

  • 无连续性要求:子序列元素在原数组中可间隔存在(区别于“最长递增子数组”)。
  • 严格递增:需满足nums[i] < nums[j]i < j),若允许相等则为“最长非递减子序列”。
  • 多解性:可能存在多个长度相同的最长子序列,但问题通常只要求返回长度。

二、基础解法:动态规划(DP)

动态规划是解决LIS问题的基础方法,核心思路是通过“以每个元素为结尾的最长子序列长度”推导全局最优。

2.1 解法思路

  1. 定义状态
    dp[i]表示“以nums[i]为结尾的最长递增子序列的长度”。
    (核心:子序列必须包含nums[i],且nums[i]是子序列的最后一个元素)

  2. 递推关系
    对于每个i(当前元素),遍历所有j < i(前驱元素):

    • nums[j] < nums[i](满足递增),则dp[i]可由dp[j] + 1更新(在前驱子序列后添加nums[i])。
    • 取所有符合条件的dp[j] + 1的最大值,即:
      dp[i] = max(dp[j] + 1) (j < i 且 nums[j] < nums[i])
    • 若没有符合条件的j(即nums[i]比所有前驱都小),则dp[i] = 1(自身为长度1的子序列)。
  3. 边界条件
    所有dp[i]初始化为1(每个元素自身都是长度为1的子序列)。

  4. 结果计算
    全局最长递增子序列长度为dp数组中的最大值。

2.2 Java代码实现

import java.util.Arrays;public class LIS_DP {public int lengthOfLIS(int[] nums) {if (nums == null || nums.length == 0) {return 0;}int n = nums.length;int[] dp = new int[n];Arrays.fill(dp, 1); // 初始化:每个元素自身是长度1的子序列int maxLen = 1; // 最小长度为1for (int i = 1; i < n; i++) {// 遍历所有前驱元素jfor (int j = 0; j < i; j++) {// 若j位置元素小于i位置,尝试更新dp[i]if (nums[j] < nums[i]) {dp[i] = Math.max(dp[i], dp[j] + 1);}}// 更新全局最大值maxLen = Math.max(maxLen, dp[i]);}return maxLen;}public static void main(String[] args) {LIS_DP solution = new LIS_DP();int[] nums = {10, 9, 2, 5, 3, 7, 101, 18};System.out.println(solution.lengthOfLIS(nums)); // 输出4}
}

2.3 复杂度分析

  • 时间复杂度O(n2)O(n^2)O(n2)。外层循环遍历n个元素,内层循环对每个元素遍历其所有前驱(平均n/2次),总操作次数为n×n/2=O(n2)n \times n/2 = O(n^2)n×n/2=O(n2)
  • 空间复杂度O(n)O(n)O(n)。需要dp数组存储n个状态。

适用场景:数组长度较小(n ≤ 1000)时,该方法简单直观,易于实现。

三、优化解法:二分查找+贪心

当数组长度较大(如n > 10^4)时,O(n2)O(n^2)O(n2)的动态规划解法会超时。“二分查找+贪心”解法通过优化状态维护方式,将时间复杂度降至O(nlog⁡n)O(n \log n)O(nlogn),是LIS问题的最优解法。

3.1 核心思路

贪心思想:维护一个尽可能小的“递增子序列尾部元素”。尾部元素越小,后续能添加的元素就越多,越容易形成更长的子序列。

例如:

  • 对于nums = [2,5,3,7],若已有的子序列尾部是[2,5],当遇到3时,可将5替换为3(得到[2,3])——虽然当前长度不变,但尾部更小,后续可添加7形成[2,3,7](比[2,5,7]更优)。

具体步骤:

  1. 定义一个列表tailstails[i]表示“长度为i+1的递增子序列的最小尾部元素”。
  2. 遍历数组nums,对每个元素x
    • x大于tails的最后一个元素,直接加入tails(子序列长度+1)。
    • 否则,在tails中找到第一个大于等于x的元素,用x替换它(维持尾部最小化)。
  3. 最终tails的长度即为LIS的长度。

3.2 二分查找的作用

tails是严格递增的(因为每次添加的元素都大于尾部,替换的元素也保证了递增性),因此可通过二分查找快速定位“第一个大于等于x的元素”,时间复杂度从O(n)O(n)O(n)降至O(log⁡n)O(\log n)O(logn)

3.3 Java代码实现

import java.util.ArrayList;
import java.util.List;public class LIS_BinarySearch {public int lengthOfLIS(int[] nums) {if (nums == null || nums.length == 0) {return 0;}List<Integer> tails = new ArrayList<>();for (int x : nums) {// 二分查找:找到tails中第一个 >= x的元素索引int left = 0, right = tails.size();while (left < right) {int mid = left + (right - left) / 2;if (tails.get(mid) < x) {left = mid + 1; // x更大,继续向右找} else {right = mid; // 可能找到,缩小右边界}}// 若left等于长度,说明x是最大的,直接添加if (left == tails.size()) {tails.add(x);} else {// 否则替换对应位置的元素tails.set(left, x);}}return tails.size();}public static void main(String[] args) {LIS_BinarySearch solution = new LIS_BinarySearch();int[] nums = {10, 9, 2, 5, 3, 7, 101, 18};System.out.println(solution.lengthOfLIS(nums)); // 输出4}
}
代码说明:
  • tails始终保持严格递增,例如输入[10,9,2,5,3,7,101,18]时,tails的变化过程为:
    [10] → [9] → [2] → [2,5] → [2,3] → [2,3,7] → [2,3,7,101] → [2,3,7,18]
    最终长度为4,与预期结果一致。

3.4 复杂度分析

  • 时间复杂度O(nlog⁡n)O(n \log n)O(nlogn)。遍历数组n次,每次二分查找操作耗时O(log⁡k)O(\log k)O(logk)k为当前tails长度,最大为n),总复杂度为n×log⁡n=O(nlog⁡n)n \times \log n = O(n \log n)n×logn=O(nlogn)
  • 空间复杂度O(n)O(n)O(n)tails列表最坏情况下存储n个元素(数组完全递增时)。

适用场景:数组长度较大(n ≥ 10^4)时,该方法效率显著优于动态规划。

四、变种问题与拓展

4.1 最长非递减子序列

问题:允许子序列元素相等(即nums[i] ≤ nums[j]),求最长子序列长度。
解法:修改二分查找条件——将“找到第一个大于等于x的元素”改为“找到第一个大于x的元素”(允许替换相等元素)。
代码调整:

// 非递减子序列的二分查找条件
if (tails.get(mid) <= x) { // 原条件为 < xleft = mid + 1;
} else {right = mid;
}

4.2 输出具体的最长递增子序列

基础解法和优化解法均只能得到长度,若需输出具体子序列,需结合动态规划记录前驱:

  1. dp数组记录长度,prev数组记录每个元素的前驱索引。
  2. 找到dp数组最大值对应的索引,通过prev回溯得到子序列。

示例代码:

public int[] getLIS(int[] nums) {int n = nums.length;int[] dp = new int[n];int[] prev = new int[n];Arrays.fill(dp, 1);Arrays.fill(prev, -1);int maxLen = 1, maxIndex = 0;for (int i = 1; i < n; i++) {for (int j = 0; j < i; j++) {if (nums[j] < nums[i] && dp[j] + 1 > dp[i]) {dp[i] = dp[j] + 1;prev[i] = j; // 记录前驱}}if (dp[i] > maxLen) {maxLen = dp[i];maxIndex = i; // 记录最长子序列的结尾索引}}// 回溯构建子序列int[] lis = new int[maxLen];int index = maxLen - 1;while (maxIndex != -1) {lis[index--] = nums[maxIndex];maxIndex = prev[maxIndex];}return lis;
}

4.3 二维LIS问题(信封嵌套)

问题:给定n个信封(w, h),若w1 < w2h1 < h2则可嵌套,求最多嵌套层数(本质是二维LIS)。
解法:

  1. 按宽度w升序排序(宽度相等时按高度h降序,避免同宽信封嵌套)。
  2. 对高度h求LIS,长度即为最大嵌套层数。

五、LIS的实际应用场景

  1. 任务调度:安排依赖关系的任务,找到最长的连续执行序列。
  2. 导弹拦截:拦截导弹的高度需严格递增,求最多拦截数量。
  3. 数据可视化:在时序数据中找到最长的增长趋势段。
  4. 算法优化:作为其他问题的子步骤(如最长递增子序列的个数、最大上升子序列和等)。

六、两种解法的对比与选择

解法时间复杂度空间复杂度优势场景核心思想
动态规划O(n2)O(n^2)O(n2)O(n)O(n)O(n)数组短(n ≤ 1000)、需输出子序列以每个元素为结尾的子序列长度
二分查找+贪心O(nlog⁡n)O(n \log n)O(nlogn)O(n)O(n)O(n)数组长(n ≥ 10^4)、仅需长度维护最小尾部元素,贪心优化

总结

LIS问题是动态规划与贪心思想结合的经典案例,从O(n2)O(n^2)O(n2)的基础解法到O(nlog⁡n)O(n \log n)O(nlogn)的优化解法,体现了“状态优化”的核心思路——通过更高效的状态维护(如tails列表)减少冗余计算。

掌握LIS问题的关键在于:

  1. 理解动态规划的状态定义(以每个元素为结尾的子序列长度);
  2. 掌握“二分查找+贪心”的优化逻辑(最小尾部元素的维护);
  3. 能根据问题需求(长度/具体子序列、数组大小)选择合适解法。

That’s all, thanks for reading~~
觉得有用就点个赞、收进收藏夹吧!关注我,获取更多干货~

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

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

相关文章

什么是HTTP长连接、短连接?谁更能抗DoS攻击?

想象你在快餐店点餐&#xff1a; 你&#xff1a;“一个汉堡”收银员&#xff1a;“好的&#xff0c;15元”交易结束&#xff0c;你离开队伍你想加杯可乐&#xff0c;重新排队你&#xff1a;“一杯可乐”收银员&#xff1a;“好的&#xff0c;8元”再次离开… 这种每次沟通后立即…

微软徽标认证是什么?如何快速获取驱动签名?

在Windows系统中安装硬件驱动时&#xff0c;是否遇到过“无法验证发布者”的警告&#xff1f;这正是驱动数字签名在背后发挥作用。对于软件开发者而言&#xff0c;驱动数字签名不仅是系统兼容性的保障&#xff0c;更是企业品牌信任度的核心。一、驱动数字签名的核心作用驱动数字…

Apache Ignite缓存基本操作

这段内容主要讲解了 Apache Ignite 中缓存&#xff08;IgniteCache&#xff09;的基本操作&#xff0c;包括获取缓存、创建缓存、销毁缓存、执行原子操作以及异步操作等。下面我将用中文对这些内容进行详细解释&#xff0c;帮助你更好地理解。一、获取缓存实例&#xff08;Gett…

最新基于R语言结构方程模型分析与实践技术应用

现代统计学理论和方法的不断完善&#xff0c;使科研工作对统计方法的要求也越来越高&#xff0c;面对纷繁复杂的数据&#xff0c;如何选择最为合适的数据分析方法已成为科研工作者&#xff0c;尤其是广大刚处于科研生涯起步阶段的研究生们最为棘手问题。随着科学的发展&#xf…

物联网_TDengine_EMQX_性能测试

一、Tdengine接口开发文档 1、数据库 1.创建数据库 URL /dp/createdb/ method post 请求示例 {"db_name":"demo01" // 必填 }响应示例 // 成功 {"code": 1,"data": {"成功创建数据库": "demo04"},"error…

从分析到优化:Amazon Q CLI 助力 EKS 网络调用链剖析与运维实践

1. 引言 在 Amazon EKS&#xff08;Elastic Kubernetes Service&#xff09;环境中&#xff0c;理解从 ALB&#xff08;Application Load Balancer&#xff09;到 Pod 的完整网络调用链对运维人员至关重要。本文将展示如何利用 Amazon Q CLI 这一 AI 助手工具&#xff0c;通过…

Class10简洁实现

Class10简洁实现 import torch from torch import nn from d2l import torch as d2l# 输入为28*28&#xff0c;输出为10类&#xff0c;第1、2隐藏层256神经元 num_inputs, num_outputs, num_hiddens1, num_hiddens2 784, 10, 256, 256 # 第1个隐藏层丢弃率为0.2&#xff0c;第…

【多线程篇22】:ConcurrentHashMap的并发安全原理剖析

文章目录一、HashMap 的“不安全”&#xff1a;问题的根源1. 数据结构回顾 (JDK 1.8)2. 并发下的致命缺陷&#xff1a;put 操作二、ConcurrentHashMap 的安全之道 (JDK 1.8)1. 核心数据结构2. 安全的 put 操作&#xff1a;分场景精细化加锁3. 安全的 size() 计算&#xff1a;并…

【Java + Vue 实现图片上传后 导出图片及Excel 并压缩为zip压缩包】

系统环境&#xff1a; Java JDK&#xff1a;1.8.0_202 Node.js&#xff1a;v12.2.0 Npm&#xff1a;6.9.0 Java后端实现 Controller /*** xxxx-导出* param response 返回信息体* param files 上传的图片文件* param param1 参数1* param param2 参数2*/PostMapping("/ex…

安科瑞:能源微电网助力工业园区“绿色”发展

朱以真近日&#xff0c;厦门市工业和信息化局印发工业园区绿色智慧微电网建设&#xff0c;拟开展全市工业园区绿色智慧微电网试点通知&#xff0c;那么对于如何实现绿色园区的建设是今天的话题。对工业园区绿色智慧微电网建设需求&#xff0c;其核心价值体现在“源-网-荷-储-充…

VUE2 学习笔记3 v-on、事件修饰符、键盘事件

事件处理v-on用于事件交互。语法&#xff1a;v-on:要绑定的事件“事件触发时执行的函数” &#xff08;函数这里可以写括号&#xff0c;也可以不写&#xff0c;没有影响&#xff09;简写&#xff1a;:事件触发时要执行的函数&#xff0c;在Vue配置参数中&#xff0c;通过method…

变换域通讯系统CCSK的matlab仿真

CCSK&#xff08;Cyclic Code Shift Keying&#xff09;通信系统的MATLAB仿真。实现完整的CCSK调制、AWGN信道传输和解调过程&#xff0c;并计算了误码率&#xff08;BER&#xff09;。 % CCSK通信系统仿真 clear; clc; close all;% 参数设置 L 31; % m序列…

技术演进中的开发沉思-40 MFC系列:多线程协作

今天说说MFC的线程&#xff0c;当年用它实现中间件消息得心应手之时&#xff0c;可以实现一边实时接收数据&#xff0c;一边更新界面图表图文信息&#xff0c;顺滑得让人想吹声口哨。 MFC 多线程它像给程序装上了分身术&#xff0c;让原本只能 “单任务跑腿” 的代码&#xff0…

高速公路自动化安全监测主要内容

近年来&#xff0c;随着社会经济的快速发展&#xff0c;高速公路的通车里程不断增加&#xff0c;交通流量日益增大。与此同时&#xff0c;高速公路交通事故数量也呈现出一定的增长趋势。这些事故不仅造成了大量的人员伤亡和财产损失&#xff0c;还严重影响了社会的稳定和经济的…

完美解决 Ubuntu 中自定义启动器图标重复的问题(以 MATLAB 为例)

如果你在 Ubuntu 上为 MATLAB、PyCharm、Android Studio 或其他第三方应用创建了自定义启动器&#xff0c;并把它固定到了左侧的 Dock 栏&#xff0c;你很可能遇到过这个令人抓狂的场景&#xff1a; 当你满心欢喜地点击固定的图标启动程序后&#xff0c;Dock 栏上竟然“凭空”冒…

一文读懂深度模型优化器,掌握炼丹工具

深度模型优化器是训练神经网络的核心工具&#xff0c;其目标是高效地找到损失函数的最小值。从基础的随机梯度下降&#xff08;SGD&#xff09;到结合一阶动量修正与两阶段更新的Ranger&#xff0c;优化器的发展始终围绕着加速收敛、提升稳定性、适应参数差异和增强泛化能力四大…

关于虚拟机部署的k8s集群calico组件在开机恢复快照时的问题

在恢复虚拟机开机快照的时候总会出现各种的网络问题&#xff0c;连接不到自己的443端口&#xff0c;集群状态异常等等&#xff0c;绕一圈都发现不了问题&#xff0c;其实根本就没问题。虚拟机环境再现&#xff0c;恢复快照后的k8s环境&#xff0c;nfs莫名宕掉提示此报错&#x…

从感知到决策:虚拟仿真系统与视觉算法融合下的多路RTSP视频接入技术探究

1️⃣ 背景概述 随着国防信息化和智能化进程的不断加快&#xff0c;虚拟仿真系统 与 智能视觉算法平台 正逐步成为现代化装备研发、测试验证与战术训练的重要技术支撑。相比传统的静态建模或离线推演&#xff0c;新一代作战仿真与智能系统更强调 实时性、感知能力与动态交互&a…

Android app如果不适配Android 15会怎么样

深度思考完成 用时49秒 首先&#xff0c;用户的问题是&#xff1a;“Android app如果不适配Android 15会怎么样&#xff1f;”我需要基于搜索结果来回答。 让我回顾搜索结果&#xff1a; ‌1&#xff1a;标题是“Android 15应用适配指南”&#xff0c;内容强调了不适配Androi…

Vue-21-利用Vue3的axios+Python的flask实现前后端交互功能

文章目录 1 任务需求 2 前端 2.1 模板template 2.2 JS代码script 2.3 Calculate.vue(子组件) 2.4 App.vue(根组件) 3 后端 3.1 导入模块 3.2 创建应用实例 3.3 配置CORS 3.4 定义路由 3.5 处理请求 3.6 main.py 4 附录 4.1 CORS 4.1.1 全局启用CORS 4.1.2 限制允许的域名(更安…