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

目錄

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

前言

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

這類問題也是大廠筆試面試常見題型,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)$,而且思路很是簡潔,代碼實現也極其簡單。spa

二分法基礎及變種結構

在講具體的方法以前,本汪先和你們一塊兒回顧下二分法,熟悉二分法的同窗能夠直接跳過這部分。設計

最先接觸二分思想的地方應該是在二分搜索中。(本質上相似快排中的 partition 思想)code

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

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

二分法的基本代碼很是簡單,這裏就很少提了,下面用類 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 件帶水的衣服,乾燥衣服有兩種方式。

1、是用烘乾機,能夠每分鐘烤乾衣服的 k 滴水。

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

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

注意,使用烘乾機的時候,其餘衣服仍然能夠保持天然烘乾狀態,如今牛牛想知道最少要多少時間能夠把衣服全烘乾。

關卡 3

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

……

……

……

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

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

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

相關文章
相關標籤/搜索