354. 俄羅斯套娃信封問題(禁止套娃!)

力扣題解《禁止套娃=烨簟(圖解過程)》

方法一:比較暴力的動態(tài)規(guī)劃

思路

  • 狀態(tài)定義:

    • dp[i] 表示僅使用信封 [0, i] (這里是區(qū)間的意思,表示前 i+1 個信封),且以第 i 個信封為頂端信封時的最大高度盾鳞。
  • 狀態(tài)轉(zhuǎn)移:

    • 首先對整個數(shù)組根據(jù)寬度排序,寬的信封放在前面瞻离。
    • 設(shè) j∈[0, i),考慮每輪計算新的 dp[i] 時腾仅,遍歷 [0, i) 區(qū)間,做以下判斷:
    1. 當(dāng) envelopes[j][0] > envelopes[i][0]envelopes[j][1] > envelopes[i][1]:認為信封 j 嚴格大于信封 i套利,此時嘗試更新底座最大高度 maxh : maxh = max(maxh, dp[j])
    2. 當(dāng)信封 j 不嚴格大于信封 i:跳過推励。
    • 全部遍歷完成后,更新 dp[i]dp[i] = maxh + 1肉迫。理解為在最大可放置的高度上验辞,再放上當(dāng)前信封。
    • 轉(zhuǎn)移方程dp[i] = max(dp[j]) + 1 (j ∈ [0, i) 且信封j比信封i嚴格大)
  • 初始狀態(tài):

    • dp[i] 所有元素置0喊衫,當(dāng)計算后才進行賦值受神。
  • 返回值:

    • 返回 dp 列表最大值,即可得最大套娃層數(shù)格侯。

復(fù)雜度分析

  • 時間復(fù)雜度 O(N^2):排序需要 O(NlogN)鼻听,遍歷 dp 列表需要 O(N)财著,計算每一個 dp[i] 需要 O(N)

  • 空間復(fù)雜度 O(N):dp列表占用 O(N) 空間

快樂小視頻













具體代碼:

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];
        });

        // 預(yù)開空間(不妨多開幾個防溢出)
        vector<int> dp(n + 5);

        int ans = 0;
        // 依次嘗試放置每個信封
        for(int i = 0; i < n; i++){
            // 遍歷他之前的每個信封撑碴,看能放下他且最多層的個數(shù)
            int maxh = 0;
            for(int j = 0; j < i; j++){
                // 判斷是否可以放下當(dāng)前的信封
                if(envelopes[j][0] > envelopes[i][0]
                && envelopes[j][1] > envelopes[i][1]){
                    // 如果可以放下當(dāng)前信封撑教,看看是不是最大高度
                    if(maxh < dp[j]){
                        maxh = dp[j];
                    }
                }
            }
            // 遍歷一圈,找到最高醉拓,且能放下當(dāng)前信封的maxh
            dp[i] = maxh + 1;

            // 判斷當(dāng)前信封高度是不是最高高度
            ans = max(ans, dp[i]);
        }
        return ans;
    }
};

用C++提交花費了1264 ms伟姐,確實挺慢的,看其他題解里寫不同語言可能會超時亿卤。

但是確實這個思路最好想愤兵,也可以很方便的套用到面試題 08.13. 堆箱子這道題上。

方法二:第二維參與排序

思路

