求解最长递增子序列(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 ...
随机推荐
- 机器学习实战基础(十一):sklearn中的数据预处理和特征工程(四) 数据预处理 Preprocessing & Impute 之 处理分类特征:编码与哑变量
处理分类特征:编码与哑变量 在机器学习中,大多数算法,譬如逻辑回归,支持向量机SVM,k近邻算法等都只能够处理数值型数据,不能处理文字,在sklearn当中,除了专用来处理文字的算法,其他算法在fit的 ...
- redis(七):Redis 字符串(String)(python)
# -*- coding: utf-8 -*- import redis #这个redis不能用,请根据自己的需要修改 r =redis.Redis(host="123.516.74.190 ...
- Django之ORM查询操作详解
浏览目录 一般操作 ForeignKey操作 ManyToManyField 聚合查询 分组查询 F查询和Q查询 事务 Django终端打印SQL语句 在Python脚本中调用Django环境 其他操 ...
- shaderlab - 9chapter-阴影
原理 相机和光源重合,相机能看到的表面,形成的深度图--叫做,阴影纹理(shadowMap). 传统的,用base和add更新深度信息,但是,此处需求是深度信息而已,掺杂了光的计算了,所以,unity ...
- TIBCO Jasper Report 中显示图片的方式
最近在做的项目中,需要输出很多报表类文档,于是选择用jasper来帮助完成. 使用jasper studio的版本是 :TIB_js-studiocomm_6.12.2_windows_x86_64. ...
- 将终结点图添加到你的ASP.NET Core应用程序中
在本文中,我将展示如何使用DfaGraphWriter服务在ASP.NET Core 3.0应用程序中可视化你的终结点路由.上面文章我向您演示了如何生成一个有向图(如我上篇文章中所示),可以使用Gra ...
- 利用tox打造自动自动化测试框架
什么是tox tox官方文档的第一句话 standardize testing in Python,意思就是说标准化python中的测试,那是不是很适合测试人员来使用呢,我们来看看他究竟是什么? 根据 ...
- 文件传输协议---TFTP
简介 TFTP协议全称为简单文件传输协议,是以UDP为基础的应用层协议,主要用于不同设备之间的文件传输.具有协议简单,易于实现的特点,常用于嵌入式设备开发中. 传输模式 数据的存储有不同的格式,磁盘中 ...
- 使用jwt进行token认证
简单说明:最近在搞权限这一块的东西,需要用到jwt进行token认证,才有了如下的demo演示 具体细节可以看gitbug,噗,不是bug是hub github地址:https://github ...
- chrome浏览器hover时文字抖动bug
今天发现一个奇怪的bug,chrome浏览器里面 当父标签定位为fixed时,hover里面子标签时,文本会发生抖动,百思不得其解,经过多方查证,发现解决办法 -webkit-transform: ...