目錄
前言
一般來說,遇到「最值問題」通用的方法都是動態規劃,而有一類「最值問題」可以用其他方法更加巧妙、簡單方便的解決,這類問題的常見問法是「使……最大值儘可能小」。
這類問題也是大廠筆試面試常見題型,2020 年美團筆試題、位元組面試題中都出現過。
這類問題有三大特徵:
- 求最值(一般是最小值)
- 值的搜尋空間是線性的
- 對該值有一個限制條件,並且若
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)$ 級別的方法。
<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 個子陣列各自和相比較,都要大於或者等於它們。
「搜尋空間」、「檢查條件」都找到了,下面閉著眼睛套結構吧~
<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 為整數)。
<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;
}
}
出師試煉
我非常喜歡看我文章的小夥伴,個個都是人才,說話又好聽,腦瓜子又聰明,還很主動的給我文章點贊。我相信砍翻兩個小怪之後,你們已經是這類問題的專家了,下面就給幾道題目供大夥隨便玩玩。
<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>