Loading

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

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;
}


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

4. 应用

4.1 leetcode 1713 得到子序列的最少操作次数 hard

题目描述
给你一个数组 target ,包含若干 互不相同 的整数,以及另一个整数数组 arrarr 可能 包含重复元素。

每一次操作中,你可以在 arr 的任意位置插入任一整数。比方说,如果 arr = [1,4,1,2] ,那么你可以在中间添加 3 得到 [1,4,3,1,2] 。你可以在数组最开始或最后面添加整数。

请你返回 最少 操作次数,使得target成为 arr 的一个子序列。

一个数组的 子序列 指的是删除原数组的某些元素(可能一个元素都不删除),同时不改变其余元素的相对顺序得到的数组。比方说,[2,7,4] [4,2,3,7,2,1,4] 的子序列(加粗元素),但 [2,4,2] 不是子序列。

数据范围
1 <= target.length, arr.length <= 105
1 <= target[i], arr[i] <= 109
target 不包含任何重复元素。

思路
我们可以先将target序列按照下标进行映射,并通过哈希表存储映射关系:哈希表中key为数组原值,value为该值在数组中的下标;

接着,我们扫描arr序列,在target中存在的序列,转换为其下标;不存在的,则删除;

最后对处理过的arr序列,求最长上升子序列即可。

class Solution {
public:
    int minOperations(vector<int>& target, vector<int>& arr) {
        unordered_map<int, int> hash;
        for(int i = 0; i < target.size(); i ++) hash[target[i]] = i;   //计算target数组中元素对应的下标

        vector<int> arrs;    //arrs中保存对应的下标子序列
        for(int i = 0; i < arr.size(); i ++)
        {
            if(hash.count(arr[i])) arrs.push_back(hash[arr[i]]);
            else continue;
        }

        //单调栈+二分计算最长上身子序列的值
        vector<int> q(arrs.size() + 1,-2e9);
        int len = 0;

        for(int i = 0; i < arrs.size(); i ++)
        {
            int l = 0, r = len;
            
            while(l < r)
            {
                int mid = l + r + 1 >> 1;
                if(q[mid] < arrs[i]) l = mid;
                else r = mid - 1;
            }
            len = max(len, r +  1);
            q[r + 1] = arrs[i];
        }
        return target.size() - len ;
       
    }
};
posted @ 2021-07-26 15:24  Kocoder  阅读(253)  评论(0)    收藏  举报