求递增子序列LIS的两种方法

news2025/4/21 0:18:37

文章目录

  • 前言
  • 一、普通动态规划(DP)求解LIS
    • 1.DP思路
    • 2.DP的状态定义与转移方程
    • 3.DP的时间与空间复杂度
    • 4.DP代码实现
    • 5.DP的图文示例
  • 二、贪心 + 二分查找求解LIS
    • 1.思路分析
    • 2.贪心 + 二分的时间与空间复杂度
  • 三. 模板题讲解
    • 1.洛谷B3637 最长上升子序列
      • 1.dp写法
      • 2.贪心+二分写法
    • 2.洛谷P3902 递增
      • 1.贪心+二分写法
  • 四.练习题
    • 1.洛谷P1091 [NOIP 2004 提高组] 合唱队形
      • 1.思路分析
    • 2.洛谷P1020 [NOIP 1999 提高组] 导弹拦截
      • 1.思路分析
  • 总结


前言

提示:这里可以添加本文要记录的大概内容:

在开始讲解算法之前,我们先明确LIS的定义

子序列:从一个序列中挑选一些元素(不要求连续),但必须保持原始相对顺序。例如,对于序列 [10, 9, 2, 5],[10, 2] 和 [9, 5] 都是子序列。
严格递增:子序列中的每个元素必须比前一个元素严格大。例如,[1, 3, 5] 是严格递增的,但 [1, 3, 3] 不是(因为有相等的情况)。
LIS问题:给定一个序列,找出其中最长的严格递增子序列的长度
示例:

输入序列:[10, 9, 2, 5, 3, 7, 101, 18]
可能的递增子序列:
[10, 101](长度 2)
[2, 5, 7, 101](长度 4)
[2, 3, 7, 18](长度 4)
答案:最长递增子序列的长度为 4。
接下来,我们将详细介绍两种方法来解决这个问题
并且给出多道例题进行讲解


提示:以下是本篇文章正文内容,下面案例可供参考

一、普通动态规划(DP)求解LIS

1.DP思路

动态规划(DP)是一种通过将大问题分解为小问题来求解的方法。对于LIS,我们可以用DP逐步计算出每个位置的最优解,最终得到全局最优解。核心思想是:对于每个元素,考虑它能接在哪些之前的元素后面,形成更长的递增子序列

2.DP的状态定义与转移方程

状态定义
定义 dp[i] 表示 第 i 个元素 nums[i] 结尾最长递增子序列的长度
例如,dp[0] 表示以 nums[0] 结尾的LIS长度,dp[1] 表示以 nums[1] 结尾的LIS长度。

状态转移方程
对于位置 i,我们需要检查它之前的所有位置 j(0 ≤ j < i):
如果 nums[j] < nums[i],说明 nums[i] 可以接在 nums[j] 后面,形成一个更长的递增子序列。此时,dp[i] 可以更新为 dp[j] + 1。
为了确保 dp[i] 是最大的,我们需要从所有满足条件的 j 中挑选 dp[j] 最大的值,然后加 1

dp[i] = max(dp[j] + 1) 对于所有 j < i 且 nums[j] < nums[i]  这里遍历所有可能的j情况取最大的那一种就行

如果没有满足条件的 j(即 nums[i] 比之前所有元素都小),则 dp[i] = 1,因为它自身就是一个长度为 1 的子序列。
初始化
每个 dp[i] 初始值为 1,因为最短的递增子序列就是元素本身。
最终答案
遍历整个 dp 数组,找到最大的 dp[i],这就是整个序列的LIS长度

3.DP的时间与空间复杂度

时间复杂度O(n²)!!!
外层循环遍历每个位置 i(n 次),内层循环遍历 0 到 i-1(平均 n/2 次),总复杂度为 O(n²)。

空间复杂度:O(n)
只需一个长度为 n 的 dp 数组存储状态

4.DP代码实现

#include <iostream>
#include <vector>
#include <algorithm> 

using namespace std;