目標(biāo)是將二維問題排吴,轉(zhuǎn)換為我們所熟悉的一維的最長上升子序列問題秆乳。因此需要降維,想辦法在計算時钻哩,僅考慮 h 的順序即可屹堰,忽略 w 影響。

  • 狀態(tài)定義:

    • dp[i] 表示僅使用信封 [0, i]街氢,且以第 i 個信封為底部信封時的最大高度扯键。
  • 狀態(tài)轉(zhuǎn)移:

    • 首先對整個數(shù)組按照寬度由小到大排序,排序同時珊肃,對于寬度相同的項荣刑,根據(jù)高度由大到小排序

    假如我們?nèi)韵穹椒ㄒ灰粯勇浊牵瑑H針對寬度排序厉亏。那么對于形如 [4, 4][4, 5]的兩項,由于 [4, 4]排序在 [4, 5]之前评矩,這就導(dǎo)致僅考慮h時叶堆,我們認為 [4, 4][4, 5]可以構(gòu)成一組“套娃”。但是按照“二維嚴格升序”的定義斥杜,兩個矩形的寬相等虱颗,不屬于嚴格升序

    為了規(guī)避上述的問題蔗喂,我們對寬度相等的兩個矩形忘渔,使用高度降序排序,這樣一來缰儿,上述問題的排列將變成: [4, 5], [4, 4]畦粮。在我們計算最長上升子序列時,由于 5>4,也就不會出現(xiàn)“錯誤套娃”宣赔。
    - 排序完畢后预麸,提取出所有的h,形成一個新的數(shù)組儒将。對新的數(shù)組執(zhí)行“最長上升子序列”問題的求解:
    - 設(shè) j∈[0, i),考慮每輪計算新的 dp[i] 時吏祸,遍歷 [0, i) 區(qū)間,做以下判斷:
    1. 當(dāng) envelopes[j][1] > envelopes[i][1]:認為信封 j 嚴格大于信封 i钩蚊,此時嘗試更新 dp[i] : dp[i] = max(dp[i], dp[j] + 1)
    2. 當(dāng)信封 j 不嚴格大于信封 i:跳過贡翘。
    - 轉(zhuǎn)移方程dp[i] = max(dp[j]) + 1 (j ∈ [0, i) 且信封j比信封i高度嚴格大)

  • 初始狀態(tài):

    • dp[i] 所有元素置1,表示該信封可獨自形成層數(shù)為1的套娃砰逻。
  • 返回值:

    • 返回 dp 列表最大值鸣驱,即可得最大套娃層數(shù)。

復(fù)雜度分析

  • 時間復(fù)雜度 O(N^2):排序需要 O(NlogN)蝠咆,遍歷 dp 列表需要 O(N)踊东,計算每一個 dp[i] 需要 O(N)

  • 空間復(fù)雜度 O(N):dp列表占用 O(N) 空間

快樂小視頻












具體代碼

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

        // 首先執(zhí)行排序,按照寬度排序勺美,小的在前大的在后
        sort(envelopes.begin(), envelopes.end(), [](vector<int>& a, vector<int>& b){
            if(a[0] == b[0]){
                // 對于寬度相等的信封递胧,根據(jù)高度逆序碑韵,大的在前小的在后
                return a[1] > b[1];
            }
            return a[0] < b[0];
        });

        // 預(yù)開空間,設(shè)初始值為1,即僅包含當(dāng)前信封
        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;
    }
};

比起方法一,有一定優(yōu)化祝闻,但是優(yōu)化效果仍然有限占卧,使用C++提交的運行時間是1004 ms。

仍然是一個 O(N^2) 的解決方法

方法三:維護單增序列

思路

