LeetCode(力扣) 1648. 銷售價值減少的顏色球,完美結合二分演算法和貪心演算法

努力努力再努力Xmn發表於2020-11-28

一個二分查詢演算法和貪心演算法結合的場景

之所以寫這個,是因為我前兩週在參加 LeetCode 周賽的時候,碰到了一個這樣題,題目連結如下:
1648. 銷售價值減少的顏色球

1648. 銷售價值減少的顏色球

你有一些球的庫存 inventory ,裡面包含著不同顏色的球。一個顧客想要 任意顏色 總數為 orders 的球。

這位顧客有一種特殊的方式衡量球的價值:每個球的價值是目前剩下的 同色球 的數目。比方說還剩下 6 個黃球,那麼顧客買第一個黃球的時候該黃球的價值為 6 。這筆交易以後,只剩下 5 個黃球了,所以下一個黃球的價值為 5 (也就是球的價值隨著顧客購買同色球是遞減的)。

給你整數陣列 inventory ,其中 inventory[i] 表示第 i 種顏色球一開始的數目。同時給你整數 orders ,表示顧客總共想買的球數目。你可以按照 任意順序 賣球。

請你返回賣了 orders 個球以後 最大 總價值之和。由於答案可能會很大,請你返回答案對 10 ** 9 + 7 取餘數 的結果。

示例 1:
alt test

輸入: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), 其中 Nordersn 是陣列長度。

而這道題的話,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>thresholdaithreshold<= orders <ai>thresholdai - 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>thresholdaithreshold 就代表了所有的操作的次數,它應該是小於等於 orders的,為啥呢?拿例 1 舉例,陣列為 [2, 5], orders4,相當於要進行四次操作。

  1. 51,變為 4
  2. 41,變為 3
  3. 31,變為 2
  4. 剩下兩個 2, 21,變為 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>thresholdaithreshold 有可能出現小於 orders 的情況,比如文中的這個例子,這時候又需要當前陣列中的部分元素再減去 1,但是又不能所有元素都減去 1,如果那樣的話, threshold 就會改變了。

因為這個函式是一個單調遞減的函式,所以存在唯一的 threshold,滿足上個式子。所以問題就轉化為了在 010 ** 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>thresholdaithreshold

看到了嗎?這個問題就轉化為了上篇文章中我們提到的二分演算法的變體問題,沒理解的話,你品,你再品。

然後  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 周賽的時候,當時心態都被它給搞崩了。。。

你看提到二分查詢演算法的話,我想每個人都知道,提及貪心演算法,每個人也都有話可說,但是二者結合起來,就讓很多人摸不著頭腦了。

當然,這並不是說我們之前學的演算法知識沒有用,而是我們缺乏一種融會貫通的思維,在學習的過程中,要學會舉一反三。

這道題帶給我的不僅僅是知識點的融會貫通,更讓我驚訝的是數學知識的使用,沒有刻意的地方,一切是那麼的自然。

我們每個人學數學的話也都學了好多年,但是更多的是用來考試,真正在程式設計過程的使用時很少的。

雖然那次周賽我只做了一道題,但是感覺收穫是很大的,開闊了眼界,擴充了思維。

如果你對程式設計也感興趣的話,歡迎關注我的微&&信&&公&&眾&&號與你一起學演算法」,我們共同交流進步。

一個人可以走的很快,但一群人可以走的更遠,共勉。

相關文章