300. 最长递增子序列以及个数(特别经典的dp + 贪心)

300. 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:

输入:nums = [0,1,0,3,2,3]
输出:4

 

这题应该是动态规划中最经典的题。同时也是面试中最经常出的题,主要有以下两种方法。

1.动态规划(O(N²))

1. 定义状态:

dp[i] 表示:以 nums[i] 结尾 的「上升子序列」的长度。

2. 状态转移方程:

只要 nums[i] 严格大于在它位置之前的某个数,那么 nums[i] 就可以接在这个数后面形成一个更长的上升子序列。

      dp[i] = max(dp[i], dp[j] + 1);  ( 0 < j < i )

3. 初始化:

个人觉得初始化这部分应该是最要纠结的边界问题了,这里有dp[i] = 1

4.输出:

因为是要最长的,而不是求包括末尾的最长子序列,所以用一个maxnum保存每次的dp[i]即可。

5.代码

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        //O(n²)
        int length = nums.size();
        int maxnum = 1;
        vector<int> dp(length, 1);
        for(int i = 0; i < length; i++){
            for(int j = 0; j < i; j++){
                if(nums[i] > nums[j]){
                    dp[i] = max(dp[i], dp[j] + 1);
                    maxnum = max(maxnum,dp[i]);
                }
            }
        }
        return maxnum; 
    }
};

 

  

2.贪心 + 二分(O(NlogN))

动态规划也是暴力的解法,只不过他是把暴力之后的数据存储起来,对于这题来说可以用贪心 + 二分的方式做,能得到更低的复杂度。

1.在遍历数组 nums 的过程中,看到一个新数 num,如果这个数 严格 大于有序数组 tail 的最后一个元素,就把 num 放在有序数组 tail 的后面,否则进入第 2 点;


注意:这里的大于是「严格大于」,不包括等于的情况。

2.在有序数组 tail 中查找第 1 个等于大于 num 的那个数,试图让它变小;
如果有序数组 tail 中存在 等于 num 的元素,什么都不做,因为以 num 结尾的最短的「上升子序列」已经存在;
如果有序数组 tail 中存在 大于 num 的元素,找到第 1 个,让它变小,这样我们就找到了一个 结尾更小的相同长度的上升子序列。

在有序数组 tail 中查找第 1 个等于大于 num 的那个数,试图让它变小;

这里是引用liweiwei1419的话,他在原文中定义了一个新的状态,但是我其实觉得这个跟动态规划没什么关系。

举个例子:

10 9  2 5 3 7 这串数字要找最长上升序列

首先读入10 然后10 9 因为9比10小所以进入(2)替换10,所以数组tail变成9

然后读入2,2小于9,数组变成2,然后是

2 5

2 5 3(3小于5,进入(2)阶段然后代替第一个比3大的数5) -> 2 3

然后是 2 3 7

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        // O(nlogn)
        int length = nums.size();
        vector<int> tail;
        tail.push_back(nums[0]);
        for(int i = 1; i < length; i++){
             if(nums[i] > tail.back()){
                 tail.push_back(nums[i]);
             }
             else{
                 // 大于等于
                 // tail 中第一个大于nums[i]的元素位置,考虑到有相等元素,不能用 upperboud !
                 vector<int>::iterator iter = lower_bound(tail.begin(),tail.end(),nums[i]);
                 *iter = nums[i];
             }
        }
        return tail.size();
    }
};

  

这种方法出来的上升子序列的数量一定是对的,但是他数组里面的数字不一定是对的。

 

673. 最长递增子序列的个数

给定一个未排序的整数数组,找到最长递增子序列的个数。

示例 1:

输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。
示例 2:

输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。

本题在300号问题的基础上做了一些改变,需要多使用一个数组来记录LIS的组合数。

其中,dp[i]表示以i结尾的最长子序列长度

num[i]表示为i结尾的最长子序列个数

通过300题(最长递增子序列)的O(N²)遍历的方法,在遍历的时候判断

1:如果dp[i] < dp[j] + 1 说明遍历到j时d[i]并不是最大的,且个数需要用num[j]来替换

  dp[i] = dp[j] + 1; num[i] = num[j];

2:如果dp[i] = dp[j] + 1 说明当前d[i]的长度和现在d[j] + 1的长度是一样的,那么说明num[i]此时可以

由之前的num[i]和num[j]来组成

  dp[i] = dp[j] + 1; num[i] += num[j];

class Solution {
public:
    int findNumberOfLIS(vector<int>& nums) {
        int n = nums.size();
        vector<int> dp(n,1);
        vector<int> num(n,1);
        int sum = 0;
        int maxnum = 1;
        for(int i = 0; i < n; i++){
            for(int j = 0; j < i; j++){
                if(nums[i] > nums[j]){
                    if(dp[i] < dp[j] + 1){
                        dp[i] = dp[j] + 1;
                        num[i] = num[j];
                    }
                    else if(dp[i] == dp[j] + 1){
                        num[i] += num[j];
                    }
                    maxnum = max(maxnum,dp[i]);
                }
            }
        }
        for(int i = 0; i < n; i++){
            if(dp[i] == maxnum){
                sum += num[i];
            }
        }
        return sum;
    }
};

 

posted @ 2021-03-08 17:33  小码农2  阅读(1271)  评论(0)    收藏  举报