int lengthOfLIS(vector<int>& nums) {
    int n = nums.size();
    if (n == 0) return 0; // 空序列返回 0
    
    vector<int> dp(n, 1); // 初始化 dp 数组,每个位置至少为 1
    int maxLen = 1;// 记录全局最大 LIS 长度
    
    for (int i = 0; i < n; i++) {以第i个元素为结尾 dp[i]
        for (int j = 0; j < i; j++) {//遍历结尾元素i前面的元素
            if (nums[j] < nums[i]) {
                dp[i] = max(dp[i], dp[j] + 1); // 更新 dp[i]
            }
        }
        maxLen = max(maxLen, dp[i]); // 更新全局最大值
    }
    return maxLen;
}

int main() {
    vector<int> nums = {10, 9, 2, 5, 3, 7, 101, 18};
    cout << "最长递增子序列长度: " << lengthOfLIS(nums) << endl; // 输出 4
    return 0;
}

5.DP的图文示例

在这里插入图片描述
结果总结
最终 dp 数组:[1, 1, 1, 2, 2, 3, 4, 4]
LIS 长度:4
表格字段说明
索引 i:当前处理元素的下标。
nums[i]:序列中第 i 个元素的值。
dp[i]:以 nums[i] 结尾的最长递增子序列的长度。
计算过程:描述 dp[i] 是如何从前面的 dp[j](j < i)计算得到的。
可能的子序列示例:一个以 nums[i] 结尾的递增子序列,仅为示例,不一定是全局最优解

二、贪心 + 二分查找求解LIS

1.思路分析

普通动态规划(DP)求 LIS 的时间复杂度是 O(n²)因为它需要比较每个元素与之前所有元素的关系。而“贪心 + 二分查找”方法通过一种更高效的方式,将复杂度降到 O(n log n)。其核心在于:

1.目标:维护一个“最优”的递增子序列(不一定是最终的 LIS),确保每个长度的子序列末尾元素尽可能小。这样,后续元素就更容易接在这个子序列后面,从而最大化 LIS 的长度
2.工具:使用一个数组 d,其中 tails[i] 表示长度为 i+1 的递增子序列的末尾元素的最小值
3.操作规则:!!!!!!!!!!!!!!!!!!!!!!!!!!

如果当前元素大于 tails 的最后一个元素,直接将它追加到 tails 末尾,延长子序列
如果当前元素小于等于 tails 的最后一个元素,用二分查找找到 tails 中第一个大于等于当前元素的位置,并替换它,优化某个长度的子序列末尾

重要说明:d数组本身不一定是最终的 LIS,但它的长度一定等于 LIS 的长度
在这里插入图片描述
这是算法竞赛入门到进阶这门书的原话解释,请大家仔细理解

关键点!!!
虽然 d 的长度是正确的它的元素只是用来维护这个长度的工具不一定直接对应原始序列中的一个实际递增子序列
但长度一定是最长的递增子序列的长度!!!!!!!!!!!!!

2.贪心 + 二分的时间与空间复杂度

时间复杂度:O(n log n)
遍历序列 n 次,每次二分查找复杂度为 O(log n)。

空间复杂度:O(n)
用于存储d 数组

三. 模板题讲解

1.洛谷B3637 最长上升子序列

在这里插入图片描述
题目不用分析,因为题意说的很清除了,现在我给出dp和贪心+二分的两种写法

1.dp写法

#include <iostream>
#include <algorithm>
using namespace std;
int n, arr[5005];
int dp[5005] ; // 以第i个数结尾的序列长度为dp[i];
int maxlen = 1;
int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        cin >> arr[i];
        dp[i] = 1;//初始化长度都为1 因为就是自己
    }

    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j < i; j++)//遍历i之前的元素
        {
            if (arr[j] < arr[i])
                dp[i] = max(dp[i], dp[j] + 1);
        }
    }

    int maxa = 0;
    for (int i = 1; i <= n; i++)
    {
        maxa = max(maxa, dp[i]);//找最大值
    }
    cout << maxa;
    return 0;
}

这样也能过,因为数据很小,n的范围我圈出来了,大家看上面的图

2.贪心+二分写法

#include <iostream>
using namespace std;
typedef long long ll;
const ll M = 1e5 + 5;
ll n, a[M], d[M], len;

