题目描述:
给定一些标记了宽度和高度的信封,宽度和高度以整数对形式 (w, h) 出现。当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。
请计算最多能有多少个信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。
说明:
不允许旋转信封。
示例:
输入: envelopes = [[5,4],[6,4],[6,7],[2,3]]
输出: 3
解释: 最多信封的个数为 3, 组合为: [2,3] => [5,4] => [6,7]。
解法一: 普通的dp
这个解法理解起来好一些,但是效率不高.
解释:
首先,排序,sort默认排序是从小到大排序.由于这里有两个值,所以在宽度W相同的情况下,就按照H从小到大排序, 当然,更好的解法是使得高度H从大到小排,但一般那个想法不容易想到,要在考试的时候相出来比较难.
排序完成后,就可以进行动态规划了,这个就是一个最长上升子序列问题,最笨的方法就是这样:
遍历排序后的envelopes, 举个例子:
envelopes | [2,100] | [3,200] | [4,300] | [5,250] | [5,400] | [5,500] | [6,360] | [6,370] | [7,380] |
---|---|---|---|---|---|---|---|---|---|
idx | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
dp | 1 | 2 | 3 | 3 | 4 | 4 | 4 | 4 | 5 |
dp中的值代表以当前信封结尾的前面所有序列的最长上升子序列的长度.例如,在idx=6的位置,因为它前面的[5,400]和[5,500]的H都比[6,360]大,所以,它遍历它前面比大小的dp最大值的位置,在追加上当前值,组成当前位置的dp[6].这个过程比较耗时,因为没到一个位置,都要遍历一遍它前面的所有数.
那么,有人就想到了一个好办法,就是使用二分法找到它左边第一个比它的H大的那个数,用当前这个数替代它.例如,对于[6,360]来说,它的左边第一个比它大的数就是[5,400],则每一步都这么做的话,最终的dp长度就是所要求的结果.这就有了第二种方法.移步到->解法二.
class Solution {
public:
int maxEnvelopes(vector<vector<int>>& envelopes) {
int n = envelopes.size();
if(n == 0) return 0;
sort(envelopes.begin(), envelopes.end());
vector<int> dp(n, 1);
dp[0] = 1;
int res = 1;
for(int i = 1; i < n; ++i){
for(int j = 0; j < i; ++j){
if(envelopes[j][0] < envelopes[i][0] && envelopes[j][1] < envelopes[i][1]){
dp[i] = max(dp[i], dp[j] + 1);
}
}
res = max(res, dp[i]);
}
return res;
}
};
解法二: 优化了的dp
W升序, H降序排序后的envelopes:
envelopes | [2,100] | [3,200] | [4,300] | [5,500] | [5,400] | [5,250] | [6,370] | [6,360] | [7,380] |
---|---|---|---|---|---|---|---|---|---|
idx | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
iter | dp |
---|---|
0 | [100] |
1 | [100, 200] |
2 | [100, 200, 300] |
3 | [100, 200, 300, 500] |
4 | [100, 200, 300, 400] |
5 | [100, 200, 250, 400] |
6 | [100, 200, 250, 370] |
7 | [100, 200, 250, 360] |
8 | [100, 200, 250, 360, 380] |
这里加粗的地方可能不好理解,为什么要用250代替300呢?这里我是这么想的,这是为了给后面做准备,因为到iter=4这一步,最长已经到了4了,那么如果所以,这个300已经没有用了,因为是从前往后这么走过来的,但是假如后面有更小的数可以代替它,并且有更小的数可以代替400,那么像后面380这样的数就能继续增加它的长度.这样更长的这个序列就覆盖了原来的那个长度为4的序列.即使没有完成增长也没关系(例如去掉最后这个数,enveploes只到[6,360], 那结果为4依然是正确的),因为dp的长度不可能再比4更小了,只可能>=4.
就像上面说的, 我要使用二分法查找比当前信封位置之前第一个比它H大的那个信封的位置,如果没找到则追加到dp末尾,所以这里dp数组中存的不是原来的最长上升子序列的数目,而是每个要参与最长上升子序列的信封的H的值.
这样在每次二分查找的时候,只需在dp中查找,然后,替换即可,遍历到最后,dp的长度就是我们所要求的值.
关于为什么把H按照降序排,下面的解释摘自这里.
先对宽度 w 进行升序排序,如果遇到 w 相同的情况,则按照高度 h 降序排序。之后把所有的 h 作为一个数组,在这个数组上计算 LIS 的长度就是答案。
然后在 h 上寻找最长递增子序列:
代码:
第一种写法: (这个是效率最高的一个).
class Solution {
public:
static bool judge(const pair<int,int> a,const pair<int,int> b){
if(a.first!=b.first){
return a.first<b.first;
}
return a.second>b.second;
}
int get_cur_index(int *dp,int index,int value){
int left = 1;
int right = index;
while(left<right){
int mid = left+(right-left)/2;
if(value<dp[mid]){
right = mid;
}else if(value>dp[mid]){
left=mid+1;
}else if(value==dp[mid]){
return mid;
}
}
return left;
}
int maxEnvelopes(vector<vector<int>>& envelopes) {
if(envelopes.empty()){
return 0;
}
vector<pair<int,int> >nums;
for(int i=0;i<envelopes.size();i++){
nums.push_back(make_pair(envelopes[i][0],envelopes[i][1]));
}
sort(nums.begin(),nums.end(),this->judge);
int dp[envelopes.size()+1];
dp[1] = nums[0].second;
int index = 1;
// 一个条件相同,怎么去除相同身高 or 相同体重的选手?
for(int i=1;i<nums.size();i++){
if(nums[i].second>dp[index]){
dp[++index] = nums[i].second;
}else{
int new_index = get_cur_index(dp,index,nums[i].second);
dp[new_index] = nums[i].second;
}
}
return index;
}
};
第二种写法:
class Solution {
public:
int maxEnvelopes(vector<vector<int>>& envelopes) {
sort(envelopes.begin(), envelopes.end(), [](const vector<int> &a, const vector<int> &b){
return a[0] == b[0] ? a[1] > b[1]: a[0] < b[0];
});
vector<int> dp; //长度为 i+1 的地方 最小的数字
for(const auto &e: envelopes) {
auto p = lower_bound(dp.begin(), dp.end(), e[1]); //二分查找第一个大于等于的地方
if(p == dp.end()) dp.push_back(e[1]);
else *p = e[1];
}
return dp.size();
}
};