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

news2024/11/19 1:48:55

目录

问题描述

最长递增子序列(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;
}

运行结果:

在这里插入图片描述


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

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

相关文章

mysql主从复制-redis集群扩容缩容、缓存优化(缓存更新策略、穿透,击穿,雪崩)、mysql主从搭建、django实现读写分离

基于Docker实现读写分离 1 redis集群扩容缩容 1.1 集群扩容 1.2 集群缩容 2 缓存优化 2.1 缓存更新策略 2.2 穿透&#xff0c;击穿&#xff0c;雪崩 3 mysql主从搭建 4 django实现读写分离 1 redis集群扩容缩容 1.1 集群扩容 # 6台机器&#xff0c;3个节点集群# 8台机器&am…

Blazor Select 实现点击一次选项触发一次后台事件

Blazor的官方案例中&#xff0c;Select组件只有两个事件 1、OnSelectedItemChanged 每次选项的时候改变触发&#xff0c;如果你点击同一个选项是不会触发后台的方法的 2、OnBeforeSelectedItemChange 我们可以用这个事件实现每次点击同一个选项都可以触发后台事件 需要注意下最…

同旺科技 USB 转 RS-485 适配器 -- 隔离型

内附链接 1、USB 转 RS-485 适配器 隔离版主要特性有&#xff1a; ● 支持USB 2.0/3.0接口&#xff0c;并兼容USB 1.1接口&#xff1b; ● 支持USB总线供电&#xff1b; ● 支持Windows系统驱动&#xff0c;包含WIN10 / WIN11 系统32 / 64位&#xff1b; ● 支持Windows …

电子学会C/C++编程等级考试2022年06月(三级)真题解析

C/C++等级考试(1~8级)全部真题・点这里 第1题:制作蛋糕 小A擅长制作香蕉蛋糕和巧克力蛋糕。制作一个香蕉蛋糕需要2个单位的香蕉,250个单位的面粉,75个单位的糖,100个单位的黄油。制作一个巧克力蛋糕需要75个单位的可可粉,200个单位的面粉,150个单位的糖,150个单位的黄…

Spring Boot 3.2.0 Tomcat虚拟线程初体验 (部分装配解析)

写在前面 spring boot 3 已经提供了对虚拟线程的支持。 虚拟线程和平台线程主要区别在于&#xff0c;虚拟线程在运行周期内不依赖操作系统线程&#xff1a;它们与硬件脱钩&#xff0c;因此被称为 “虚拟”。这种解耦是由 JVM 提供的抽象层赋予的。 虚拟线程的运行成本远低于平…

UE 事件分发机制 day9

观察者模式原理 观察者模式通常有观察者与被观察者&#xff0c;当被观察者状态发生改变时&#xff0c;它会通知所有的被观察者对象&#xff0c;使他们能够及时做出响应&#xff0c;所以也被称作“发布-订阅模式”。总得来说就是你关注了一个主播&#xff0c;主播的状态改变会通…

逆向 wa 发送图片

开发工具 工具名称工具类型说明AndroidStuduo编辑工具开发工具jadxjava工具将apk解成java项目xposed插件工具插件whatsApp版本2.23.16.77 分析源码的点&#xff1a; 发送图片的点 获取SendMedia 回调 实现 public void sendImg(String user, String path) throws Exce…

毫米波雷达DOA角度计算-----MUSIC算法

MUSIC算法如下&#xff1a; txNum &#xff1a;发射天线 2个 &#xff0c;rxNum&#xff1a;接收天线 4 个 。 ant &#xff1a; 为目标点的 天线 接收数据 &#xff0c; 为 8*1矩阵。 A ant;d 0.5;M 1; % # 快拍数ang_ax -90:90; % 角度坐标% 接收信号方向向量for k1:…

04 # 第一个 TypeScript 程序

初始化项目以及安装依赖 新建 ts_in_action 文件夾 npm init -y安装好 typescript&#xff0c;就可以执行下面命令查看帮助信息 npm i typescript -g tsc -h创建配置文件&#xff0c;执行下面命令就会生成一个 tsconfig.json 文件 tsc --init使用 tsc 编译一个 js 文件 新…

