动态规划-最长上升子序列模型

1. 题目描述

给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式

第一行包含整数N。

第二行包含N个整数,表示完整序列。

输出格式

输出一个整数,表示最大长度。

数据范围

\(1≤N≤1000\)
\(−10^9≤数列中的数≤10^9\)

输入样例:

7
3 1 2 1 8 5 6

输出样例:

4

2. (DP)朴素解法\(O(n^2)\)

本题是一个简单的DP问题。

\(a[i]\)表示数组中第\(i\)个数,\(f[i]\)表示以数组中第\(i\)个数结尾的最长上升子序列的长度。

\(f[i] = max(f[j]) + 1,j < i 且a[j] < a[i]\)
代码如下:

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;

int f[N], a[N];
int n;

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
    int res = 0;
    for(int i = 1; i <= n; i ++)
    {
        // 注意初始化f[i] = 1.
        f[i] = 1;   
        for(int j = 1; j < i ; j ++)
            if(a[j] < a[i])
                f[i] = max(f[i],f[j] + 1);
        res = max(res, f[i]);
    }
    cout << res << endl;
	return 0;
}

3. (DP+贪心)优化版本\(O(nlgn)\)

通过对本题的观察与思考。我们可以得到如下的事实:

  • 对于两个长度相同的子序列,假设两个子序列的最后一个值分别是\(x_1, x_2\),且\(x_1 > x_2\)。假设我们的\(a[i]\)可以放在最后一个值为\(x_1\)的序列的后面,则其一定能够放在最后一个值为\(x_2\)的序列的后面。故,对于长度相同的序列,我们只需要存储最后一个值小的序列即可。

当我们扫描到第\(i\)个数的时候,可以将其前面的数构成的子序列按长度进行分类,长度为\(1\)的最长上升子序列只存储结尾值最小的,长度为\(2\)的最长上升子序列只存储结尾值最小的,那么我们可以证明:不同长度最长上升子序列最后一个数的值是随长度严格单调递增的

证明:

假设长度为\(5\)的最长上升子序列最后一个数为\(a\),长度为\(6\)的最长上升子序列最后一个数为\(b\),倒数第二个数为\(c\)

反证法:如果\(a>=b\),由于\(b > c\),则\(a>c\)。又由于对于长度相同的子序列我们只存储最后一个数值较小值。故\(a< c\)。推出矛盾。

这样的话,当我们扫描到第\(i\)个数的时候,就可以通过二分法,找到小于\(a[i]\),且最后一个数最大的子序列,将其该数添加到子序列后面。

代码如下:

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;

int n;
int a[N];
int q[N];

int main()
{
    cin >> n;
    for(int i = 0; i < n; i ++)cin >> a[i];

    int len = 0;
    q[0] = -2e9;
    for(int i = 0; i < n; i ++)
    {
        int l = 0, r =len ;
        while(l < r)  // 二分法找出小于等于a[i]的最大的子序列。
        {
            int mid = l + r + 1 >> 1;
            if(q[mid] < a[i]) l = mid;
            else r = mid - 1;
        }
        len = max(len, r + 1);  // 更新长度
        q[r + 1] = a[i];        // 更新末尾值

    }
    cout << len << endl;
    return 0;
}


注:代码中含有部分细节没有详细说明,供读者思考