1 问题描述
输入一个二维数组,每个元素代表一个信封,信封内每个元素代表了一个信封,一个信封是一个长度为2的数组,存储了宽和长
- 长和宽都比另一个信封小的情况下,能够装入另一个信封
- 求解最多能够多少个信封套在一起
- 信封不能旋转
2 选择排序后遍历
解题思路
- 从小到大排序,对于信封按照宽度进行排序,宽度一致时按照高度进行排序,排序方式采用选择排序
- 通过遍历的方式求出套娃的最大值
- 第一个信封套娃数量为1,记录上一个信封为当前信封,整套信封套娃数量最大值暂时计为1
- 第二个信封开始
- 比较当前信封的宽度和上一个信封的宽度
- 如果宽度相等,跳过当前信封,在宽度一样的情况下信封高度越小越好,而上一个信封高度必定更小
- 如果宽度比上一个信封大,比较当前信封的高度和上一个信封的高度
- 如果高度大于上个信封,那么可以将上个信封装入当前信封,记录上一个信封为当前信封,整套信封最大套娃数量加一
- 如果高度小于上个信封,跳过当前信封
- 比较当前信封的宽度和上一个信封的宽度
- 算法复杂度:O(n^2)
- 选择排序 O(n^2)
- 遍历求套娃最大值 O(n)
以上解题思路确实不正确,感觉像是只考虑了宽,是对宽度进行贪心的情况,换一个角度,如果先对于高度进行排序,其中宽度作为第二次序,有可能得到不一样的答案。
如果保持上边的思路不变,希望正确求解的话,在求出了优先对宽排序的情况下的套娃最大值后,再次对高度进行排序,宽度作为第二次序,重复一遍上边的步骤,得到一个优先对高度进行排序的套娃最大值,最后两者进行比较,得到一个套娃最大值
感觉还是不对,从两个片面的解当中选出一个大的,不能代表这个解就是正确的,综合考量宽度和高度的话有可能得到更大的套娃值
下面是我一半正确的代码,通过32 / 84 个通过测试用例,感觉思路走错方向了就没有再改
class Solution {
public int maxEnvelopes(int[][] envelopes) {
int n = envelopes.length;
if(n == 1){
return 1;
}
int min = 0;
for(int i = 0; i < n - 1; i++){
min = i;
for(int j = i + 1; j < n; j++){
if(envelopes[min][0] > envelopes[j][0]){
min = j;
}else if(envelopes[min][0] == envelopes[j][0] ){
min = (envelopes[min][1] < envelopes[j][1])?min:j;
}
}
swap(envelopes,min,i);
}
int most = 1;
int last = 0;
for(int i = 1; i < n; i++){
if(envelopes[i][0] > envelopes[last][0] && envelopes[i][1] > envelopes[last][1]){
most ++;
last = i;
}
}
return most;
}
public void swap(int[][] envelopes, int i, int j){
int tmpW = envelopes[i][0];
int tmpH = envelopes[i][1];
envelopes[i][0] = envelopes[j][0];
envelopes[i][1] = envelopes[j][1];
envelopes[j][0] = tmpW;
envelopes[j][1] = tmpH;
}
}
3 排序后遍历+二分查找
解题思路
- 排序,希望宽度是从小到大的顺序,宽度相等时,高度是从大到小的顺序
- 遍历,使用一个链表来记录套娃的信封的序号
- 如果当前的信封高度大于套娃信封的最后一个信封高度,这个时候直接将这个信封作为最大信封,放到链表末尾
- 如果当前的信封高度小于等于套娃信封的最后一个信封高度,那我们在链表中找到一个位置j0,使得j0的高<当前的高,当前的高<=j0 + 1的高,将j0 + 1的信封改为当前
Q:遍历时第二种情况的疑问:这样修改链表某个结点的高度,宽度的递增顺序不就没有用了吗
修改最后一个结点时,要么宽度和其相等,要么宽度大于它
修改一个结点后面的结点,说明修改后后面的结点宽度一定大于前面的结点(同一宽度,高度从大到小排列)
我们希望的是在宽度顺序增大的基础上,高度增加的越慢越好
- 修改链表中的结点,并不会改变链表的长度,也就是说链表依然记录了到当前位置的套娃最大个数
- 修改分为两种,修改链表最后一个结点,修改链表其他位置结点
- 修改最后一个结点,表明其高度大于倒数第二个结点,但是小于最后一个结点,修改这个结点有利于1后面的结点直接添加到链表后面
- 修改的不是最后一个结点,表明在被修改的这个结点存在这么一种信封的放置方案,此时链表中宽度严格单调递增的规律可能已经被打破了,但是
- 不影响后续信封高度链接到最后一个结点
- 当前链表依旧记录了到当前位置的最大套娃数
- 后续结点参照这个修改的高度修改进来有影响吗,没有
- 如果这个结点的高度作为上界被后面的结点参考并采用,宽度更大的结点可能会修改到这个结点前,或者覆盖这个结点,但这都不影响,前面被修改的这部分只是覆盖了原来的放置
- 如果这个结点的高度作为下界被后面的结点参考并采用,前置条件是后面的结点高度大于当前结点高度,由于同一宽度下信封按高度从大到小排列,他们不可能是同一宽度的结点,只能是不同宽度,而后者的宽度必然大于前者的宽度,说明修改之后,其宽度是存在递增的放置方案的。
Q:为什么需要高度从大到小排序
-
因为在进行遍历的时候,我们的关注度都放在了高度上,我们并不知道宽度是大于前面还是等于前面,如果高度顺序从小到大,按照我们的逻辑,如果这个宽度的第一个信封高度就大于链表末尾结点,这个宽度的所有信封都会链接到链表上,会造成最大嵌套数溢出的错误。
-
假设存在n种宽度的信封,而每个宽度的信封都有若干种高度,那么最多的嵌套数就是n,嵌套数不能超过n。链接同一个宽度的多个信封必然是错误的
-
添加最后一个结点时,如果高度是逆序,不可能存在同一宽度连续添加
-
时间复杂度 ;O(nlogn)
- 排序 O(nlogn), 这里采用了其他的排序方法
- 遍历 + 二分查找: O(nlogn)
class Solution {
public int maxEnvelopes(int[][] envelopes) {
int n = envelopes.length;
Arrays.sort(envelopes, new Comparator<int[]>() {
public int compare(int[] e1, int[] e2) {
if (e1[0] != e2[0]) {
return e1[0] - e2[0];
} else {
return e2[1] - e1[1];
}
}
});
List<Integer> f = new ArrayList<Integer>();
f.add(envelopes[0][1]);
for (int i = 1; i < n; ++i) {
int num = envelopes[i][1];
if (num > f.get(f.size() - 1)) {
f.add(num);
} else {
int index = binarySearch(f, num);
f.set(index, num);
}
}
return f.size();
}
public int binarySearch(List<Integer> f, int target) {
int low = 0, high = f.size() - 1;
while (low < high) {
int mid = (high - low) / 2 + low;
if (f.get(mid) < target) {
low = mid + 1;
} else {
high = mid;
}
}
return low;
}
}