LeetCode

354. 俄罗斯套娃信封问题(禁止套娃!)

2021-03-05  本文已影响0人  闭门造折

力扣题解《禁止套娃!(图解过程)》

方法一:比较暴力的动态规划

思路

复杂度分析

快乐小视频













具体代码:

class Solution {
public:
    int maxEnvelopes(vector<vector<int>>& envelopes) {
        int n = envelopes.size();
        // 先按照宽做排序,越大的放在越前面
        sort(envelopes.begin(), envelopes.end(), [](vector<int>& a, vector<int>& b){
            return a[0] > b[0];
        });

        // 预开空间(不妨多开几个防溢出)
        vector<int> dp(n + 5);

        int ans = 0;
        // 依次尝试放置每个信封
        for(int i = 0; i < n; i++){
            // 遍历他之前的每个信封,看能放下他且最多层的个数
            int maxh = 0;
            for(int j = 0; j < i; j++){
                // 判断是否可以放下当前的信封
                if(envelopes[j][0] > envelopes[i][0]
                && envelopes[j][1] > envelopes[i][1]){
                    // 如果可以放下当前信封,看看是不是最大高度
                    if(maxh < dp[j]){
                        maxh = dp[j];
                    }
                }
            }
            // 遍历一圈,找到最高,且能放下当前信封的maxh
            dp[i] = maxh + 1;

            // 判断当前信封高度是不是最高高度
            ans = max(ans, dp[i]);
        }
        return ans;
    }
};

用C++提交花费了1264 ms,确实挺慢的,看其他题解里写不同语言可能会超时。

但是确实这个思路最好想,也可以很方便的套用到面试题 08.13. 堆箱子这道题上。

方法二:第二维参与排序

思路

目标是将二维问题,转换为我们所熟悉的一维的最长上升子序列问题。因此需要降维,想办法在计算时,仅考虑 h 的顺序即可,忽略 w 影响。

复杂度分析

快乐小视频












具体代码

class Solution {
public:
    int maxEnvelopes(vector<vector<int>>& envelopes) {
        int n = envelopes.size();

        // 首先执行排序,按照宽度排序,小的在前大的在后
        sort(envelopes.begin(), envelopes.end(), [](vector<int>& a, vector<int>& b){
            if(a[0] == b[0]){
                // 对于宽度相等的信封,根据高度逆序,大的在前小的在后
                return a[1] > b[1];
            }
            return a[0] < b[0];
        });

        // 预开空间,设初始值为1,即仅包含当前信封
        vector<int> dp(n, 1);

        int ans = 0;
        // 计算最长上升子序列
        for(int i = 0; i < n; i++){
            for(int j = 0; j < i; j++){
                if(envelopes[j][1] < envelopes[i][1]){
                    // 如果h严格升序,尝试更新dp[i]
                    dp[i] = max(dp[i], dp[j] + 1);
                }
            }
            // 尝试更新最大值ans
            ans = max(ans, dp[i]);
        }
        return ans;
    }
};

比起方法一,有一定优化,但是优化效果仍然有限,使用C++提交的运行时间是1004 ms。

仍然是一个 O(N^2) 的解决方法

方法三:维护单增序列

思路

更换想法,对方法二中最长上升子序列问题的求解做优化,不再维护长度为 O(N)dp 数组,改为维护一条最长递增序列。

复杂度分析

快乐小视频

















具体代码

class Solution {
public:
    int maxEnvelopes(vector<vector<int>>& envelopes) {
        int n = envelopes.size();

        // 首先执行排序,按照宽度排序,小的在前大的在后
        sort(envelopes.begin(), envelopes.end(), [](vector<int>& a, vector<int>& b){
            if(a[0] == b[0]){
                // 对于宽度相等的信封,根据高度逆序,大的在前小的在后
                return a[1] > b[1];
            }
            return a[0] < b[0];
        });

        // 预开空间,初始值为排序后第一个信封的高度
        vector<int> dp(1, envelopes[0][1]);

        int ans = 0;
        // 计算最长上升子序列
        // 第0个元素已默认放入dp,因此从1开始遍历
        for(int i = 1; i < n; i++){
            // 搜索合适的更新位置
            int j = 0;
            for(; j < dp.size(); j++){
                // 需要注意,只要不小于当前大小,即可更新
                if(dp[j] >= envelopes[i][1]){
                    dp[j] = envelopes[i][1];
                    break;
                }
            }
            // 如果整个dp列表中,不含有比当前h大的值,则扩展dp列表
            if(j == dp.size()){
                dp.emplace_back(envelopes[i][1]);
            }
        }
        return dp.size();
    }
};

比起方法二,这个方法其实优化了很多,远不是 O(N^2) 的复杂度,而是 O(N * len(dp)) 但是假如一开始给定的就是一个层层套娃的合法序列,那么最差时间复杂度仍然能达到O(N^2)

方法四:二分查找

思路

方法三中,搜索插入位置的过程,用的是 O(N) 的搜索插入,可以通过二分法,优化到 O(logN)

复杂度分析

快乐小视频

没啥快乐小视频了,看看方法三的得了。

具体代码

class Solution {
public:
    int maxEnvelopes(vector<vector<int>>& envelopes) {
        int n = envelopes.size();

        // 首先执行排序,按照宽度排序,小的在前大的在后
        sort(envelopes.begin(), envelopes.end(), [](vector<int>& a, vector<int>& b){
            if(a[0] == b[0]){
                // 对于宽度相等的信封,根据高度逆序,大的在前小的在后
                return a[1] > b[1];
            }
            return a[0] < b[0];
        });

        // 预开空间,初始值为排序后第一个信封的高度
        vector<int> dp(1, envelopes[0][1]);

        int ans = 0;
        // 计算最长上升子序列
        // 第0个元素已默认放入dp,因此从1开始遍历
        for(int i = 1; i < n; i++){
            // 搜索合适的更新位置,使用二分模板
            // 额外引入一个index来记录满足条件合法的值
            // 有的人的模板中,只有l和r两个变量,但是那个边界条件我总是记不住
            // 引入一个新的变量,个人感觉逻辑更明朗
            int l = 0, r = dp.size() - 1;
            int index = -1;
            while(l <= r){
                // mid这里用l加一半的形式,不容易溢出int
                int mid = l + (r - l) / 2;
                if(dp[mid] >= envelopes[i][1]){
                    // 我们要找的是dp数组中第一个大于等于当前h的位置
                    // 因此在这里更新index值
                    index = mid;
                    r = mid - 1;
                }
                else{
                    l = mid + 1;
                }
            }
            if(index == -1){
                dp.emplace_back(envelopes[i][1]);
            }
            else{
                dp[index] = envelopes[i][1];
            }
        }
        return dp.size();
    }
};

C++写到这里,差不多就是40-50 ms了,比方法一快了不老少,心满意足。

写在最后

延伸扩展问题

简化到一维:300. 最长递增子序列
扩展到三维:面试题 08.13. 堆箱子

加油熹熹坨坨!

给阿熹的鼓劲儿!
上一篇 下一篇

猜你喜欢

热点阅读