void slove()
{
  cin>>n;
  for (int i = 0;i<n;i++)
  {
    cin >> a[i];
  }

  d[0]=a[0];
  len = 0;//长度为i+1 因为从0开始
  for (int i = 1;i<n;i++)//这里从1开始 因为0位置我们初始化了
  {
    if(a[i]>d[len])//直接放入即可
    {
      len++;//这个别忘记
      d[len] = a[i];
    }
    else if(a[i]<d[len])
    {
      //查找第一个大于或等于a[i]的元素
      ll pos = lower_bound(d, d + len + 1, a[i]) - d;
      d[pos] = a[i];//其实没找到也没啥 因为没找到会返回数组last的位置 
      所以这里不判断也没事 长度又没更新
    }
  }

  cout << len + 1; // 别忘记加1 因为从0开始
}

signed main()
{
  //关流 加速输入输出
  ios::sync_with_stdio(0);
  cin.tie(0);
  cout.tie(0);
  slove();
  return 0;
}

2.洛谷P3902 递增

在这里插入图片描述
大家仔细看n最大可以到1e5,那么就不能再用dp的双重循环了
如果有网友不信邪,可以自己试试哦哈哈哈
那么这里我直接给一份贪心+二分代码

1.贪心+二分写法

#include <iostream>
using namespace std;
typedef long long ll;
const ll M = 1e5 + 5;
ll n, a[M], d[M], len;

void slove()
{
  cin>>n;
  for (int i = 0;i<n;i++)
  {
    cin >> a[i];
  }

  d[0]=a[0];
  len = 0;//长度为i+1 因为从0开始
  for (int i = 1;i<n;i++)//这里从1开始 因为0位置我们初始化了
  {
    if(a[i]>d[len])//直接放入即可
    {
      len++;//这个别忘记
      d[len] = a[i];
    }
    else if(a[i]<d[len])
    {
      //查找第一个大于或等于a[i]的元素
      ll pos = lower_bound(d, d + len + 1, a[i]) - d;
      d[pos] = a[i];//其实没找到也没啥 因为没找到会返回数组last的位置 
      所以这里不判断也没事 长度又没更新
    }
  }

  cout << n-(len + 1); // 别忘记加1 因为从0开始
}

signed main()
{
  //关流 加速输入输出
  ios::sync_with_stdio(0);
  cin.tie(0);
  cout.tie(0);
  slove();
  return 0;
}

四.练习题

1.洛谷P1091 [NOIP 2004 提高组] 合唱队形

在这里插入图片描述

1.思路分析

题目意思我们可以翻译为从数组中找一个点p,然后从前到这个点找一个递增子序列,从后到这个点找一个递增子序列,使得两个子序列和最大就行
在这里插入图片描述
全部代码我放在GitHub上了可以点击此处进入,记得挂梯子哦

2.洛谷P1020 [NOIP 1999 提高组] 导弹拦截

在这里插入图片描述

1.思路分析

第一个输出很常规,就是从后往前找一个最长递增子序列,因为题目说的是找递减的,因为后面的不能比前面的大,那我们反过来求就行
但第二个输出是一个关键点!!!!!!!
问题的本质
这个问题实际上是在问:如何用最少的严格递增子序列来覆盖整个序列。根据组合数学中的一个重要定理——Dilworth 定理(在偏序集上),我们可以得出以下结论

一个序列能被划分成的最少严格递增子序列的数量,等于该序列中最长的严格递减子序列!!!

换句话说,要解决这个问题,我们需要:

1.计算序列中最长的严格递减子序列的长度。
2.这个长度就是答案

那么我们一开始说了,这道题中我们从后往前求出来的就是递增子序列,所以显而易见,求递减子序列,我们只要从前往后就行

全部代码我放在GitHub上了可以点击此处进入,记得挂梯子哦

总结

大家可以发现 稍微难一点的都会卡n的范围,故意设置到1e5,所以这个LIS的贪心+二分的方法很有必要学习
还有就是练习题第2道的求最少多少个 递增! 子序列可以覆盖全部数组数据的这个定理请牢记!
数组的最长严格 递减 !子序列的长度即为数量

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

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

相关文章

【Linux篇】进程状态(僵尸进程,孤儿进程),优先级与调度机制

