编程之美-2.16-求数组中最长递增子序列
1. 简述
写一个时间复杂度尽可能低的程序,求一个一维数组中最长递增子序列的长度。
例如在序列1,-1,2,-3,4,-5,6,-7中,其最长的递增子序列的长度为4(如1,2,4,6)。
2. 思路
这个题目与前面求一维数组中子数组之和最大值有点像,不过区别还是很明显,比如:子数组是数组中一串连续相邻的数字,而子序列不一定是相邻的,因此要得到[0-k]的子数组最大和,只要分析[0-(k-1)]的子数组最大和即可,而考虑[0-k]的子序列的最长长度,就不能只分析[0-(k-1)]中子序列的最长长度。另外一个区别就是,子数组的和可以扩展到二维数组的情况,但是递增子序列就不好扩展了,最多也就扩展成杨氏三角吧。
方法一:我们计算每个可能的子序列,判断其是否为递增的,然后选取其中最大的一个。可能的子序列有2^N个,因此复杂度是O(2^N)。
方法二:由于我们要判断一个元素能否与已有的一个子序列构成一个更长的子序列,只有比较这个元素与这个子序列的最后一个元素即可,那么实际上对于[0-(i-1)]内的2^i个子序列,实际上我们只需要记录以包含每个元素且以该元素结尾的最长子序列长度即可。即定义MaxLen[i],表示A[0]-A[i]范围内,且包含A[i]的最长子序列长度。这样2^i个子序列用i个子序列就能够代表了。
递推公式为:MaxLen[0]=1,MaxLen[i]=max{ A[i]>=A[k]?(MaxLen[k]+1):1 },k=0,1,2,...,i-1。
最后max{MaxLen[i]},i=0,1,2,...,N-1,即为所求。
这种方法将2^N中子序列用N个子序列来代表(根据子序列构成的方法,只需要判断子序列的最后一个元素),复杂度为O(N^2)。
方法三:方法二实际上使用通过尾部元素的方法,从关注2^N个子序列,到关注N个分别以A[i]结尾的最长子序列上。这里换一种映射的方法,我们关注长度分别为i,且尾部元素最小的子序列上。定义:LenMinValue[i],表示长度为i的若干子序列中,尾部的最小元素值。MaxLen是当前找到的子序列最大长度。
递推公式为:MaxLen=1,LenMinValue[0] = INT_MIN(哨兵),LenMinValue[1]=A[0]。对于A[i],我们在LenMinValue[0]-LenMinValue[MaxLen]这个范围内,找到A[i]的位置。如果A[i]>LenMinValue[MaxLen],这说明要更新最大长度了,MaxLen++,LenMinValue[MaxLen]=A[i];如果LenMinValue[j]<A[i]<LenMinValue[j+1],0<=j<=MaxLen-1(必然大于一个数,因为LenMinValue[0]是哨兵),那么这就说明Right长度子序列有更小的尾部数值了,即A[i],LenMinValue[Right]=A[i]。
这里先说明一下,假设前面已经计算了5个元素了。那么前面最长的子序列的长度(即MaxLen)可能只有1,即全部逆序的时候,那么此时MaxLen=1,这样就压缩很很多。另外值得注意的是,假如MaxLen=3,那么必然有LenMinValue[0]<LenMinValue[1]<LenMinValue[2]<LenMinvalue[3],这是因为,长度为2的子序列就是在长度为1的子序列后增加了一个元素产生的,这个元素的值至少是LenMinValue[2](至少是因为可能有多个子序列长度为2,LenMinValue[2]是其中的最小值)。即LenMinValue[0]-LenMinValue[MaxLen]是有序的,因此在搜索A[i]的位置的时候,可以使用二分的方法(数组中数字如果允许有重复就要注意了,可能有变化)。
总体来说最坏情况是O(N*LogN),实际上还要小一点,因为从A[2]开始,每次二分最多也就是说Log2+Log3+Log4+...+Log(N-1),当然实际上还要小,因为如果长度一直递增,那么说明数组是升序的,那么每次二分就很快用不了LogK,如果数组不是升序的,那么长度不会一直递增,因此Log中的因子不会一直在增加,所有不管怎样复杂度都要被N*LogN更小。
3. 代码
方法二和方法三的代码,其中为了简单实现,方法三没有加二分搜索。
#include <iostream>
using namespace std;
int find_max_len_method2(const int *A, int N) {
int* max_len = new int[N]; // max_len[i]表示以A[i]结尾的那些子序列中的最长长度
max_len[0] = 1;
for(int i=1; i<N; i++) { // 依次考虑以A[1],...,A[N-1]结尾的子序列
max_len[i] = 1;
for(int j=0; j<i; j++) {
if(A[i] > A[j]) {
max_len[i] = max_len[j]+1>max_len[i] ? (max_len[j]+1):max_len[i];
}
}
}
cout << "max_len" << endl;
for(int i=0; i<N; i++)
cout << max_len[i] << " ";
cout << endl;
int result = 0;
for(int i=0; i<N; i++)
result = max_len[i]>result ? max_len[i]:result;
delete[] max_len;
return result;
}
int find_max_len_method3(const int *A, int N) {
int* len_min_value = new int[N+1];
int max_len;
len_min_value[0] = INT_MIN;
len_min_value[1] = A[0];
max_len = 1;
for(int i=1; i<N; i++) { // 依次引入A[1],...,A[N-1]
int pos = max_len;
if(A[i] > len_min_value[max_len]) {
max_len++;
len_min_value[max_len] = A[i];
}
else {
int j = max_len-1;
while(A[i] < len_min_value[j])
j--;
len_min_value[j+1] = A[i];
}
}
cout << "len_min_value" << endl;
for(int i=0; i<N+1; i++)
cout << "d: " << len_min_value[i] << " ";
cout << endl;
delete []len_min_value;
return max_len;
}
int main() {
int a[8] = {1, -1, 2, -3, 4, -5, 6, -7}; // 1,2,4,6
cout << "数组:" << endl;
for(int i=0; i<8; i++)
cout << a[i] << " ";
cout << endl;
cout << "数组的最长递增子序列长度为:" << find_max_len_method2(a, 8) << endl;
cout << "数组的最长递增子序列长度为:" << find_max_len_method3(a, 8) << endl;
system("PAUSE");
return 0;
}
结果输出为:
4. 参考
编程之美,2.16节,求数组中最长递增子序列