雙周賽110。可能會是全站第二難的題,只有41人通過,太扯了。
而且明明測資範圍才1000,結果O(N^2)空間還會MLE,沒優化沒辦法過。

2023-08-14更新:本題分數高達2978,差點搶下全站最高難度寶座。

題目

輸入兩個長度相同的整數陣列nums1和nums2。 每一秒鐘,對於所有滿足 0 <= i < nums1.length的索引i,將nums1[i]的值增加nums2[i]。然後你可以執行以下操作:

  • 選擇滿足 0 <= i < nums1.length的索引i,使nums1[i] = 0

另外還有另外還有給定整數x。

最少需要多少時間,才能使得nums1的元素和小於等於x。若不可能則回傳-1。

解法

對於每個nums1[i]來說,每一秒都會增加num2[i]。如果同一個索引i被清除超過一次,則第二次清除時,除了nums[i]同樣為0之外,其他所有位置的值只可能變得更大,不會變小。
因此得出結論:每個索引最多只會被清除一次,總共有N個位置,所以答案最大是N秒。

題目要求的是最少的時間,可以從小到大依序檢查,在t秒鐘清除t個不同索引後的元素和。
如果直接求元素和很麻煩,還要額外維護哪些i選過,非常難處理。 但是可以算出t秒時nums1未經修改的成長值,只要計算選t個索引可以清除的最大元素和,從成長值中扣掉後得到第t秒的最小元素和。

假設nums1初始值總和為sm1,且nums2總和為sm2:

在第t秒時,若沒有經過清除,則nums1的元素和會變成sm1 + sm2*t。
選t個不同的索引,清除後減少的值總共是nums1[i1] + nums2[i1]*t + nums1[i2] + nums2[i1]*(t-1) + … 例如t=2,我們選擇兩個索引i,j分別在第1秒和第2秒清除
可清除的值就是nums1[i] + nums2[i]*1 + nums1[j] + nums2[j]*2

可以發現,若nums2[i]的值越大、成長速率越快,越晚清除越好。先把nums1和nums2封裝,並按照nums2的值排序。
那如何找到選t個索引的最大值?其實就變成簡單的背包問題,在前i個物品中選擇t個,決定第i個選或不選。

定義dp(i,t):在前i個索引中,選擇t個索引的最大清除元素和。
轉移方程式:dp(i,t) = max(選i, 不選i),不選的話就是dp(i-1,t);選的話就是dp(i-1,t-1)+nums1[i]+num2[i]*t。
base cases:當i<0代表沒東西選了,回傳0;或是t=0時沒有選擇次數了,也回傳0。

最後從0秒開始往上加,看哪秒可以讓元素和降到x,就是答案。
注意:是從0秒開始,有可能一開始nums1總和就小於x。
注意2:記憶體限制很嚴苛,動態規劃時,若需要選的次數t大於可選數量i+1,全都是無用的狀態,要剪枝才能通過。

時間複雜度O(N^2)。
空間複雜度O(N^2)。

class Solution:
    def minimumTime(self, nums1: List[int], nums2: List[int], x: int) -> int:
        N=len(nums1)
        sum1=sum(nums1)
        sum2=sum(nums2)
        arr=sorted(zip(nums1,nums2),key=itemgetter(1))
        
        @cache
        def dp(i,t): # nums2[0,i], we can clear t more times
            if t>i+1: # pruning
                return dp(i,i+1)
            if i<0 or t==0: 
                return 0
            res=dp(i-1,t) # no take
            res=max(res,dp(i-1,t-1)+arr[i][0]+arr[i][1]*t) # clear nums[i] at j-th second
            return res
        
        for t in range(N+1):
            if sum1+sum2*t-dp(N-1,t)<=x:
                return t
            
        return -1

改成遞推版本,沒有空間優化的版本就可以過,而且記憶體用量少很多。

class Solution:
    def minimumTime(self, nums1: List[int], nums2: List[int], x: int) -> int:
        N=len(nums1)
        sum1=sum(nums1)
        sum2=sum(nums2)
        arr=sorted(zip(nums1,nums2),key=itemgetter(1))
        
        dp=[[0]*(N+1) for _ in range(N+1)]
        for i in range(1,N+1):
            for t in range(1,N+1):
                dp[i][t]=max(
                    dp[i-1][t],
                    dp[i-1][t-1]+arr[i-1][0]+arr[i-1][1]*t
                )
                
        for t in range(N+1):
            if sum1+sum2*t-dp[N][t]<=x:
                return t
            
        return -1

這轉移方程式其實和01背包差不多,可以壓縮掉一個維度。

時間複雜度O(N^2)。
空間複雜度O(N)。

class Solution:
    def minimumTime(self, nums1: List[int], nums2: List[int], x: int) -> int:
        N=len(nums1)
        sum1=sum(nums1)
        sum2=sum(nums2)
        arr=sorted(zip(nums1,nums2),key=itemgetter(1))
        
        dp=[0]*(N+1)
        for i in range(1,N+1):
            for t in reversed(range(1,N+1)):
                dp[t]=max(
                    dp[t],
                    dp[t-1]+arr[i-1][0]+arr[i-1][1]*t
                )
                
        for t in range(N+1):
            if sum1+sum2*t-dp[t]<=x:
                return t
            
        return -1