遇到「最值問題」還在無腦動態規劃?二分法考慮一下唄

往西汪發表於2020-08-19

目錄

  1. 前言
  2. 二分法基礎及變種結構
  3. 小試牛刀
  4. 打怪升級
  5. 出師試煉

前言

一般來說,遇到「最值問題」通用的方法都是動態規劃,而有一類「最值問題」可以用其他方法更加巧妙、簡單方便的解決,這類問題的常見問法是「使……最大值儘可能小」。

這類問題也是大廠筆試面試常見題型,2020 年美團筆試題、位元組面試題中都出現過。

這類問題有三大特徵:

  1. 求最值(一般是最小值)
  2. 值的搜尋空間是線性的
  3. 對該值有一個限制條件,並且若 x 滿足條件,則 [x, +∞) 都滿足條件,反之 (-∞, x] 都不滿足條件。

你說的我都懂,可問題是,這是啥意思呢?叉會腰,冷靜一下
<center style="font-size:14px;color:#C0C0C0;text-decoration:underline">你說的我都懂,可問題是,這是啥意思呢?叉會腰,冷靜一下。</center>

為了方便大家理解這類問題到底是個什麼玩意兒,本汪在這裡列出 leetcode 上的一道題目作為例子:

給定一個非負整數陣列和一個整數 m,你需要將這個陣列分成 m 個非空的連續子陣列。設計一個演算法使得這 m 個子陣列各自和的最大值最小。

不熟悉二分法的同學初遇此題,看到關鍵詞“分割成 m 份”、“最小”,就想到了動態規劃。誠然此題具備了使用動態規劃的一切前提條件,也確實可以通過動態規劃做出來,但其時間複雜度為 $O(n^2×m)$ , 空間複雜度為 $O(n*m)$.

如果你看了本汪的這篇文章,學會了用二分法解決這一類問題,那麼時間複雜度可以優化到 $O(n * logx)$, 空間複雜度可以優化到 $O(1)$,並且思路非常簡潔,程式碼實現也極其簡單。

二分法基礎及變種結構

在講具體的方法之前,本汪先和大家一起回顧下二分法,熟悉二分法的同學可以直接跳過這部分。

最早接觸二分思想的地方應該是在二分搜尋中。(本質上類似快排中的 partition 思想)

二分法是在有序線性搜尋空間內,利用有序性,每次搜尋通過排除一半的剩餘搜尋空間,使 $O(n)$ 級別的線性搜尋優化到 $O(logn)$ 級別的方法。

image
<center style="font-size:14px;color:#C0C0C0;text-decoration:underline">二叉樹:我都會二分,不會還有人不會二分法吧?不會就讓往西汪那小子教你</center>

二分法的基本程式碼非常簡單,這裡就不多提了,下面用類 java 語言給出其針對「最值問題」的變種結構:

// nums 陣列是搜尋空間,m 是限制條件
// check(x, m) 表示在搜尋空間 nums 中的點 x,滿足了限制條件 m
public int binary(int[] nums, int m){  
      初始化 l, r 為搜尋空間的起始點和終點
    while(l < r){
        int mid = (l + r) >> 1; //二分,一次搜尋可以排除 mid 的左邊或者右邊(剩餘搜尋空間的一半)
        if(check(mid, m)) r = mid; //因為這一類最值問題要找的是最小,mid 滿足條件了,(mid, r] 就不是最小的答案了
        else l = mid + 1; // mid 不滿足條件,根據有序性,[l, mid] 裡的數全不滿足條件
    }
    return r;
}

根據結構我們可以看出,遇到這類問題的時候只需要找到「搜尋空間」、「檢查條件」,然後套用結構就能輕輕鬆鬆地解決問題。

下面就讓我們來用二分法解決剛剛提到的問題。

小試牛刀

題目

給定一個非負整數陣列和一個整數 m,你需要將這個陣列分成 m 個非空的連續子陣列。設計一個演算法使得這 m 個子陣列各自和的最大值最小。

思路

前文提及,解決本題只需要找到「搜尋空間」、「檢查條件」,剩下的就是閉著眼睛套結構的事兒了。

先找「搜尋空間」,因為此類問題的搜尋空間都是線性的,所以找到了起始點終點,也就找到了搜尋空間。

看問題描述 “子陣列各自和的最大值最小”,也就是說我們搜尋的點是 "子陣列各自和的最大值",那麼搜尋空間的起始點是陣列 nums 中的最大值,即 min(nums),搜尋空間的終點是陣列 nums 中的所有元素之和,即 sum(nums)。因此我們找到了搜尋空間,為 [min(nums), sum(nums)].

再看「檢查條件」。給定搜尋空間 nums,和點 x,判斷 x 是否滿足限制條件 m。

本題的條件是“子陣列各自和的最大值”,也就是說 x 和 m 個子陣列各自和相比較,都要大於或者等於它們。

「搜尋空間」、「檢查條件」都找到了,下面閉著眼睛套結構吧~

image
<center style="font-size:14px;color:#C0C0C0;text-decoration:underline">(瑟瑟發抖.jpg) 好的,這就上程式碼</center>

