在做了算法题长时间不复习后总是会遗忘,从今天开始写博客,力争用最简单的话语讲明算法,以便巩固与复习。
题目:
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。 子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
解答:
方法一:动态规划(O(n^2))
新建数组dp,dp[i]表示数组中前i个数中,以第i个元素结尾的最长递增子序列的长度。对于元素i需要遍历前i-1个数,并且在遍历第i个数的时候,数组dp中对应的前i-1个数的最长子序列均已计算完毕。因此对于第i个元素只需要统计前i-1个数中比第i个数小的最长子序列的长度。
以数组[0,3,1,6,2,2,7]为例:
- 在第一轮中,dp[0] = 1
- 第二轮,在3之前的比3小的数只有0,并且0对应的子序列长度为1,因此此时dp[1] = 1
- 第三轮,在1之前的比1小的数同样只有0,因此dp[2] = 1
- 第四轮,在6之前的数均比6小,首先0对应的子序列长度为1,此时dp[3] = 2;接着3对应的子序列长度为2,经过比较,dp[3] = 3;最后是1,对应的子序列长度同样为1,因此最后dp[3] = 3
- ……
- 最后一轮,在7之前的数均比7小,其中,6对应的子序列长度为3,2对应的子序列长度同样为3,因此7对应的子序列长度为4。
具体的代码实现如下所示:
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int len = nums.size();
if(len == 0)
return 0;
int arr[len], sum = 0;
for(int i = 0; i < len; i++){
arr[i] = 1;
for(int j = 0; j < i; j++){
if(nums[j] < nums[i])
arr[i] = max(arr[j] + 1, arr[i]);
}
sum = max(sum, arr[i]);
}
return sum;
}
};
需要注意的是并不是dp中最后一个数据最大,需要进行比较。
方法二:贪心(O(nlogn))
该算法之所以称为贪心,是因为想要让序列上升的尽可能慢,也就是子序列的数据尽可能小。创建一个数组dp,这时的dp表示的含义与第一种方法不同,dp[i]表示长度为 i 的最长上升子序列的末尾元素的最小值,最后的结果是数组的长度。在遍历的同时将符合条件的数据插入数组或者将数组中的数据进行替换。
算法主要功能是将比dp中最后一个数大的数据直接插到数组末尾,将比最后一个数小的数进行替换。这一步其实比较难理解,我们举个例子,[4, 10, 11, 12, 13, 14, 5, 8, 9]。我们可以比较直观的看出最长子序列长度为6。
- 一直到14的时候,获取的子序列为[4, 10, 11, 12, 13, 14]。但是到5的时候,因为5比14小,根据二分查找,5将替换10,现有的数组变为[4, 5, 11, 12, 13, 14]。到8的时候,8将替换11,数组变为[4, 5, 8, 12, 13, 14]。这样依次下去,数组最终为[4, 5, 8, 9, 13, 14],我们可以发现,后面的数据并没有阻碍结果的正确性。
- 与之相对的例子[4, 8, 9, 5, 6, 7]。当到9的时候,获取的结果为[4, 8, 9],到5的时候,获取的子序列为[4, 5, 9];下一个获取的子序列为[4, 5, 6];下一个获取的子序列为[4, 5, 6, 7],也就是我们需要的结果。
从这两个例子中我们可以看出,该算法的本质是将我们当前获取的最长子序列的较大的数由后面的较小的数替换。该类型的题目中最关键的问题是无法确定是否将一个数插入该子序列。该方法就是为了避免这种情况的出现,或者说我们可以将任意符合条件的数据插入子序列,最后算法会对结果进行处理。
具体的代码实现如下所示:
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int len = nums.size();
if(len == 0)
return 0;
vector<int> v1;
v1.push_back(nums[0]);
for(int i = 1; i < len; i++){
if(nums[i] > v1.back())
v1.push_back(nums[i]);
else{
int begin = 0, end = v1.size() - 1, mid;
while(begin < end){
mid = (begin + end) / 2;
if (v1[mid] < nums[i]) {
begin = mid + 1;
} else {
end = mid;
}
}
v1[begin] = nums[i];
}
}
return v1.size();
}
};
变式 Leetcode-354 俄罗斯套娃信封问题
给你一个二维整数数组 envelopes ,其中 envelopes[i] = [wi, hi] ,表示第 i 个信封的宽度和高度。
当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。
请计算 最多能有多少个 信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。
注意:不允许旋转信封。
class Solution {
public:
static bool cmpl(const vector<int> &a, const vector<int> &b){
if(a[0] == b[0])
return a[1] < b[1];
else
return a[0] < b[0];
}
int maxEnvelopes(vector<vector<int>>& envelopes) {
int arr[envelopes.size() + 1], maxn = 0;
arr[1] = 1;
if(envelopes.size() == 0)
return 0;
sort(envelopes.begin(), envelopes.end(), cmpl);
for(int i = 0; i < envelopes.size(); i++){
arr[i] = 1;
for(int j = 0; j < i; j++){
if(envelopes[i][0] > envelopes[j][0] && envelopes[i][1] > envelopes[j][1]){
arr[i] = max(arr[j] + 1, arr[i]);
}
}
maxn = max(maxn, arr[i]);
}
return maxn;
}
};
该方法与动态规划类似,只不过重点是如何将所有的信封按照尺寸合理的排序。在实现中是将信封首先按照长进行排列,长度相同按照宽排列。
容易出错的例子:[[2, 100], [3, 200], [4, 300], [5, 220]. [6, 230], [7, 240]]。