【动态规划】求最长递增子序列问题

目录

问题描述

最长递增子序列(Longest Increasing Subsequence,LIS

  • 子序列:对于任意序列s,它的子序列是通过删除其中零个或多个元素得到的另⼀个序列
    注:剩余元素的相对顺序保持不变

给定n个整数组成的序列 s [ 1... n ] s[1...n] s[1...n],求最长递增子序列LIS(的长度)

83613547

递推关系

建立递推关系的思路

假设能够求出 s [ 1... k − 1 ] s[1...k-1] s[1...k1]LIS,考虑能否由此推出 s [ 1... k ] s[1...k] s[1...k]LIS

  • 如果仅知道长度
    • 无法判断 s [ k ] s[k] s[k] 能否让LIS变长
  • 如果不仅知道长度,还知道具体序列 L
    • s [ k ] s[k] s[k] 能让 L 变长,那问题就解决了
    • 也许 L 就是 s [ 1... k ] s[1...k] s[1...k]LIS
    • 也许存在 s [ 1... k ] s[1...k] s[1...k] 的另⼀个LIS:L‘, s [ k ] s[k] s[k] 能让L’变长
    • 可能需要记住 s [ 1... k − 1 ] s[1...k-1] s[1...k1] 的所有LIS

原始子问题: 令 L ( k ) L(k) L(k) 表示 s [ 1... k ] s[1...k] s[1...k]LIS的长度,原问题即求解 L ( n ) L(n) L(n)

  • O(n)个子问题,但不容易建立递归关系

约束条件:以 s [ k ] s[k] s[k] 结尾

  • L ( k ) L(k) L(k) 表示 s [ 1... n ] s[1...n] s[1...n] s [ k ] s[k] s[k] 结尾LIS的长度
  • 原问题即为求解 max ⁡ 1 ≤ k ≤ n L ( k ) \max_{1\le k\le n}L(k) max1knL(k)
  • 基本情况: 如果 k = 1 k = 1 k=1,那么 L ( k ) = 1 L(k) = 1 L(k)=1
  • 归纳步骤
    • L k = max ⁡ { 1 , max ⁡ 1 ≤ i ≤ k − 1 { L ( i ) + 1 ∣ s [ k ] > s [ i ] } } L_k = \max \{1, \max_{1\le i \le k-1} \{ L(i) +1 | s[k] > s[i] \}\} Lk=max{1,max1ik1{L(i)+1∣s[k]>s[i]}},其中, max ⁡ ⊘ \max \oslash max的值定义为0

在这里插入图片描述

  • 此时的递推关系:

L ( k ) = { 1 i f k = 1 max ⁡ { 1 , max ⁡ 1 ≤ i ≤ k − 1 { L ( i ) + 1 ∣ s [ k ] > s [ i ] } } i f k > 1 L(k) = \begin{cases} 1 &if\quad k=1\\ \max \{1, \max_{1\le i \le k-1} \{ L(i) +1 | s[k] > s[i] \}\} &if \quad k>1 \end{cases} L(k)={1max{1,max1ik1{L(i)+1∣s[k]>s[i]}}ifk=1ifk>1

  • O ( n ) O(n) O(n) 个子问题,每个子问题复杂度为 O ( k ) O(k) O(k)。时间复杂度为 O ( n 2 ) O(n^2) O(n2)

约束条件:以 s [ k ] s[k] s[k] 开头

  • L ( k ) L(k) L(k) 表示 s [ 1... n ] s[1...n] s[1...n] s [ k ] s[k] s[k] 开头LIS的长度
  • 原问题即为求解 max ⁡ 1 ≤ k ≤ n L ( k ) \max_{1\le k\le n}L(k) max1knL(k)
  • 基本情况: 如果 k = n k = n k=n,那么 L ( k ) = 1 L(k) = 1 L(k)=1

在这里插入图片描述

  • 此时的递推关系:

L ( k ) = { 1 i f k = n max ⁡ { 1 , max ⁡ k + 1 ≤ i ≤ n { L ( i ) + 1 ∣ s [ k ] < s [ i ] } } i f k < n L(k) = \begin{cases} 1 &if\quad k=n\\ \max \{1, \max_{k+1\le i \le n} \{ L(i) +1 | s[k] < s[i] \}\} &if \quad k<n \end{cases} L(k)={1max{1,maxk+1in{L(i)+1∣s[k]<s[i]}}ifk=nifk<n

  • O ( n ) O(n) O(n) 个子问题,每个子问题复杂度为 O ( k ) O(k) O(k)。时间复杂度为 O ( n 2 ) O(n^2) O(n2)

约束条件:增加子问题参数(前缀)

  • L ( i , j ) L(i,j) L(i,j) 表示 s [ j . . . n ] s[j...n] s[j...n] 中每个元素都大于 s [ i ] s[i] s[i] LIS的长度
  • s [ 0 ] = − ∞ s[0] =-\infty s[0]= ,原问题即求解 L ( 0 , 1 ) L(0,1) L(0,1)
  • 基本情况: 如果 j > n j> n j>n ,那么 L ( i , j ) = 0 L(i,j)= 0 L(i,j)=0

在这里插入图片描述

  • 归纳步骤
    • 如果 s [ i ] > s [ j ] s[i] > s[j] s[i]>s[j] L ( i , j ) = L ( i , j + 1 ) L(i,j) = L(i,j+ 1) L(i,j)=L(i,j+1)
    • 否则 L ( i , j ) = max ⁡ { L ( i , j + 1 ) , 1 + L ( j , j + 1 ) } L(i,j) = \max \{ L(i,j+ 1),1 + L(j,j+ 1)\} L(i,j)=max{L(i,j+1),1+L(j,j+1)}
  • O ( n 2 ) O(n^2) O(n2)个子问题,每个子问题求解复杂度为 O ( 1 ) O(1) O(1),时间复杂度: O(n2); 空间复杂度: O(n2)
  • 此时的递推关系:
    L ( i , j ) = { 0 i f j > n L ( i , j + 1 ) i f s [ i ] ≥ s [ j ] max ⁡ { L ( i , j + 1 ) 1 + L ( j , j + 1 ) o t h e r w i s e L(i,j) = \begin{cases} 0 &if\quad j>n\\ L(i,j+1) &if\quad s[i]\ge s[j] \\ \max \begin{cases} L(i,j+1) \\ 1+L(j,j+1) \end{cases} &otherwise \end{cases} L(i,j)= 0L(i,j+1)max{L(i,j+1)1+L(j,j+1)ifj>nifs[i]s[j]otherwise

在这里插入图片描述


约束条件:增加子问题参数(后缀)

  • L ( i , j ) L(i,j) L(i,j) 表示 s [ 1... j ] s[1...j] s[1...j] 中每个元素都小于 s [ i ] s[i] s[i] LIS的长度
  • s [ n + 1 ] = ∞ s[n+1] =\infty s[n+1]= ,原问题即求解 L ( n + 1 , n ) L(n+1,n) L(n+1,n)
  • 基本情况: 如果 j = 0 j=0 j=0 ,那么 L ( i , j ) = 0 L(i,j)= 0 L(i,j)=0

在这里插入图片描述

  • 归纳步骤

    • 如果 s [ i ] ≤ s [ j ] s[i] \le s[j] s[i]s[j] L ( i , j ) = L ( i , j − 1 ) L(i,j) = L(i,j- 1) L(i,j)=L(i,j1)
    • 否则 L ( i , j ) = max ⁡ { L ( i , j − 1 ) , 1 + L ( j , j − 1 ) } L(i,j) = \max \{ L(i,j- 1),1 + L(j,j- 1)\} L(i,j)=max{L(i,j1),1+L(j,j1)}
  • 此时的递推关系:
    L ( i , j ) = { 0 i f j = 0 L ( i , j − 1 ) i f s [ i ] ≤ s [ j ] max ⁡ { L ( i , j − 1 ) 1 + L ( j , j − 1 ) o t h e r w i s e L(i,j) = \begin{cases} 0 &if\quad j=0\\ L(i,j-1) &if\quad s[i]\le s[j] \\ \max \begin{cases} L(i,j-1) \\ 1+L(j,j-1) \end{cases} &otherwise \end{cases} L(i,j)= 0L(i,j1)max{L(i,j1)1+L(j,j1)ifj=0ifs[i]s[j]otherwise

在这里插入图片描述


约束条件:LIS长度为k且末尾元素最小

  • 对于长度为 k k k 的递增子序列,只需记住末尾元素最小的那个

  • 本质是寻找上限最高(可拓展性最强)的那个子序列

  • L ( k ) L(k) L(k) 表示 s [ 1... n ] s[1...n] s[1...n] 中长度为 k k k 且末尾元素最小的递增子序列,且 L ( k ) . l a s t L(k).last L(k).last 表示该序列中最后那个元素

  • 引理: L ( 1 ) . l a s t < L ( 2 ) . l a s t < . . . < L ( k ) . l a s t L(1) .last < L(2) .last < ... < L(k).last L(1).last<L(2).last<...<L(k).last

    • 假设 x ≥ y x \ge y xy,而 y ≥ z y \ge z yz,所以 x ≥ z x \ge z xz
    • 那么灰色元素构成一个长度为 k k k 且末尾元素最小的递增子序列,矛盾
      在这里插入图片描述
  • 归纳假设: 对长度小于 n n n 的序列,可以计算其所有的 L ( k ) L(k) L(k),并有序存储

  • 基本情况: 长度为1的序列,有 L [ 1 ] ← s [ 1 ] L[1]\leftarrow s[1] L[1]s[1]

  • 如何基于归纳假设求解 s [ 1.. n ] s[1..n] s[1..n] 的所有的 L ( k ) L(k) L(k)

    • L ( k ) . l a s t L(k).last L(k).last 构成的有序数组中查找插入位置 k ′ k' k,使得 s [ n ] s[n] s[n] 加入后仍然有序
    • 如果 k ′ = k + 1 k' =k+1 k=k+1,那么 L ( k + 1 ) + L ( k ) + 1 L(k + 1) + L(k) +1 L(k+1)+L(k)+1 L ( k + 1 ) . l a s t ← s [ n ] L(k + 1).last \leftarrow s[n] L(k+1).lasts[n]
    • 否则 L ( k ′ ) . l a s t ← s [ n ] L(k').last \leftarrow s[n] L(k).lasts[n],但 L ( k ′ ) L(k') L(k) 的值不变
  • 时间复杂度: O ( l o g n ) O(logn) O(logn)

在这里插入图片描述


运行实例

#include <iostream>
#include <vector>
#include <algorithm>
#include <set>
using namespace std;

ostream& operator<<(ostream& os, const vector<int>& v) {
    for (auto e : v)
        os << e << ' ';
    return os;
}

int lis_dp1(const vector<int>& s, int n) {
    vector<int> dp(n + 1, 1); // 初始化dp数组,dp[i]表示以s[i]结尾的LIS的长度

    for (int k = 2; k <= n; ++k) {
        for (int i = 1; i < k; ++i) {
            if (s[k] > s[i]) {
                dp[k] = max(dp[k], dp[i] + 1);
            }
        }
    }

    return *max_element(dp.begin(), dp.end()); // 返回dp数组中的最大值作为整个序列的最长递增子序列的长度
}

int lis_dp2(const vector<int>& s, int n) {
    vector<vector<int>> dp(n + 2, vector<int>(n + 2, -1)); // 初始化dp数组,dp[i][j]表示L(i,j)

    for (int i = 0; i <= n + 1; ++i)
        dp[i][n + 1] = 0; // 基本情况:当 j > n 时,L(i,j) = 0

    for (int j = n; j >= 1; --j) {
        for (int i = 0; i < j; ++i) {
            if (s[i] >= s[j]) {
                dp[i][j] = dp[i][j + 1]; // 如果s[i] >= s[j],则L(i,j) = L(i,j+1)
            }
            else {
                dp[i][j] = max(dp[i][j + 1], 1 + dp[j][j + 1]); // 否则L(i,j) = max{L(i,j+1), 1+L(j,j+1)}
            }
        }
    }

    return dp[0][1]; // 返回L(0,1)作为整个序列的最长递增子序列的长度
}

int lis_dp3(const vector<int>& s, int n) {
    
    if (n == 0) return 0;

    set<int> L;

    L.insert(s[1]);

    for (int i = 2; i < n+1; ++i) {
        if (s[i] > * L.rbegin()) {
            L.insert(s[i]);
        }
        else {
            L.erase(L.lower_bound(s[i]));
            L.insert(s[i]);
        }
    }

    return L.size();
}

vector<int> find_lis_dp1(const vector<int>& s, int n) {
    vector<int> dp(n + 1, 1); // 初始化dp数组,dp[i]表示以s[i]结尾的LIS的长度
    vector<int> parent(n + 1, -1); // 记录每个元素的父节点索引

    for (int k = 2; k <= n; ++k) {
        for (int i = 1; i < k; ++i) {
            if (s[k] > s[i] && dp[k] < dp[i] + 1) {
                dp[k] = dp[i] + 1;
                parent[k] = i; // 更新父节点索引
            }
        }
    }

    int max_length = *max_element(dp.begin(), dp.end()); // 获取最长递增子序列的长度
    int max_index = distance(dp.begin(), find(dp.begin(), dp.end(), max_length)); // 获取最长递增子序列的结束索引

    vector<int> lis;
    while (max_index != -1) {
        lis.push_back(s[max_index]);
        max_index = parent[max_index]; // 根据父节点索引回溯
    }

    reverse(lis.begin(), lis.end()); // 反转得到正确顺序的最长递增子序列

    return lis;
}

vector<int> find_lis_dp2(const vector<int>& s, int n) {
    vector<vector<int>> dp(n + 2, vector<int>(n + 2, -1)); // 初始化dp数组,dp[i][j]表示L(i,j)
    vector<vector<int>> parent(n + 2, vector<int>(n + 2, -1)); // 记录每个元素的父节点索引

    for (int i = 0; i <= n + 1; ++i)
        dp[i][n + 1] = 0; // 基本情况:当 j > n 时,L(i,j) = 0

    for (int j = n; j >= 1; --j) {
        for (int i = 0; i < j; ++i) {
            if (s[i] >= s[j]) {
                dp[i][j] = dp[i][j + 1];
            }
            else {
                dp[i][j] = max(dp[i][j + 1], 1 + dp[j][j + 1]);
                if (dp[i][j] == dp[j][j + 1] + 1) {
                    parent[i][j] = j; // 更新父节点索引
                }
            }
        }
    }

    vector<int> lis;
    int i = 0, j = 1;
    while (j <= n) {
        if (dp[i][j] == dp[j][j + 1] + 1) {
            lis.push_back(s[j]);
            i = j;
        }
        ++j;
    }

    return lis;
}

vector<int> find_lis_dp3(const vector<int>& s, int n) {
    vector<int> lis;
    set<int> L;

    L.insert(s[1]);

    for (int i = 2; i < n + 1; ++i) {
        if (s[i] > *L.rbegin()) {
            L.insert(s[i]);
        }
        else {
            L.erase(L.lower_bound(s[i]));
            L.insert(s[i]);
        }
    }
    
    for (int num : L) {
        lis.push_back(num);
    }
    return lis;
}

int main(int argc, const char* argv[]) {
    vector<int> s = { -1, 8, 3, 6, 1, 3, 5, 4, 7 }; // 注意s[0]仅作标识,真实数据为s[1]~s[n]
    cout << lis_dp1(s, s.size() - 1) << endl;
    cout << find_lis_dp1(s, s.size() - 1) << endl;
    cout << "------------------------------"  << endl;
    cout << lis_dp2(s, s.size() - 1) << endl;
    cout << find_lis_dp2(s, s.size() - 1) << endl;
    cout << "------------------------------" << endl;
    cout << lis_dp3(s, s.size() - 1) << endl;
    cout << find_lis_dp3(s, s.size() - 1) << endl;
    cout << "------------------------------" << endl;
    return 0;
}

运行结果:

在这里插入图片描述


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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MySQL -DDL 及表类型

DDL 创建数据库 CREATE DATABASE [IF NOT EXISTS] db_name [create_specification [, create_specification] ...] create_specification:[DEFAULT] CHARACTER SET charset_name [DEFAULT] COLLATE collation_name 1.CHARACTER SET&#xff1a…

Java后端开发——MVC商品管理程序

Java后端开发——MVC商品管理程序 本篇文章内容主要有下面几个部分&#xff1a; MVC架构介绍项目环境搭建商品管理模块Servlet代码重构BaseServlet文件上传 MVC 是模型-视图-控制器&#xff08;Model-View-Controller&#xff09;&#xff0c;它是一种设计模式&#xff0c;也…

.net7.0中把exe和dll分开打包

之前写过 C#把dll分别放在指定的文件夹_wpf core dll 放文件夹-CSDN博客 C#把dll打包到exe_c# 打包exe_故里2130的博客-CSDN博客 这都是老技术了&#xff0c;可以进行参考。 现在的.netcore系列有单独支持把exe和dll分开打包的功能了&#xff0c;当然也支持.net7.0和.net8.…

开源堡垒机Jumpserver

文章目录 开源堡垒机JumpserverJumpserver介绍安装环境部署安装jumpserver访问jumpserver的web界面 开源堡垒机Jumpserver Jumpserver介绍 Jumpserver 是全球首款完全开源的堡垒机&#xff0c;使用 GNU GPL v2.0 开源协议&#xff0c;是符合 4A 的运维安全审计系统。 Jumpse…

AIGC-文生视频

stable diffusion&#xff1a; stable diffusion原理解读通俗易懂&#xff0c;史诗级万字爆肝长文&#xff0c;喂到你嘴里 - 知乎个人网站一、前言&#xff08;可跳过&#xff09;hello&#xff0c;大家好我是 Tian-Feng&#xff0c;今天介绍一些stable diffusion的原理&#…

js小技巧|如何提取经过Function函数混淆了的代码

关注它&#xff0c;不迷路。 本文章中所有内容仅供学习交流&#xff0c;不可用于任何商业用途和非法用途&#xff0c;否则后果自负&#xff0c;如有侵权&#xff0c;请联系作者立即删除&#xff01; 1.需求 星友发过来一个混淆代码&#xff0c;打开一看&#xff0c;长这…

(三)Pytorch快速搭建卷积神经网络模型实现手写数字识别(代码+详细注解)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言Q1&#xff1a;卷积网络和传统网络的区别Q2:卷积神经网络的架构Q3:卷积神经网络中的参数共享&#xff0c;也是比传统网络的优势所在4、 具体的实现代码网络搭建…

C++二分查找、离线算法:最近的房间

作者推荐 利用广度优先或模拟解决米诺骨牌 本文涉及的基础知识点 二分查找算法合集 题目 一个酒店里有 n 个房间&#xff0c;这些房间用二维整数数组 rooms 表示&#xff0c;其中 rooms[i] [roomIdi, sizei] 表示有一个房间号为 roomIdi 的房间且它的面积为 sizei 。每一…

linux设置主机名

查看主机名&#xff1a;hostname 临时修改主机名&#xff1a;hostname 新主机名 [rootlocalhost ~]#hostname centos [rootlocalhost ~]#hostname centos 永久修改主机名&#xff1a; [rootlocalhost ~]#cat /etc/hostname localhost.localdomain

ArrayList 和 HashMap 源码解析

1、ArrayList 1.1、ArrayList 构造方法 无参创建一个 ArrayList 数组默认为空数组 transient Object[] elementData; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA {}; private int size; // 数组容量大小public ArrayList() {this.elementData DEFA…

基于springboot校园车辆管理系统

背景 伴随着社会经济的快速发展&#xff0c;机动车保有量不断增加。不断提高的大众生活水平以及人们不断增长的自主出行需求&#xff0c;人们对汽车的 依赖性在不断增强。汽车已经发展成为公众日常出行的一种重要的交通工具。在如此形势下&#xff0c;高校校园内的机动车数量也…

java设计模式学习之【原型模式】

文章目录 引言原型模式简介定义与用途实现方式UML 使用场景优势与劣势原型模式在spring中的应用员工记录示例代码地址 引言 原型模式是一种创建型设计模式&#xff0c;它允许对象能够复制自身&#xff0c;以此来创建一个新的对象。这种模式在需要重复地创建相似对象时非常有用…

近五年—中国十大科技进展(2018年—2022年)

近五年—中国十大科技进展&#xff08;2018-2022&#xff09; 2022年中国十大科技进展1. 中国天眼FAST取得系列重要进展2. 中国空间站完成在轨建造并取得一系列重大进展3. 我国科学家发现玉米和水稻增产关键基因4. 科学家首次发现并证实玻色子奇异金属5. 我国科学家将二氧化碳人…

Vue 定义只读数据 readonly 与 shallowReadonly

readonly 让一个响应式数据变为 **深层次的只读数据**。 shallowReadonly 让一个响应式数据变为 **浅层次的只读数据**&#xff0c;只读第一层。 isReadonly 判断一个数据是不是只读数据。 应用场景&#xff1a;不希望数据被修改时使用。 readonly深层次只读&#xff1a; …

读像火箭科学家一样思考笔记12_实践与测试(下)

1. 舆论的火箭科学 1.1. 如果苹果违反了“即飞即测”原则&#xff0c;那苹果的iPhone就不会问世了 1.1.1. iPhone在其上市前的民意调查中相当失败 1.1.1.1. iPhone不可能获得太大市场份额&#xff0c;不可能。 1.1.1.1.1. 微软前CEO史蒂夫鲍尔默&#xff08;Steve Ballmer&…

msng病毒分析

这是一个非常古老的文件夹病毒&#xff0c;使用XP系统的文件夹图标&#xff0c;采用VB语言开发&#xff0c;使用了一种自定义的壳来保护&#xff0c;会打开网址http://www.OpenClose.ir,通过软盘、U盘和共享目录进行传播&#xff0c;会在U盘所有的目录下生成自身的副本&#xf…

采集工具-免费采集器下载

在当今信息时代&#xff0c;互联网已成为人们获取信息的主要渠道之一。对于研究者和开发者来说&#xff0c;如何快速准确地采集整个网站数据是至关重要的一环。以下将从九个方面详细探讨这一问题。 确定采集目标 在着手采集之前&#xff0c;明确目标至关重要。这有助于确定采集…

三季度营收下滑16.3%,网易云音乐如何讲出新故事?

在选择重新回归音乐本身后&#xff0c;网易云音乐(09899.HK)业绩承压的困局写在最新的三季报里。 「不二研究」据网易云音乐三季报发现&#xff1a;今年三季度&#xff0c;网易云音乐净收入同比下滑16.3%。目前&#xff0c;网易云音乐主要面临营收下滑、商业化场景探索尚未形成…

MSB3541 Files 的值“<<<<<<< HEAD”无效。路径中具有非法字符。

MSB3541 Files 的值“<<<<<<< HEAD”无效。路径中具有非法字符。 一般来说出现这个问题是因为使用git版本控制工具合并代码出现了问题&#xff0c;想要解决也很简单。 如图点击错误后定位到文件&#xff0c;发现也没有什么问题。 根据错误后边的提示&a…

前后端分离开发出现的跨域问题

先说说什么是跨域。 请求的URL地址中的协议、域名、端口号中的任意一个与当前URL不同就是跨域。 比如&#xff1a; 当前页面的URL请求的URL是否跨域原因htttp://localhost:8080htttps://localhost:8080是协议不同htttp://localhostll:8080htttp://localhost:8080是域名不同htt…