更換想法联喘,對方法二中最長上升子序列問題的求解做優(yōu)化华蜒,不再維護長度為 O(N)dp 數(shù)組,改為維護一條最長遞增序列豁遭。

  • 狀態(tài)定義:

    • dp[i] 表示長度為 i + 1 的最長上升子序列叭喜,第 i 位的最小值。
  • 狀態(tài)轉(zhuǎn)移:

    • 同方法二蓖谢,首先對整個數(shù)組按照寬度由小到大排序捂蕴,排序同時,對于寬度相同的項闪幽,根據(jù)高度由大到小排序啥辨。
    • 排序完畢后,提取出所有的h盯腌,形成一個新的數(shù)組溉知。對新的數(shù)組執(zhí)行“最長上升子序列”問題的求解:
      • 遍歷 [0, n) 區(qū)間,對每一個值 x ,判斷 x 在單增序列 dp 中的位置级乍,做以下判斷:

        1. 當(dāng) dp 中僅存在某值滿足 y > x:使用 x 替換該值 y
        2. 當(dāng) dp 中存在多值滿足 y_t > y_2 > y_1 > x:使用 x 替換符合條件的最小值 y_1
        3. 當(dāng) dp 中不存在值滿足 y > x:將 x 添加到 dp 數(shù)組最末端

        為什么可以這樣做舌劳?:假設(shè)當(dāng)前 dp 隊列為1, 3, 6。
        這翻譯過來是:

        1. 存在一條上升子序列玫荣,長度為1蒿囤,且以1為結(jié)尾;
        2. 存在一條上升子序列崇决,長度為2材诽,且以3為結(jié)尾;
        3. 存在一條上升子序列恒傻,長度為3脸侥,且以6為結(jié)尾。

        假設(shè)此時來了個新的值4盈厘,顯然的睁枕,可以在長度為2且末尾為3的那條上升子序列后,再添加一個4沸手,形成一條長度為3外遇,且以4為結(jié)尾的上升子序列。
        同時更新 dp 隊列變成1,3,4契吉。這是因為同樣是長度為3跳仿,以6為結(jié)尾后,想繼續(xù)擴充捐晶,新增的數(shù)字至少為7菲语;而以4為結(jié)尾時,新增數(shù)字僅需至少為5即可惑灵。

        額外需要注意的是:dp 的結(jié)果山上,不一定就是一條存在的上升子序列:仍然以1,3,6為例,假設(shè)此時新增的數(shù)字為2英支,則 dp 更新為1,2,6佩憾。但實際上并不存在一條子序列,滿足1,2,6干花。1,2,6僅可翻譯為:

        1. 存在一條上升子序列妄帘,長度為1,且以1為結(jié)尾把敢;
        2. 存在一條上升子序列寄摆,長度為2,且以2為結(jié)尾修赞;
        3. 存在一條上升子序列婶恼,長度為3桑阶,且以6為結(jié)尾。
          - 轉(zhuǎn)移方程dp[index] = x (index為第一個大于x的值所在位置)
  • 初始狀態(tài):

    • dp 僅有一個元素勾邦,為排序后第一個信封的 h 值蚣录。
  • 返回值:

    • 返回 dp 列表長度,即可得最大套娃層數(shù)眷篇。

復(fù)雜度分析

  • 時間復(fù)雜度 O(N^2):排序需要 O(NlogN)萎河,遍歷信封列表需要 O(N),計算每一個信封插入位置需要 O(N)

  • 空間復(fù)雜度 O(N):dp列表占用 O(N) 空間

快樂小視頻

















具體代碼

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

        // 首先執(zhí)行排序蕉饼,按照寬度排序虐杯,小的在前大的在后
        sort(envelopes.begin(), envelopes.end(), [](vector<int>& a, vector<int>& b){
            if(a[0] == b[0]){
                // 對于寬度相等的信封,根據(jù)高度逆序昧港,大的在前小的在后
                return a[1] > b[1];
            }
            return a[0] < b[0];
        });

        // 預(yù)開空間,初始值為排序后第一個信封的高度
        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++){
                // 需要注意,只要不小于當(dāng)前大小创肥,即可更新
                if(dp[j] >= envelopes[i][1]){
                    dp[j] = envelopes[i][1];
                    break;
                }
            }
            // 如果整個dp列表中达舒,不含有比當(dāng)前h大的值,則擴展dp列表
            if(j == dp.size()){
                dp.emplace_back(envelopes[i][1]);
            }
        }
        return dp.size();
    }
};

比起方法二叹侄,這個方法其實優(yōu)化了很多巩搏,遠不是 O(N^2) 的復(fù)雜度,而是 O(N * len(dp)) 但是假如一開始給定的就是一個層層套娃的合法序列,那么最差時間復(fù)雜度仍然能達到O(N^2)

方法四:二分查找

思路

