一、题目
300. 最长递增子序列
给定一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
法一:动态规划
- 模式识别1:看到最长字眼,首先考虑使用动态规划的思想解题
- 模式识别2:指纹最优解,不问具体解,考虑使用动态规划,不能使用回溯算法来搜索具体的解
- 题型:最值型+坐标型动态规划
1.1 确定状态
-
最后一步:在最优策略中,最长严格递增子序列一定存在着最后一个元素nums[i]。
虽然我们不知道是哪个字符,但是肯定有最后一个元素。
那么,有了最后一个字符之后,我们发现这里存在着两种情况(考虑序列长度大于2的条件下): -
第一种情况:最优策略中的最后一个元素 nums[i] 小于前面一个元素 nums[i - 1],这时最值取决于前面一个元素 nums[i - 1] 结尾的序列,这种情况不属于递增,可以直接跳过。
-
第二种情况:大于,这时候由于题目不要求子序列是连续的,因此我们需要枚举最后一个字符前面的所有元素进行比较,得到最长的递增子序列,然后加上 nums[i]。
-
由此,我们发现:
-
需要求以 nums[i - 1] 结尾的序列的最长递增子序列
-
而原问题是求以 nums[i] 结尾的序列的最长递增子序列
-
问题规模缩小了
-
状态定义:设 f[i] 是以 nums[i] 结尾的序列的最长递增子序列的长度。
-
如何设定状态?根据题目中的要求,如果是求长度,那么我们需要将状态的宾语设定为长度。
1.2 转移方程
f[i] = max( f[i], f[j] + 1) for j in [0, i) 且 nums[i] > nums[j]
1.3 初始条件和边界情况
- 初始条件:每一个元素都可以单独作为一个子序列,这时候的长度为1。
- 边界条件:情况2中 0 <= j < i,而且 nums[i] > nums[j],满足单调性。
1.4 计算顺序
- 从小到大计算:f[0], f[1], ……, f[n -1]
- 答案:最终的答案不一定是f[n - 1],因为我们不确定最优策略中的最后一个元素是哪个
- 因此答案为全局最大值:max(f[0], f[1], ……, f[n -1])
1.5 编程
1.5.1 参考1
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
int result = 1;
// 定义并初始化状态方程
vector<int> f(n, 1);
for (int i = 0; i < n; ++i) {
// f[i] = 1;
// 在子问题中,只有最后一个元素与前面的每一个元素都进行对比
// 这里和674题的连续不同:仅仅需要比较前一个元素就能确保最长
for (int j = 0; j < i; ++j) {
if (nums[j] < nums[i]) {
f[i] = max(f[i], f[j] + 1);
}
}
if (f[i] > result) {
result = f[i];
}
}
return result;
}
};
1.5.2 参考2:利用C++的函数求最大值
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
vector<int> f(n, 1);
for (int i = 0; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (nums[j] < nums[i]) {
f[i] = max(f[i], f[j] + 1);
}
}
}
return *max_element(f.begin(), f.end());
}
};
1.6 运行结果
法二:贪心 + 二分查找
2.1 题目分析
2.2 编程
2.2.1 参考1–替换策略1
- 在 tail 数组中进行二分查找,找到第一个比nums[i] 小的数 tail[k] ,并更新tail[k+1] = nums[i]。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int len = 1, n = (int)nums.size();
if (n == 0) {
return 0;
}
vector<int> d(n + 1, 0);
d[len] = nums[0];
for (int i = 1; i < n; ++i) {
if (nums[i] > d[len]) {
d[++len] = nums[i];
} else {
int l = 1, r = len, pos = 0; // 如果找不到说明所有的数都比 nums[i] 大,此时要更新 d[1],所以这里将 pos 设为 0
while (l <= r) {
int mid = (l + r) >> 1;
if (d[mid] < nums[i]) {
pos = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
d[pos + 1] = nums[i];
}
}
return len;
}
};
// 作者:LeetCode-Solution
// 链接:https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/zui-chang-shang-sheng-zi-xu-lie-by-// leetcode-soluti/
// 来源:力扣(LeetCode)
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2.2.2 参考2–替换策略2
- 用 nums[i] 覆盖掉大于它的元素中最小的那个。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> res(1);
res[0] = nums[0];
for(int i = 1; i < nums.size(); ++i) {
if (nums[i] > res[res.size() - 1]) {
res.emplace_back(nums[i]);
}
// 使用二分查找
else {
int left = 0, right = res.size() - 1;
while (left < right) {
// int mid = (left + right) >> 1;
int mid = (left + right) / 2;
if (res[mid] < nums[i]) {
left = mid + 1;
}
else {
right = mid;
}
}
// 用当前元素覆盖掉 >= 它的元素中最小的那个。
res[left] = nums[i];
}
}
return res.size();
}
};
2.2.3 参考3:借助C++的lower_bound(查找左边界,右边界)函数
- lower_bound( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> res;
for(auto n : nums) {
auto lb = lower_bound(res.begin(), res.end(), n);
if(lb == res.end()) {
res.push_back(n);
}
else {
res[lb - res.begin()] = n;
}
}
return res.size();
}
};
2.3 运行结果
1
2
3