求解最长递增子序列(LIS) | 动态规划(DP)+ 二分法
1、题目描述
给定数组arr,返回arr的最长递增子序列。
2、举例
arr={2,1,5,3,6,4,8,9,7},返回的最长递增子序列为{1,3,4,8,9}。
3、解答
本期主要从动态规划和二分法两个方向来求解最长递增子序列问题。
3.1 动态规划求解最长递增子序列
先介绍时间复杂度为O(N^2^)的方法,具体过程如下:
- 生成数组dp,dp[i]表示在以arr[i]这个数结尾的情况下,arr[0…i]中的最大递增子序列长度。
- 对第一个数arr[0]来说,令dp[0]=1,接下来从左到右(i=1,2,…,N-1)依次算出以每个位置的数结尾的情况下,最长递增子序列长度。
- 假设计算到位置i,求以arr[i]结尾情况下的最长递增子序列长度,即dp[i]。如果最长递增子序列以arr[i]结尾,那么在arr[0…i-1]中所有比arr[i]小的数都可以作为倒数第二个数。在这么多倒数第二个数的选择中,以哪个数结尾的最大递增子序列更大,就选那个数作为倒数第二个数,所以dp[i]=max{dp[j]+1(0≤j<i,arr[j]<arr[i])}。如果arr[0…i-1]中所有的数都不比arr[i]小,令dp[i]=1即可,说明以arr[i]结尾情况下的最长递增子序列只包含arr[i]。
按照步骤1~3可以计算出dp数组,具体过程请参看如下代码中的方法,参考代码如下:
#include<stdio.h>
#define MAXN 1000
int arr[MAXN + 10];
int dp[MAXN + 10];
int main() {
int N, i, j;
scanf("%d", &N);
for (i = 0; i < N; ++i) {
scanf("%d", &arr[i]);
}
dp[0] = 1;
for (i = 1; i < N; ++i) {
/*
* 每次求以第i个数为终点的最长上升子序列的长度
*/
int tmp = 0;/* 记录满足条件的、第i个数左边的上升子序列的最大长度 */
for (j = 0; j < i; ++j) {
/* 查看以第j个数为终点的最长上升子序列 */
if (arr[i] > arr[j]) {
if (tmp < dp[j])
tmp = dp[j];
}
}
dp[i] = tmp + 1;
}
int ans = -1;
for (i = 0; i < N; ++i) {
if (ans < dp[i])
ans = dp[i];
}
printf("%d\n", ans);
return 0;
}
程序执行完后,数组arr[]和状态数组dp[]如下:
最长上升子序列有6个:(1,5,6,8,9)、(2,5,6,8,9)、(2,3,6,8,9)、(2,3,4,8,9)、(1,3,4,8,9)和(1,3,6,8,9),长度都是5。
问题:如果还要输出最长的子序列呢?例如,除了输出5之外,还要输出(1,3,4,8,9)这个序列。
接下来解释如何根据求出的dp数组得到最长递增子序列。以题目的例子来说明,arr={2,1,5,3,6,4,8,9,7},求出的数组dp={1,1,2,2,3,3,4,5,4}。具体求解步骤如下:
- 遍历dp数组,找到最大值以及位置。在本例中最大值为5,位置为7,说明最终的最长递增子序列的长度为5,并且应该以arr[7]这个数(arr[7]=9)结尾。
- 从arr数组的位置7开始从右向左遍历。如果对某一个位置i,既有arr[i]<arr[7],又有dp[i]=dp[7]-1,说明arr[i]可以作为最长递增子序列的倒数第二个数。在本例中,arr[6]<arr[7],并且dp[6]=dp[7]-1,所以8应该作为最长递增子序列的倒数第二个数。
- 从arr数组的位置6开始继续向左遍历,按照同样的过程找到倒数第三个数。在本例中,位置5满足arr[5]<arr[6],并且dp[5]=dp[6]-1,同时位置4也满足。选arr[5]或者arr[4]作为倒数第三个数都可以。
- 重复这样的过程,直到所有的数都找出来。
dp数组包含每一步决策的信息,其实根据dp数组找出最长递增子序列的过程就是从某一个位置开始逆序还原出决策路径的过程。具体过程请参看如下代码:
#include<stdio.h>
#include<stdlib.h> /* 动态内存分配 */
#define MAXN 1000
int arr[MAXN + 10];
int dp[MAXN + 10];
int main() {
int N, i, j;
scanf("%d", &N);
for (i = 0; i < N; ++i) {
scanf("%d", &arr[i]);
}
dp[0] = 1;
for (i = 1; i < N; ++i) {
/*
* 每次求以第i个数为终点的最长上升子序列的长度
*/
int tmp = 0;/* 记录满足条件的、第i个数左边的上升子序列的最大长度 */
for (j = 0; j < i; ++j) {
/* 查看以第j个数为终点的最长上升子序列 */
if (arr[i] > arr[j]) {
if (tmp < dp[j])
tmp = dp[j];
}
}
dp[i] = tmp + 1;
}
int ans = -1;
for (i = 0; i < N; ++i) {
if (ans < dp[i])
ans = dp[i];
}
printf("%d\n", ans); /* 输出最长递增子序列的长度 */
/*
* 下面根据dp数组还原出最长递增子序列。
* len中记录了最长递增子序列的长度,当然有len=ans。
* index记录最长递增子序列中最后一个数在arr数组中的位置。
*/
int len = 0;
int index = 0;
for (i = 0; i < N; ++i) {
if (dp[i] > len) {
len = dp[i];
index = i;
}
}
/*
* lis数组用来存放最长递增子序列。
*/
int* lis = (int*)malloc(sizeof(int) * len);
lis[--len] = arr[index]; /* 最长递增子序列中最后一个数为arr[index] */
for (i = index; i >= 0; i--) { /* 从index位置开始从右往左扫描数组arr */
if (arr[i] < arr[index] && dp[i] == dp[index] - 1) {
lis[--len] = arr[i];
index = i;
}
}
/* 打印最长递增子序列 */
for (i = 0; i < ans; ++i) {
printf("%d", lis[i]);
if (i < ans - 1)printf(" ");
}
printf("\n");
free(lis);
return 0;
}
输入:
9
2 1 5 3 6 4 8 9 7
输出:
5
1 3 4 8 9
运行结果:
计算dp数组过程的时间复杂度为O(N^2^),根据dp数组得到最长递增子序列过程的时间复杂度为O(N),所以整个过程的时间复杂度为O(N^2^)。
问题:如果把序列的长度增加到N=10^4^,10^5^,10^6^ 呢?如何将计算dp数组的时间复杂度降到O(Nlog N)?
3.2 二分法求解最长递增子序列
时间复杂度O(Nlog N)生成dp数组的过程是利用二分查找来进行的优化。先生成一个长度为N的数组ends,初始时ends[0]=arr[0],其他位置上的值为0。生成整型变量right, 初始时right=0。在从左到右遍历arr数组的过程中,求解dp[i]的过程需要使用ends数组和 right变量,所以这里解释一下其含义。遍历的过程中,ends[0..right]为有效区, ends[right+1..N-1]为无效区。对有效区上的位置b如果有ends[b]=c,则表示遍历到目前为止,在所有长度为b+1的递增序列中,最小的结尾数是c。无效区的位置则没有意义。
比如,arr=[2,1,5,3,6,4,8,9,7],初始时 dp[0]=1,ends[0]=2, right=0。ends[0..0]为有效区, ends[0]=2的含义是,在遍历过arr[0]之后,所有长度为1的递增序列中(此时只有[2]),最小的结尾数是2。之后的遍历继续用这个例子来说明求解过程。
- 遍历到arr[1]==1。ends有效区=ends[0..0]=[2],在有效区中找到最左边的大于或等于arr[1]的数。发现是ends[0],表示以arr[1]结尾的最长递增序列只有arr[1],所以令dp[1]=1。然后令ends[0]=1,因为遍历到目前为止,在所有长度为1的递增序列中,最小的结尾数是1,而不再是2。
- 遍历到arr[2]==5。ends有效区=ends[0..0]=[1],在有效区中找到最左边大于或等于arr[2]的数。发现没有这样的数,表示以arr[2]结尾的最长递增序列长度=ends有效区长度+1, 所以令dp[2]=2。ends整个有效区都没有比arr[2]更大的数,说明发现了比ends有效区长度更长的递增序列,于是把有效区扩大,ends有效区=ends[0..1]=[1,5]。
- 遍历到arr[3]==3。ends有效区=ends[0..1]=[1,5],在有效区中用二分法找到最左边大于或等于arr[3]的数。发现是ends[1],表示以arr[3]结尾的最长递增序列长度为2,所以令dp[3]=2。然后令ends[1]=3,因为遍历到目前为止,在所有长度为2的递增序列中,最小的结尾数是3,而不再是5。
- 遍历到arr[4]==6。ends有效区=ends[0..1]=[1,3],在有效区中用二分法找到最左边,大于或等于arr[4]的数。发现没有这样的数,表示以arr[4]结尾的最长递增序列长度=ends 有效区长度+1,所以令dp[4]=3。ends整个有效区都没有比arr[4]更大的数,说明发现了比 ends有效区长度更长的递增序列,于是把有效区扩大,ends有效区=ends[0..2]=[1,3,6]。
- 遍历到arr[5]==4。ends有效区=ends[0..2]=[1,3,6],在有效区中用二分法找到最左边大于或等于arr[5]的数。发现是ends[2],表示以arr[5]结尾的最长递增序列长度为3,所以令dp[5]=3。然后令ends[2]=4,表示在所有长度为3的递增序列中,最小的结尾数变为4。
- 遍历到arr[6]==8。ends有效区=ends[0..2]=[1,3,4],在有效区中用二分法找到最左边大于或等于arr[6]的数。发现没有这样的数,表示以arr[6]结尾的最长递增序列长度=ends有效区长度+1,所以令dp[6]=4。ends整个有效区都没有比arr[6]更大的数,说明发现了比 ends有效区长度更长的递增序列,于是把有效区扩大,ends有效区=ends[0..3]=[1,3,4,8]。
- 遍历到arr[7]==9。ends有效区=ends[0..3]=[1,3,4,8],在有效区中用二分法找到最左边大于或等于arr[7]的数。发现没有这样的数,表示以arr[7]结尾的最长递增序列长度=ends 有效区长度+1,所以令dp[7]=5。ends整个有效区都没有比arr[7]更大的数,于是把有效区 扩大,ends 有效区=ends[0..5]=[1,3,4,8,9]。
- 遍历到arr[8]==7。ends有效区=ends[0..5]=[1,3,4,8,9],在有效区中用二分法找到最左边大于或等于arr[8]的数。发现是ends[3],表示以arr[8]结尾的最长递增序列长度为4, 所以令dp[8]=4。然后令ends[3]=7,表示在所有长度为4的递增序列中,最小的结尾数变为7。
具体过程请参看如下代码:
#include<stdio.h>
#include<stdlib.h> /* 动态内存分配 */
#define MAXN 100000
int arr[MAXN + 10];
int dp[MAXN + 10];
int ends[MAXN + 10];
int max(int x, int y) {
return x > y ? x : y;
}
int main() {
int N, i;
scanf("%d", &N);
for (i = 0; i < N; ++i) {
scanf("%d", &arr[i]);
}
dp[0] = 1;
ends[0] = arr[0];
int right = 0;
int ll = 0;
int rr = 0;
int mm = 0;
for (i = 1; i < N; ++i) {
ll = 0;
rr = right;
while (ll <= rr) {
mm = (ll + rr) / 2;
if (arr[i] > ends[mm]) {
ll = mm + 1;
} else {
rr = mm - 1;
}
}
right = max(right, ll);
ends[ll] = arr[i];
dp[i] = ll + 1;
}
int ans = -1;
for (i = 0; i < N; ++i) {
if (ans < dp[i])
ans = dp[i];
}
printf("%d\n", ans); /* 输出最长递增子序列的长度 */
/*
* 下面根据dp数组还原出最长递增子序列。
* len中记录了最长递增子序列的长度,当然有len=ans。
* index记录最长递增子序列中最后一个数在arr数组中的位置。
*/
int len = 0;
int index = 0;
for (i = 0; i < N; ++i) {
if (dp[i] > len) {
len = dp[i];
index = i;
}
}
/*
* lis数组用来存放最长递增子序列。
*/
int* lis = (int*) malloc(sizeof(int) * len);
lis[--len] = arr[index]; /* 最长递增子序列中最后一个数为arr[index] */
for (i = index; i >= 0; i--) { /* 从index位置开始从右往左扫描数组arr */
if (arr[i] < arr[index] && dp[i] == dp[index] - 1) {
lis[--len] = arr[i];
index = i;
}
}
/* 打印最长递增子序列 */
for (i = 0; i < ans; ++i) {
printf("%d", lis[i]);
if (i < ans - 1)
printf(" ");
}
printf("\n");
free(lis);
return 0;
}
运行结果:
4、文章推荐
推荐一:《用x种方式求第n项斐波那契数,99%的人只会第一种》,文章内容:斐波那契数列及其求法,动态规划,数组的巧妙使用--滚动数组。
推荐二:《深入浅出理解动态规划(二) | 最优子结构》,文章内容:经典例题---数字三角形求解。
推荐三:《深入浅出理解动态规划(一) | 交叠子问题》,文章内容:记忆化搜索算法、打表法求解第n个斐波那契数。
求解最长递增子序列(LIS) | 动态规划(DP)+ 二分法的更多相关文章
- 动态规划(DP),最长递增子序列(LIS)
题目链接:http://poj.org/problem?id=2533 解题报告: 状态转移方程: dp[i]表示以a[i]为结尾的LIS长度 状态转移方程: dp[0]=1; dp[i]=max(d ...
- 算法之动态规划(最长递增子序列——LIS)
最长递增子序列是动态规划中最经典的问题之一,我们从讨论这个问题开始,循序渐进的了解动态规划的相关知识要点. 在一个已知的序列 {a1, a 2,...an}中,取出若干数组成新的序列{ai1, ai ...
- 动态规划 - 最长递增子序列(LIS)
最长递增子序列是动态规划中经典的问题,详细如下: 在一个已知的序列{a1,a2,...,an}中,取出若干数组组成新的序列{ai1,ai2,...,aim},其中下标i1,i2,...,im保持递增, ...
- 最长回文子序列LCS,最长递增子序列LIS及相互联系
最长公共子序列LCS Lintcode 77. 最长公共子序列 LCS问题是求两个字符串的最长公共子序列 \[ dp[i][j] = \left\{\begin{matrix} & max(d ...
- 51 Nod 1134 最长递增子序列 (动态规划基础)
原题链接:1134 最长递增子序列 题目分析:长度为 的数列 有多达 个子序列,但我们应用动态规划法仍可以很高效地求出最长递增子序列().这里介绍两种方法. 先考虑用下列变量设计动态规划的算法. ...
- 2.16 最长递增子序列 LIS
[本文链接] http://www.cnblogs.com/hellogiser/p/dp-of-LIS.html [分析] 思路一:设序列为A,对序列进行排序后得到B,那么A的最长递增子序列LIS就 ...
- 一个数组求其最长递增子序列(LIS)
一个数组求其最长递增子序列(LIS) 例如数组{3, 1, 4, 2, 3, 9, 4, 6}的LIS是{1, 2, 3, 4, 6},长度为5,假设数组长度为N,求数组的LIS的长度, 需要一个额外 ...
- Python动态规划求解最长递增子序列(LIS)
原始代码错误,移步博客查看O(N^2)及优化的O(N*logN)的实现:每天一道编程题--最长递增子序列
- 51Nod - 1134 最长递增子序列【动态规划】
给出长度为N的数组,找出这个数组的最长递增子序列.(递增子序列是指,子序列的元素是递增的) 例如:5 1 6 8 2 4 5 10,最长递增子序列是1 2 4 5 10. Input 第1行:1个数N ...
随机推荐
- (六)pandas 日常使用技巧
pandas数据处理 1.删除重复元素 import numpy as np import pandas as pd from pandas import Series,DataFrame df = ...
- 数据可视化之powerBI技巧(二)Power BI性能分析器,原来还有这个功能
性能分析器是Power BI前几个月推出的功能,主要用于检测视觉对象和DAX的性能如何,该功能的使用比较简单,首先勾选这个功能, 然后点击开始记录并刷新视觉对象,就可以看出每个元素耗费的时间,如下图 ...
- Video 自动播放
先说ios ios之前的政策是视频只能在用户主动操作后才能播放,且播放时必须全屏. 随着 iOS 10 的正式发布,Safari 也迎来了大量更新,首先划出重点:1)iOS 10 Safari 支持特 ...
- 使用Red5-Pro Android官方Demo拆解分析(一)
一.配置文件 1.导入库文件jniLibs到main文件夹下 2.导入red5streaming.jar 3.在build里到入其他的包,代码如下: dependencies { implementa ...
- 利用tox打造自动自动化测试框架
什么是tox tox官方文档的第一句话 standardize testing in Python,意思就是说标准化python中的测试,那是不是很适合测试人员来使用呢,我们来看看他究竟是什么? 根据 ...
- 当小程序遇见物联网IoT,几行代码搞定智能插座控制
在 5G 热潮的推动下,与其紧密结合的物联网(IoT)正日益成为个人和企业工作生活中的重要组成部分,它为企业和个人带来了操作流程的改进和更好的生活体验,随着人工智能(AI)技术的日趋成熟,IoT 与 ...
- vue使用elementUI form表单label样式修改
更多关于修改ElementUI样式的方法,可以参考这篇文章 1.删除style标签中的 scoped 属性 <style lang="lang" scoped> ... ...
- package.json中dependencies和devDependencies区别
package.json中dependencies和devDependencies区别 dependencies: 应用能够正常运行依赖的包.用户发布环境,依赖的包不仅开发环境能够使用,生产环境也能使 ...
- 一张PDF了解JDK11 GC调优秘籍-附PDF下载
目录 简介 废弃的VM选项 Source-File Mode Code Heap状态分析 AppCDS 总结 简介 JDK11相比JDK10,添加了一个新的Source-File Mode,可以直接通 ...
- python如何编写win程序
python可以编写win程序.win程序的格式是exe,下面我们就来看一下使用python编写exe程序的方法. 编写好python程序后py2exe模块即可将其打包为exe程序. 实际操作过程: ...