LeetCode(力扣) 1648. 銷售價值減少的顏色球,完美結合二分演算法和貪心演算法
一個二分查詢演算法和貪心演算法結合的場景
之所以寫這個,是因為我前兩週在參加 LeetCode
周賽的時候,碰到了一個這樣題,題目連結如下:
1648. 銷售價值減少的顏色球
1648. 銷售價值減少的顏色球
你有一些球的庫存
inventory
,裡面包含著不同顏色的球。一個顧客想要 任意顏色 總數為orders
的球。
這位顧客有一種特殊的方式衡量球的價值:每個球的價值是目前剩下的 同色球 的數目。比方說還剩下
6
個黃球,那麼顧客買第一個黃球的時候該黃球的價值為6
。這筆交易以後,只剩下5
個黃球了,所以下一個黃球的價值為5
(也就是球的價值隨著顧客購買同色球是遞減的)。
給你整數陣列
inventory
,其中inventory[i]
表示第i
種顏色球一開始的數目。同時給你整數orders
,表示顧客總共想買的球數目。你可以按照 任意順序 賣球。
請你返回賣了
orders
個球以後 最大 總價值之和。由於答案可能會很大,請你返回答案對10 ** 9 + 7
取餘數 的結果。
示例 1:
輸入:inventory = [2,5], orders = 4
輸出:14
解釋:賣 1 個第一種顏色的球(價值為 2 ),賣 3 個第二種顏色的球(價值為 5 + 4 + 3)。
最大總和為 2 + 5 + 4 + 3 = 14 。
提示
1 <= inventory.length <= 10 ** 5
1 <= inventory[i] <= 10 ** 9
1 <= orders <= min(sum(inventory[i]), 10 ** 9)
分析
剛開始我完全沒有意識到有二分查詢的思想,就是想著用一個優先佇列,然後每次取出一個元素,加上當前值,把當前值減一,然後再把當前值放入優先佇列。
沒過幾分鐘,程式就寫完了,但是呢提交後顯示執行超時了,我就想著去優化程式。於是我又讀了下題,看看是不是我漏了啥重要條件,結果讀了幾遍發現這道題就是貪心的思想啊,不可能錯的呀,但是結果就是超時了。。。
於是周賽結束後我特意去查了下大神寫的程式碼,真的是讓我驚呆了,是貪心的思想沒錯,但是時二分和貪心進行結合。
這個題想法很簡單,設定一個 sum
變數,sum
每次加上陣列中的最大值,然後將當前值減去 1
,直到此過程重複 orders
步驟。
然後為啥用優先佇列會超時呢?其實優先佇列的底層就是堆,每次取出元素,加入元素都需要對堆進行調整,調整的時間複雜度是 O(logn)
,其中 n
是優先佇列的長度,然後需要進行 orders
操作,所以總的時間複雜度就是 O(Nlogn)
, 其中 N
是 orders
, n
是陣列長度。
而這道題的話,orders
會取到 10 ** 9
,所以自然而然就會超時了。
那麼應該如何解決呢?
解決思路
既然單獨使用優先佇列解決不了問題,那我們就換個思路進行思考。因為每次都要取陣列中的最大值,然後減去 1
, 所以最後呢陣列中的元素肯定是小於等於某一個閾值的,這個我想你肯定是能夠理解的。
那這個閾值能不能求出來呢?如果能求出來的話,那問題是不是就容易解決了呢?你想啊,如果現在我們已經求出了這個閾值,那麼是不是就知道了陣列中的每個元素被減了多少次,進而累加求和不就得到結果了嘛。
好,現在問題已經變成了如何求解閾值了,這個如何求解呢?我們假定閾值為 threshold
,那麼它滿足啥條件呢?
∑ a i > t h r e s h o l d a i − t h r e s h o l d < = orders < ∑ a i > t h r e s h o l d a i - threshold + 1 \sum_{a_{i}>t h r e s h o l d} a_{i}-t h r e s h o l d<=\text { orders }<\sum_{a_{i}>t h r e s h o l d} a_{i} \text { - threshold }+1 ai>threshold∑ai−threshold<= orders <ai>threshold∑ai - threshold +1
怎麼理解呢?
就是說當前值是 threshold
時,
∑
a
i
>
t
h
r
e
s
h
o
l
d
a
i
−
t
h
r
e
s
h
o
l
d
\sum_{a_{i}>t h r e s h o l d} a_{i}-t h r e s h o l d
∑ai>thresholdai−threshold 就代表了所有的操作的次數,它應該是小於等於 orders
的,為啥呢?拿例 1
舉例,陣列為 [2, 5]
, orders
為 4
,相當於要進行四次操作。
5
減1
,變為4
4
減1
,變為3
3
減1
,變為2
- 剩下兩個
2
,2
減1
,變為1
此時所有陣列中的元素都小於等於 2
,所以這個例子中的 threshold
就是 2
。但是
∑
a
i
>
t
h
r
e
s
h
o
l
d
a
i
−
t
h
r
e
s
h
o
l
d
\sum_{a_{i}>t h r e s h o l d} a_{i}-t h r e s h o l d
∑ai>thresholdai−threshold 有可能出現小於 orders
的情況,比如文中的這個例子,這時候又需要當前陣列中的部分元素再減去 1
,但是又不能所有元素都減去 1
,如果那樣的話, threshold
就會改變了。
因為這個函式是一個單調遞減的函式,所以存在唯一的 threshold
,滿足上個式子。所以問題就轉化為了在 0
和 10 ** 9
之間查詢最小的 threshold
,使得
∑
a
i
>
t
h
r
e
s
h
o
l
d
a
i
−
t
h
r
e
s
h
o
l
d
\sum_{a_{i}>t h r e s h o l d} a_{i}-t h r e s h o l d
∑ai>thresholdai−threshold
看到了嗎?這個問題就轉化為了上篇文章中我們提到的二分演算法的變體問題,沒理解的話,你品,你再品。
然後
orders -
∑
a
i
>
threshold
a
i
-threshold
\text { orders - } \sum_{a_{i}>\text { threshold }} a_{i} \text { -threshold }
orders - ∑ai> threshold ai -threshold 就表示了陣列中元素還需要再減 1
的次數。
這個問題到這裡就解決了,接下來看看程式碼吧,這裡面還有很多騷操作,保證出乎你的意外。
程式碼實現
class Solution:
def maxProfit(self, inventory: List[int], orders: int) -> int:
max_num = 10 ** 9 + 7
res = 0
low, high = 0, 10 ** 9
threshold = None
while low <= high:
mid = low + ((high - low) >> 1)
temp = 0
for i in range(len(inventory)):
if inventory[i] >= mid:
temp += (inventory[i] - mid)
if temp <= orders:
threshold = mid
high = mid - 1
else:
low = mid + 1
temp = 0
for i in range(len(inventory)):
if inventory[i] > threshold:
temp += (inventory[i] - threshold)
temp = orders - temp
for i in range(len(inventory)):
if inventory[i] >= threshold:
if temp > 0:
# 等差數列求和公式
res += ((inventory[i] + threshold) * (inventory[i] - (threshold) + 1) // 2)
temp -= 1
else:
# 等差數列求和公式
res += ((inventory[i] + threshold + 1) * (inventory[i] - (threshold + 1) + 1) // 2)
return res % max_num
這個是我今天下午剛寫的,前一部分是二分查詢的實現,後面是累加求和的過程。
不知道最後一個 for
迴圈你看懂了沒?在計算 res
的過程中,運用到了等差數列的求和公式,我當時在看別人程式碼的時候是一臉懵逼的,當時想著計算的話不是應該有迴圈的嗎?為啥沒有迴圈呢?沒有迴圈怎麼計算從 a[i]
到 threshold
呢?突然間恍然大悟,不得不佩服大神。
總結
現在回過頭來看,這道題的思想已經很正常了,但是當我在參加 LeetCode 周賽的時候,當時心態都被它給搞崩了。。。
你看提到二分查詢演算法的話,我想每個人都知道,提及貪心演算法,每個人也都有話可說,但是二者結合起來,就讓很多人摸不著頭腦了。
當然,這並不是說我們之前學的演算法知識沒有用,而是我們缺乏一種融會貫通的思維,在學習的過程中,要學會舉一反三。
這道題帶給我的不僅僅是知識點的融會貫通,更讓我驚訝的是數學知識的使用,沒有刻意的地方,一切是那麼的自然。
我們每個人學數學的話也都學了好多年,但是更多的是用來考試,真正在程式設計過程的使用時很少的。
雖然那次周賽我只做了一道題,但是感覺收穫是很大的,開闊了眼界,擴充了思維。
如果你對程式設計也感興趣的話,歡迎關注我的微&&信&&公&&眾&&號「與你一起學演算法」,我們共同交流進步。
一個人可以走的很快,但一群人可以走的更遠,共勉。
相關文章
- 【力扣】最大子陣列和(貪心)力扣陣列
- 力扣 leetcode 435. 無重疊區間 貪心力扣LeetCode
- 貪心演算法演算法
- 力扣--連結串列演算法力扣演算法
- 貪心演算法(貪婪演算法,greedy algorithm)演算法Go
- 資料結構與演算法——貪心演算法資料結構演算法
- 演算法---貪心演算法和動態規劃演算法動態規劃
- 常用演算法之貪心演算法演算法
- 【LeetCode】貪心演算法–分發糖果(135)LeetCode演算法
- LeetCode解題記錄(貪心演算法)(二)LeetCode演算法
- LeetCode解題記錄(貪心演算法)(一)LeetCode演算法
- leetcode:跳躍遊戲II(java貪心演算法)LeetCode遊戲Java演算法
- 學一下貪心演算法-學一下貪心演算法演算法
- 貪心演算法Dijkstra演算法
- 9-貪心演算法演算法
- 快速冪演算法(二分思想減少連乘次數)演算法
- leetcode410分割陣列的最大值(二分+貪心,困難)LeetCode陣列
- 演算法基礎–貪心策略演算法
- Moving Tables(貪心演算法)演算法
- Python演算法:貪心策略Python演算法
- leetcode1552題解【二分+貪心】LeetCode
- 二分圖的最大匹配、完美匹配和匈牙利演算法演算法
- 二分與貪心-Gone Fishing(演算法基礎 第9周)Go演算法
- [Coursera]演算法基礎_Week8_二分與貪心_Q1演算法
- [Coursera]演算法基礎_Week8_二分與貪心_Q3演算法
- [Coursera]演算法基礎_Week8_二分與貪心_Q2演算法
- 貪心演算法——換酒問題演算法
- Rabbit加密演算法:效能與安全的完美結合加密演算法
- 演算法基礎 - 列舉/遞迴/動歸/深廣搜/二分/貪心演算法遞迴
- 五大常用演算法之三:貪心演算法演算法
- 演算法--力扣27. 移除元素演算法力扣
- 野生前端的資料結構練習(12)貪心演算法前端資料結構演算法
- 加油站問題(貪心演算法)演算法
- 貪心演算法篇——區間問題演算法
- 求最大子陣列(貪心演算法)陣列演算法
- 《演算法筆記》9. 培養貪心思維、貪心演算法深度實踐演算法筆記
- 力扣演算法:LC 704-二分查詢,LC 27-移除元素--js力扣演算法JS
- 貪心演算法-找不重疊的區間段演算法