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; } };
浙公网安备 33010602011771号