给定数组arr,返回arr的最长递增子序列。算法
arr={2,1,5,3,6,4,8,9,7},返回的最长递增子序列为{1,3,4,8,9}。数组
本期主要从动态规划和二分法两个方向来求解最长递增子序列问题。ide
先介绍时间复杂度为O(N^2^)的方法,具体过程以下:学习
按照步骤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。3d
问题:若是还要输出最长的子序列呢?例如,除了输出5以外,还要输出(1,3,4,8,9)这个序列。code
接下来解释如何根据求出的dp数组获得最长递增子序列。以题目的例子来讲明,arr={2,1,5,3,6,4,8,9,7},求出的数组dp={1,1,2,2,3,3,4,5,4}。具体求解步骤以下:blog
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)?
时间复杂度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<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; }
运行结果:
推荐一:《用x种方式求第n项斐波那契数,99%的人只会第一种》,文章内容:斐波那契数列及其求法,动态规划,数组的巧妙使用--滚动数组。
推荐二:《深刻浅出理解动态规划(二) | 最优子结构》,文章内容:经典例题---数字三角形求解。
推荐三:《深刻浅出理解动态规划(一) | 交叠子问题》,文章内容:记忆化搜索算法、打表法求解第n个斐波那契数。
做者: C you again,从事软件开发 努力在IT搬砖路上的技术小白
公众号: 【C you again】,分享计算机类毕业设计源码、IT技术文章、游戏源码、网页模板、程序人生等等。公众号回复 【粉丝】进博主技术群,与大佬交流,领取干货学习资料
关于转载:欢迎转载博主文章,转载时代表出处
求赞环节:创做不易,记得 点赞+评论+转发 谢谢你一路支持