&#x1f4cc; 个人主页&#xff1a; 孙同学_ &#x1f527; 文章专栏&#xff1a;Liunx &#x1f4a1; 关注我&#xff0c;分享经验&#xff0c;助你少走弯路&#xff01; 文章目录 1. 前文铺垫理解内核链表 2. 进程状态2.1 进程状态查看2.2 僵尸进程2.3 僵尸进程危害2.4 孤儿…

AI应用加速落地丨MaxKB正在被政府、公共事业、教育和医疗行业用户广泛采纳

2025年2月至3月上旬&#xff0c;伴随着各个行业接入并使用DeepSeek&#xff0c;MaxKB开源知识库问答系统正在被越来越多的行业用户所采纳&#xff0c;是人工智能行业落地的强应用。目前&#xff0c;MaxKB在政府、公共事业、教育和医疗四大行业已经拥有了众多典型案例&#xff0…

2024年第十五届蓝桥杯软件C/C++大学A组——五子棋对弈

蓝桥杯原题&#xff1a; 题目描述&#xff1a; “在五子棋的对弈中&#xff0c;友谊的小船说翻就翻&#xff1f; ” 不&#xff01;对小蓝和小桥来说&#xff0c;五子棋不仅是棋盘上的较量&#xff0c;更是心与心之间的沟通。这两位挚友秉承着 “ 友谊第一&#xff0c;比赛第二…

复试难度解析,西电先进材料与纳米科技学院学院考研录取情况

01、先进材料与纳米科技学院各个方向 02、24先进材料与纳米科技学院近三年复试分数线对比 PS&#xff1a;材料院24年院线学硕方向降低10分&#xff0c;专硕上涨15分&#xff1b;材料院在分数线相对于其他211、985院校对比来看&#xff0c;依然分数偏低&#xff0c;推荐大家关注…

Deepseek Chatgpt Kimi 推荐的深度学习书单

朋友让推荐一些深度学习的书&#xff0c;让 Deepseek、Chatgpt、Kimi 分别生成了一份书单并做了对比&#xff0c;记录一下以备以后用到。 Chatgpt 推荐的深度学习书 1. chatgpt 推荐的书目截图 1.2 Chatgpt 推荐的深度学习书目文字版 如果你想学习 Deep Learning&#xff0…

高频面试题(含笔试高频算法整理)基本总结回顾25

干货分享&#xff0c;感谢您的阅读&#xff01; &#xff08;暂存篇---后续会删除&#xff0c;完整版高频面试题基本总结回顾&#xff08;含笔试高频算法整理&#xff09;&#xff09; 备注&#xff1a;引用请标注出处&#xff0c;同时存在的问题请在相关博客留言&#xff0c…

mac安装mysql之后报错zsh: command not found: mysql !

在Mac上安装MySQL后&#xff0c;如果终端中找不到mysql命令&#xff0c;通常是 因为MySQL的命令行工具&#xff08;如mysql客户端&#xff09;没有被正确地添加到你的环境变量中。 检查 MySQL 是否已安装 ps -ef|grep mysql查看到路径在 /usr/local/mysql/bin 查看 .bash_pro…

蓝桥杯备考:set容器用法(lower_bound)---营业额统计

如图所示&#xff0c;这道题的暴力解法就是枚举每天的营业额&#xff0c;让该营业额和前面的天的营业额依次相减取最小值这样的话我们的时间复杂度就是N平方&#xff0c;我们是很有可能超时的 所以我们选择用set容器的二分查找功能 我们每次遍历到一个数的时候&#xff0c;前…

VSCode集成C语言开发环境

下载MinGW https://sourceforge.net/projects/mingw/ 点击download按钮下载exe文件到本地 点击exe文件安装 选择基础包和c编译版 vscode安装部分跳过 安装code runner和c/c插件 **(1) 创建 C 文件** 新建一个测试文件&#xff08;例如 hello.c&#xff09;&#xf…

Python----数据可视化(pyecharts二:绘图一:条形图,直方图,折线图,散点图,箱图,饼图,热力图)

1、条形图 from pyecharts.charts import Bar from pyecharts.faker import Faker from pyecharts import options as opts # 绘制柱状图 bar (Bar() # 创建柱状图.add_yaxis("商家A", Faker.values(),colorFaker.rand_color()) # 添加数据.add_yaxis("商家B&…