PVE中CT容器安装openwrt X86的极简方法

下载推荐&#xff1a;https://openwrt.ai/ 使用环境PVE8.0&#xff0c;openwrt是以上网址的最新版&#xff0c;内涵及其丰富组件。 问题来源&#xff1a; 在PVE虚拟机可以很方便的使用img文件&#xff0c;转换qm 成一个硬盘文件&#xff0c;加入到虚拟机也就完成了&#xff0c…

python基础练习题库实验6

文章目录 题目1代码实验结果题目2代码实验结果题目3代码实验结果题目4代码实验结果题目总结题目1 根据以下规范编写一个函数: 函数名称:triple输入参数:1个输入参数数据类型字符串返回值:函数返回1个字符串值。该字符串由每个字符重复3次的句子构成。例如,如果句子是Uni,…

科学与工程计算基础(数值计算)知识点总结

数值计算 第1章 概论1.2 数值计算中的误差1.2.1 误差的来源和分类1.2.2 误差与有效数字1.2.3 数值运算的误差估计 1.3 误差定性分析和避免误差危害1.3.1 算法的数值稳定性1.3.3 避免误差危害 1.4 数值计算中算法设计的技术1.5 习题1.5.1 判断题1.5.2 计算题 第2章 插值法2.2 拉…

i已学赋能智慧教育时代的幼儿教育

伴随“教育数字化战略行动”的深入开展,智慧教育正式成为国家战略。智慧教育延伸至家校社教育的每个阶段。当前,为适应智慧教育发展趋势,我国制定了《中国教育现代化2035》《教育部关于加强“三个课堂”应用的指导意见》《教育信息化2.0行动计划》等文件。幼儿作为智慧教育、智…

计算机网络408

一&#xff1a;计算机网络体系结构 1.计网的概念&#xff0c;组成&#xff0c;功能和分类 一&#xff1a;计算机网络的发展 (3)从功能组成视觉看&#xff1a;分为资源子网和通信子网 2.计网性能指标

“PredictingChildrenHeight“ app Tech Support(URL)

Using our app, we can predict a childs height through formulas. Because there are many factors that affect a childs height, it is for reference only. ​​​​​​​ If you have any questions, you can either leave a message or send the questions to our em…

Unity-链接MySql5.7

链接MySql5.7 前言&#xff1a; 为什么不选择最新的MySQL8.0或者MySQL8.2呢&#xff0c;实际发现&#xff0c;如果使用这两个版本&#xff0c;虽然能够用同样的方法找到合适的dll&#xff0c;但是在编写代码的过程中往往会卡死&#xff0c;非常的影响效率&#xff0c;因此放弃…

11.28 知识回顾(Web框架、路由控制、视图层)

一、 web 框架 1.1 web框架是什么&#xff1f; 别人帮咱们写了一些基础代码------》我们只需要在固定的位置写固定的代码--》就能实现一个web应用 Web框架&#xff08;Web framework&#xff09;是一种开发框架&#xff0c;用来支持动态网站、网络应用和网络服务的开发。这大多…

模拟实现offsetof宏(详解)

我们在以前学过这个offsetof函数&#xff0c;知道它的功能是求指针相较于起始位置的偏移量&#xff0c;我们今天要来写出一个宏&#xff0c;计算结构体中某成员变量相对于起始位置的偏移。 目录 1.offsetof函数 1.1offsetof函数介绍 1.2offsetof函数代码实现 2.offsetof函数…

20 章 多线程

20.1线程简介. 20.2创建线程 2.1继承Thread类 Thread 类是java.lang包中的一个类&#xff0c;从这个类中实例化的对象代表线程&#xff0c;程序员启动一个新线程需要建立Thread 实例。Thread类中常用的两个构造方法如下: public Thread():创建一个新的线程对象。 public Thre…

JVM 参数介绍

在一些规模稍大的应用中&#xff0c;Java虚拟机&#xff08;JVM&#xff09;的内存设置尤为重要&#xff0c;想在项目中取得好的效率&#xff0c;GC&#xff08;垃圾回收&#xff09;的设置是第一步。 PermGen space&#xff1a;全称是Permanent Generation space.就是说是永久…