当前位置: 首页 > news >正文

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

文章目录

  • 前言
  • 一、普通动态规划(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道的求最少多少个 递增! 子序列可以覆盖全部数组数据的这个定理请牢记!
数组的最长严格 递减 !子序列的长度即为数量

相关文章:

  • PHP语法基础
  • C++ Primer Plus 编程练习题 第四章 复合类型
  • 星越L_行李舱空间拓展讲解
  • 智能血糖仪语音提示播报-高品质音质WT2003H芯片方案
  • 广东省首批服务机器人应用技术员三级职业技能等级认定及申请条件
  • 设备预测性维护:企业降本增效的关键密码​
  • DeFi开发的深度解析与展望
  • flutter 图片资源路径管理
  • Android jetpack 之LiveData 结合ViewModel的简单使用
  • FX-std::list
  • rv1106 PWM控制
  • C语言为例谈数据依赖性
  • Android 7 及以上,Fiddler 抓 https 包
  • 容器适配方法模拟Stack和Queue
  • Ansible命令行模式常用模块使用案例(二)
  • Keil创建stm32f103c8t6标准库
  • c++常用的算术生成算法
  • 深度学习项目--基于DenseNet网络的“乳腺癌图像识别”,准确率090%+,pytorch复现
  • RGV调度算法
  • C++20 `<bit>` 中的整数 2 的幂运算和 `std::bit_cast`:由浅入深的探索
  • 免费论坛网站建设/网络营销推广方案案例
  • 以中文做域名的网站/网络优化工作应该怎么做
  • 网页制作及网站建设/搜索广告和信息流广告区别
  • 如何自己建立网站/人民日报客户端
  • 复制审查元素做网站/腾讯广告官网
  • css网站元素设计/百度搜索