【题目描述】

给定N个数,求这N个数的最长上升子序列的长度。

【样例输入】

7

2 5 3 4 1 7 6

【样例输出】

4

所谓最长上升子序列,就是给定一列数,求序列中严格上升(后一个数 > 前一个数)的子序列,这个子序列中数的位置不一定连续。

这是动态规划中的一个经典应用题目,动态规划的思想就是把问题分解为一些本质上还是相同问题的小问题,这些小问题的区别仅在于输入的参数不同,而每一个小问题的解都可以由比他参数更小的一些小问题的解推出,最小的小问题有显而易见的解,这被称之为边界。最长上升子序列(LIS)问题也是这样。

比如我们用a[]这个数组存数列,a[i]表示数列的第i个数字;定义一个dp[]数组,dp[i]表示以a[i]结尾的LIS的长度。

dp[i]一定是把a[i]加到以a[i]之前的数(a[1] ~ a[i-1])中某一个数结尾的LIS后面的,那么dp[i]一定等于dp[1] ~ dp[i-1]中某一个数+1。假设这个数为dp[j],按照LIS的定义,其对应的a[j]一定小于a[i]。所以此时就很明确了:令1<=j<=i-1,且a[j]<a[i],在这样的j存在的范围内,找一个最大的dp[j],然后令dp[i] = dp[j] + 1即求出了dp[i]。

然后在所有的dp[i]中取一个最大的即是整个数列的最长上升子序列的长度。

按照这个思想写出来的代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <queue>
using namespace std;
const int INF = 0x3f3f3f3f;
const int MAXN = 1e9;
typedef long long ll;

int a[30], dp[30];

int main()
{
    int n;
    scanf("%d", &n);
    for(int i = 0; i < n; i++)
        scanf("%d", &a[i]);
    int LIS = 1;
    for(int i = 0; i < n; i++)
    {
        dp[i] = 1;
        for(int j = 0; j < i; j++)
            if(a[j]<a[i])
                dp[i] = max(dp[i], dp[j]+1);
        LIS = max(dp[i], LIS);
    }
    printf("%d\n", LIS);
    return 0;
}


比较明显,这个算法的时间复杂度为O(n^2),当范围大一些的时候是有些低效的。

这个时候我们不妨换一种思路:
我们把LIS定义为当前最长上升子序列的长度,dp[LIS]定义为当前LIS对应的最小末尾
例如:a[] = {1 5 3 4 7},那么dp[1] = 1,dp[2] = 3,dp[3] = 4, dp[4] = 7

当i = 1时,a[i] = 1,此时i扫过的数组长度只有1,自然LIS = a[i] = 1,所以dp[1] = a[1] = 1。
然后当i逐渐增加时,每新扫过一个a[i],因为dp[LIS]代表长度为i的最长上升子序列的最小末尾,这可以代表一个长度为LIS的最长上升子序列,我们考虑可以把这个新扫过的a[i]加到哪个最长上升子序列后面构成一个长度+1的新的最长上升子序列。那么我们就可以在dp数组中去寻找那个小于a[i]的最大的那个数,我们把a[i]加到这个数代表的序列后面:设这个数为dp[j],则dp[j+1] = a[i]。

这样观察这个dp数组,他的dp[i+1]一定大于dp[i],也就是说这是一个有序的数组,于是我们在进行找数操作的时候就可以用二分搜索来进行。而当我们把a数组全部扫描完之后,dp数组中的最后一个有效的数字的位置即是我们所求的LIS。

代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <queue>
using namespace std;
const int INF = 0x3f3f3f3f;
const int MAXN = 1e9;
typedef long long ll;

int a[30], dp[30];

int main()
{
    memset(dp, 0, sizeof(dp));
    int n;
    scanf("%d", &n);
    for(int i = 0; i < n; i++)
        scanf("%d", &a[i]);
    int LIS = 1;
    dp[1] = a[0];
    for(int i = 1; i < n; i++)
        if(a[i] > dp[LIS]) dp[++LIS] = a[i];
        else dp[lower_bound(dp+1, dp+1+LIS, a[i])-dp] = a[i];
    printf("%d\n", LIS);
    return 0;
}


由于循环中的搜索变成了二分搜索,时间复杂度为O(logn),所以整体时间复杂度是O(nlogn)。

第二个算法其实已经不算dp了吧。。。观察算法整体过程,这里的dp数组其实模拟了一个栈,栈顶元素是当前最长上升子序列的最小末尾,属于网上说的单调栈优化。

在第二个算法中我们可以通过在二分查找中改变“上确界”和“下确界”,以及在第一个算法中通过改变符号(“<”和“<=”或“>”、“>=”等),求出最长不下降、不上升、严格下降子序列等问题。