方法三中,搜索插入位置的過程,用的是 O(N) 的搜索插入,可以通過二分法淤翔,優(yōu)化到 O(logN)

  • 狀態(tài)定義:

    • dp[i] 表示長度為 i + 1 的最長上升子序列饶唤,第 i 位的最小值肄程。
  • 狀態(tài)轉(zhuǎn)移:

    • 同方法二,首先對整個數(shù)組按照寬度由小到大排序得湘,排序同時杖玲,對于寬度相同的項,根據(jù)高度由大到小排序淘正。
    • 排序完畢后摆马,提取出所有的h,形成一個新的數(shù)組鸿吆。對新的數(shù)組執(zhí)行“最長上升子序列”問題的求解:
      • 遍歷 [0, n) 區(qū)間囤采,對每一個值 x ,判斷 x 在單增序列 dp 中的位置惩淳,做以下判斷:
        1. **當(dāng) dp 中僅存在某值 y > x **:使用 x 替換該值 y
        2. **當(dāng) dp 中存在多值 y_t > y_2 > y_1 > x **:使用 x 替換符合條件的最小值 y_1
        3. 當(dāng) dp 中不存在值滿足 y > x:將 x 添加到 dp 數(shù)組最末端
      • 轉(zhuǎn)移方程dp[index] = x (index為第一個大于x的值所在位置)
  • 初始狀態(tài):

    • dp 僅有一個元素蕉毯,為排序后第一個信封的 h 值乓搬。
  • 返回值:

    • 返回 dp 列表長度,即可得最大套娃層數(shù)代虾。

復(fù)雜度分析

  • 時間復(fù)雜度 O(NlogN):排序需要 O(NlogN)进肯,遍歷信封列表需要 O(N),計算每一個信封插入位置需要 O(logN)

  • 空間復(fù)雜度 O(N):dp列表占用 O(N) 空間

快樂小視頻

沒啥快樂小視頻了棉磨,看看方法三的得了江掩。

具體代碼

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

        // 首先執(zhí)行排序,按照寬度排序乘瓤,小的在前大的在后
        sort(envelopes.begin(), envelopes.end(), [](vector<int>& a, vector<int>& b){
            if(a[0] == b[0]){
                // 對于寬度相等的信封环形,根據(jù)高度逆序,大的在前小的在后
                return a[1] > b[1];
            }
            return a[0] < b[0];
        });

        // 預(yù)開空間,初始值為排序后第一個信封的高度
        vector<int> dp(1, envelopes[0][1]);

        int ans = 0;
        // 計算最長上升子序列
        // 第0個元素已默認放入dp衙傀,因此從1開始遍歷
        for(int i = 1; i < n; i++){
            // 搜索合適的更新位置斟赚,使用二分模板
            // 額外引入一個index來記錄滿足條件合法的值
            // 有的人的模板中,只有l(wèi)和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數(shù)組中第一個大于等于當(dāng)前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. 堆箱子

加油熹熹坨坨刃鳄!

給阿熹的鼓勁兒!
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末钱骂,一起剝皮案震驚了整個濱河市叔锐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌见秽,老刑警劉巖愉烙,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異解取,居然都是意外死亡步责,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門禀苦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蔓肯,“玉大人,你說我怎么就攤上這事振乏≌岚” “怎么了?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵慧邮,是天一觀的道長调限。 經(jīng)常有香客問我邻储,道長,這世上最難降的妖魔是什么旧噪? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任吨娜,我火速辦了婚禮,結(jié)果婚禮上淘钟,老公的妹妹穿的比我還像新娘宦赠。我一直安慰自己,他們只是感情好米母,可當(dāng)我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布勾扭。 她就那樣靜靜地躺著,像睡著了一般铁瞒。 火紅的嫁衣襯著肌膚如雪妙色。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天慧耍,我揣著相機與錄音身辨,去河邊找鬼。 笑死芍碧,一個胖子當(dāng)著我的面吹牛煌珊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播泌豆,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼定庵,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了踪危?” 一聲冷哼從身側(cè)響起蔬浙,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎贞远,沒想到半個月后畴博,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡兴革,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年绎晃,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片杂曲。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖袁余,靈堂內(nèi)的尸體忽然破棺而出擎勘,到底是詐尸還是另有隱情,我是刑警寧澤颖榜,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布棚饵,位于F島的核電站煤裙,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏噪漾。R本人自食惡果不足惜硼砰,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望欣硼。 院中可真熱鬧题翰,春花似錦、人聲如沸诈胜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽焦匈。三九已至血公,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間缓熟,已是汗流浹背累魔。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留够滑,地道東北人薛夜。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像版述,于是被迫代替她去往敵國和親梯澜。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內(nèi)容