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^)的方法,具体过程如下:

  1. 生成数组dp,dp[i]表示在以arr[i]这个数结尾的情况下,arr[0…i]中的最大递增子序列长度。
  2. 对第一个数arr[0]来说,令dp[0]=1,接下来从左到右(i=1,2,…,N-1)依次算出以每个位置的数结尾的情况下,最长递增子序列长度。
  3. 假设计算到位置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}。具体求解步骤如下:

  1. 遍历dp数组,找到最大值以及位置。在本例中最大值为5,位置为7,说明最终的最长递增子序列的长度为5,并且应该以arr[7]这个数(arr[7]=9)结尾。
  2. 从arr数组的位置7开始从右向左遍历。如果对某一个位置i,既有arr[i]<arr[7],又有dp[i]=dp[7]-1,说明arr[i]可以作为最长递增子序列的倒数第二个数。在本例中,arr[6]<arr[7],并且dp[6]=dp[7]-1,所以8应该作为最长递增子序列的倒数第二个数。
  3. 从arr数组的位置6开始继续向左遍历,按照同样的过程找到倒数第三个数。在本例中,位置5满足arr[5]<arr[6],并且dp[5]=dp[6]-1,同时位置4也满足。选arr[5]或者arr[4]作为倒数第三个数都可以。
  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。之后的遍历继续用这个例子来说明求解过程。

  1. 遍历到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。
  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]。
  3. 遍历到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。
  4. 遍历到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]。
  5. 遍历到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。
  6. 遍历到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]。
  7. 遍历到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]。
  8. 遍历到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)+ 二分法的更多相关文章

  1. 动态规划(DP),最长递增子序列(LIS)

    题目链接:http://poj.org/problem?id=2533 解题报告: 状态转移方程: dp[i]表示以a[i]为结尾的LIS长度 状态转移方程: dp[0]=1; dp[i]=max(d ...

  2. 算法之动态规划(最长递增子序列——LIS)

    最长递增子序列是动态规划中最经典的问题之一,我们从讨论这个问题开始,循序渐进的了解动态规划的相关知识要点. 在一个已知的序列 {a1, a 2,...an}中,取出若干数组成新的序列{ai1, ai ...

  3. 动态规划 - 最长递增子序列(LIS)

    最长递增子序列是动态规划中经典的问题,详细如下: 在一个已知的序列{a1,a2,...,an}中,取出若干数组组成新的序列{ai1,ai2,...,aim},其中下标i1,i2,...,im保持递增, ...

  4. 最长回文子序列LCS,最长递增子序列LIS及相互联系

    最长公共子序列LCS Lintcode 77. 最长公共子序列 LCS问题是求两个字符串的最长公共子序列 \[ dp[i][j] = \left\{\begin{matrix} & max(d ...

  5. 51 Nod 1134 最长递增子序列 (动态规划基础)

    原题链接:1134 最长递增子序列 题目分析:长度为  的数列  有多达  个子序列,但我们应用动态规划法仍可以很高效地求出最长递增子序列().这里介绍两种方法. 先考虑用下列变量设计动态规划的算法. ...

  6. 2.16 最长递增子序列 LIS

    [本文链接] http://www.cnblogs.com/hellogiser/p/dp-of-LIS.html [分析] 思路一:设序列为A,对序列进行排序后得到B,那么A的最长递增子序列LIS就 ...

  7. 一个数组求其最长递增子序列(LIS)

    一个数组求其最长递增子序列(LIS) 例如数组{3, 1, 4, 2, 3, 9, 4, 6}的LIS是{1, 2, 3, 4, 6},长度为5,假设数组长度为N,求数组的LIS的长度, 需要一个额外 ...

  8. Python动态规划求解最长递增子序列(LIS)

    原始代码错误,移步博客查看O(N^2)及优化的O(N*logN)的实现:每天一道编程题--最长递增子序列

  9. 51Nod - 1134 最长递增子序列【动态规划】

    给出长度为N的数组,找出这个数组的最长递增子序列.(递增子序列是指,子序列的元素是递增的) 例如:5 1 6 8 2 4 5 10,最长递增子序列是1 2 4 5 10. Input 第1行:1个数N ...

随机推荐

  1. alert(1) to win Part Ⅰ

    alert(1) to win Adobe: function escape(s) { s = s.replace(/"/g, '\\"'); return '<script ...

  2. java 数据结构(十):Collection子接口:Set接口

    1. 存储的数据特点:无序的.不可重复的元素具体的: 以HashSet为例说明:1. 无序性:不等于随机性.存储的数据在底层数组中并非照数组索引的顺序添加,而是根据数据的哈希值决定的.2. 不可重复性 ...

  3. 03 flask源码剖析之threading.local和高级

    03 threading.local和高级 目录 03 threading.local和高级 1.python之threading.local 2. 线程唯一标识 3. 自定义threading.lo ...

  4. 机器学习实战基础(八):sklearn中的数据预处理和特征工程(一)简介

    1 简介 数据挖掘的五大流程: 1. 获取数据 2. 数据预处理 数据预处理是从数据中检测,纠正或删除损坏,不准确或不适用于模型的记录的过程 可能面对的问题有:数据类型不同,比如有的是文字,有的是数字 ...

  5. ValueError: X needs to contain only non-negative integers.

    for feature in short_cate_feature: enc.fit(data[feature].values.reshape(-1, 1)) base_train_csr = spa ...

  6. Viper解析&加载配置

    Viper解析&加载配置 1    Viper是什么 Viper是一个方便Go语言应用程序处理配置信息的库.它可以处理多种格式的配置.它支持的特性: 设置默认值 从JSON.TOML.YAML ...

  7. [apue] Linux / Windows 系统上只能建立不超过 PATH_MAX / MAX_PATH 长度的路径吗?

    问题的提出 在处理文件系统路径的时候,我们一般会先开辟一块内存区,用来接收路径.或者拼接好路径传递给系统调用.这是因为路径在各个系统上都有最大长度限制,在 Windows 上这个值是 MAX_PATH ...

  8. OSCP Learning Notes - File Transfers(1)

    File transfer type: 1. HTTP Transfer files through the website. 2.wget wget http://10.0.0.109/exploi ...

  9. Python Ethical Hacking - WEB PENETRATION TESTING(1)

    WHAT IS A WEBSITE Computer with OS and some servers. Apache, MySQL ...etc. Cotains web application. ...

  10. 硬核干货:5W字17张高清图理解同步器框架AbstractQueuedSynchronizer

    前提 并发编程大师Doug Lea在编写JUC(java.util.concurrent)包的时候引入了java.util.concurrent.locks.AbstractQueuedSynchro ...