程式碼

class Solution {
      // 這個方法就是直接套結構
    public int splitArray(int[] nums, int m) {
        long l = 0, r = 0;
        for(int num: nums) {r += num; l = Math.max(l, num);} // 初始化 l 和 r
        while(l < r) {
            long mid = (l + r) >> 1;
            if(check(nums, mid, m)) r = mid;
            else l = mid + 1;
        }
        return (int)r;
    }
    
    public boolean check(int[] nums, long a, int m) { // 給定搜尋空間中的點 a,看它是否大於等於所有子陣列的各自和
        int cnt = 1;
        long sum = 0;
        for(int n: nums) {
            sum += n;
            if(sum > a) {
                sum = n;
                cnt ++;
                if(cnt > m) return false;
            }
        }
        return true;
    }
}

打怪升級

我們們再來看一道題,小夥伴們可以先自己思考,有思路的話自己動手實現下。再看看本汪給出的思路,加深理解。

題目

珂珂喜歡吃香蕉。這裡有 N 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警衛已經離開了,將在 H 小時後回來。

珂珂可以決定她吃香蕉的速度 K (單位:根/小時)。每個小時,她將會選擇一堆香蕉,從中吃掉 K 根。如果這堆香蕉少於 K 根,她將吃掉這堆的所有香蕉,然後這一小時內不會再吃更多的香蕉。

珂珂喜歡慢慢吃,但仍然想在警衛回來前吃掉所有的香蕉。

返回她可以在 H 小時內吃掉所有香蕉的最小速度 K(K 為整數)。

image

<center style="font-size:14px;color:#C0C0C0;text-decoration:underline">往西汪:我請你們吃香蕉,可以給我點個贊嗎</center>

思路

老規矩,先找「搜尋空間」,再找「檢查條件」,最後閉著眼睛套結構。

「搜尋空間」:H 小時內必須吃完 sum(piles) 根香蕉,所以初始點為 sum(piles) / H,一次最多隻能吃一堆,所以終點為 max(piles)。因此,搜尋空間為 [sum(piles) / H, max(piles)]

「檢查條件」:“在 H 小時內吃掉所有香蕉”,因此以 K 個 / 小時 的速度吃完香蕉的用時要小於等於 H。

下面就閉著眼睛套結構吧。

程式碼

class Solution {
    public int minEatingSpeed(int[] piles, int H) {
        int l = 1, r = 0; // 這裡本汪為了程式碼的簡潔性,在不影響時間複雜度的情況下,直接讓初始點為 1
        for(int i: piles) r = Math.max(r, i); // 一次最多吃一堆
        while(l < r){
            int mid = (l + r) >> 1;
            if(check(piles, mid, H)) r = mid;
            else l = mid + 1;
        }
        return r;
    }

    boolean check(int[] piles, int K, int H){
        int t = 0;
        for(double i: piles){
            t += Math.ceil(i / K); // 一堆香蕉共計 i 個,需要 ⌈i / K⌉ 個小時吃完
            if(t > H) return false; // 警衛回來了還沒吃完
        }
        return true;
    }
}

出師試煉

我非常喜歡看我文章的小夥伴,個個都是人才,說話又好聽,腦瓜子又聰明,還很主動的給我文章點贊。我相信砍翻兩個小怪之後,你們已經是這類問題的專家了,下面就給幾道題目供大夥隨便玩玩。

image
<center style="font-size:14px;color:#C0C0C0;text-decoration:underline">超喜歡往西汪的文章的</center>

關卡 1

你將會獲得一系列視訊片段,這些片段來自於一項持續時長為 T 秒的體育賽事。這些片段可能有所重疊,也可能長度不一。

視訊片段 clips[i] 都用區間進行表示:開始於 clips[i][0]並於 clips[i][1] 結束。我們甚至可以對這些片段自由地再剪輯,例如片段 [0, 7] 可以剪下成 [0, 1] + [1, 3] + [3, 7] 三部分。

我們需要將這些片段進行再剪輯,並將剪輯後的內容拼接成覆蓋整個運動過程的片段([0, T])。返回所需片段的最小數目,如果無法完成該任務,則返回 -1 。

關卡 2

牛牛有 n 件帶水的衣服,乾燥衣服有兩種方式。

一、是用烘乾機,可以每分鐘烤乾衣服的 k 滴水。

二、是自然烘乾,每分鐘衣服會自然烘乾 1 滴水。

烘乾機比較小,每次只能放進一件衣服。

注意,使用烘乾機的時候,其他衣服仍然可以保持自然烘乾狀態,現在牛牛想知道最少要多少時間可以把衣服全烘乾。

關卡 3

你太強了!我已經沒有更多的題目給你玩了。你可以憑藉這套武功闖蕩江湖了!

……

……

……

等等,我說笑呢。你這小身板,要不先進我的其他訓練營再練練唄?

啥,你問我的其他訓練營在哪裡?動動小手,點個關注,新的訓練營已經在建設了,嘿嘿。

咳咳,暗示的很明顯了
<center style="font-size:14px; color:#C0C0C0">咳咳,暗示的很明顯了</center>

相關文章