Training-free Neural Architecture Search for RNNs and Transformers(预览版本)

摘要 神经架构搜索 (NAS) 允许自动创建新的有效神经网络架构&#xff0c;为手动设计复杂架构的繁琐过程提供了替代方案。然而&#xff0c;传统的 NAS 算法速度慢&#xff0c;需要大量的计算能力。最近的研究调查了图像分类架构的无训练 NAS 指标&#xff0c;大大加快了搜索算…

计算机考研C语言

C语言程序设计从入门到精通【2025完整版】考研复试 嵌入式 计算机二级 软考 专升本也适用_哔哩哔哩_bilibili 1、第一个C程序 helloC #include <stdio.h>int main(){printf("hehe");return 0;}每个C语言程序不管有多少行代码&#xff0c;都是从main函数开始执…

【MySQL】(4) 表的操作

一、创建表 语法&#xff1a; 示例&#xff1a; 生成的数据目录下的文件&#xff1a; 二、查看表结构 三、修改表 语法&#xff1a; 另一种改表名语法&#xff1a;rename table old_name1 to new_name1, old_name2 to new_name2; 示例&#xff1a; 四、删除表 语法&#xf…

Qt 中实现自定义控件子类化

一、子类化关键步骤 ‌1、选择基类‌ 根据需求选择合适的 Qt 原生控件作为基类&#xff08;如 QWidget、QPushButton、QSpinBox 等&#xff09;&#xff0c;通过继承实现功能扩展‌。 ‌2、重写关键方法‌ ‌绘制逻辑‌&#xff1a;重写 paintEvent() 方法&#xff0c;使用 Q…

6. MySQL 索引的数据结构(详细说明)

6. MySQL 索引的数据结构(详细说明) 文章目录 6. MySQL 索引的数据结构(详细说明)1. 为什么使用索引2. 索引及其优缺点2.1 索引概述 3. InnoDB中索引的推演3.1 索引之前的查找3.2 设计索引3.3 常见索引概念1. 聚簇索引2. 二级索引&#xff08;辅助索引、非聚簇索引&#xff09;…

pytorch 50 大模型导出的onnx模型优化尝试

本博文基于Native-LLM-for-Android项目代码实现,具体做了以下操作: 1、尝试并实现将模型结构与权重零散的onnx模型进行合并,通过该操作实现了模型加载速度提升,大约提升了3倍 2、突破了onnxconverter_common 无法将llm模型导出为fp16的操作,基于该操作后将10g的权重降低到…

LeetCode1871 跳跃游戏VII

LeetCode 跳跃游戏 IV&#xff1a;二进制字符串的跳跃问题 题目描述 给定一个下标从 0 开始的二进制字符串 s 和两个整数 minJump 和 maxJump。初始时&#xff0c;你位于下标 0 处&#xff08;保证该位置为 0&#xff09;。你需要判断是否能到达字符串的最后一个位置&#xf…

RabbitMQ 从入门到精通

1 MQ架构设计原理 1.1 什么是消息中间件 消息中间件基于队列模型实现异步/同步传输数据 作用&#xff1a;可以实现支撑高并发、异步解耦、流量削峰、降低耦合度。 1.2 传统的http请求存在那些缺点 1.Http请求基于请求与响应的模型&#xff0c;在高并发的情况下&#xff0c…

考研复试c语言常见问答题汇总2

11. 关键字和一般标识符有什么不同&#xff1f; C语言中关键字与一般标识符区别&#xff1a; 定义&#xff1a;关键字是C语言预定义的特殊单词&#xff08;如int、for&#xff09;&#xff0c;有固定含义&#xff1b;标识符是自定义的名称&#xff08;如变量名、函数名&#xf…

Qt表格美化笔记

介绍 表格是一种常见的数据管理界面形式&#xff0c;在大批量的数据交互情形下使用的比较多 表格 可以通过样式表设置线条以及边框的颜色 QTableWidget { gridline-color : rgb(55, 60, 62); border: 1px solid rgb(62,112,181);}表头 如果表头和第一行的分割线显示&#…