给定数组arr,返回arr的最长递增子序列。
arr={2,1,5,3,6,4,8,9,7},返回的最长递增子序列为{1,3,4,8,9}。
本期主要从动态规划和二分法两个方向来求解最长递增子序列问题。
先介绍时间复杂度为O(N2)的方法,具体过程如下:
按照步骤1~3可以计算出dp数组,具体过程请参看如下代码中的方法,参考代码如下:
#include
#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;
}
最长上升子序列有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数组包含每一步决策的信息,其实根据dp数组找出最长递增子序列的过程就是从某一个位置开始逆序还原出决策路径的过程。具体过程请参看如下代码:
#include
#include /* 动态内存分配 */
#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(N2),根据dp数组得到最长递增子序列过程的时间复杂度为O(N),所以整个过程的时间复杂度为O(N2)。
问题:如果把序列的长度增加到N=104,105,106 呢?如何将计算dp数组的时间复杂度降到O(Nlog N)?
时间复杂度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。之后的遍历继续用这个例子来说明求解过程。
具体过程请参看如下代码:
#include
#include /* 动态内存分配 */
#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;
}
推荐一:《用x种方式求第n项斐波那契数,99%的人只会第一种》,文章内容:斐波那契数列及其求法,动态规划,数组的巧妙使用–滚动数组。
推荐二:《深入浅出理解动态规划(二) | 最优子结构》,文章内容:经典例题—数字三角形求解。
推荐三:《深入浅出理解动态规划(一) | 交叠子问题》,文章内容:记忆化搜索算法、打表法求解第n个斐波那契数。