雙周賽122。這題大概也算是腦筋急轉彎,快把我搞吐血。

題目

輸入正整數陣列 nums。

你可以執行以下操作任意(包含零)次,並將 nums 的長度最小化

  • 選擇兩個不同的索引 i, j,滿足 nums[i] > 0 且 nums[j] > 0
  • 將 nums[i] % nums[j] 的結果加入 nums 尾端
  • 刪除 nums[i] 和 nums[j]

求任意次操作後,nums 的最小長度

解法

分類討論選擇 a, b 兩數做 a % b 的情形:

  • 若 a < b,一定得到 a
  • 若 a >= b:
    • a 是 b 的倍數,得到 0
    • a 不是 b 的倍數,得到 a % b

整理後發現,只要存在不同大小的數,則一定可以刪除較大者
最後剩下的幾個相同的數,倆倆一組變成 0,不成對的則維持不變。
例如:

[1,1,1,2,2,3,4]
先透過 1 把較大的數都刪掉,得到 [1,1,1]
剩下的 1 倆倆成對變成 0,得到 [0,1]
落單的 1 能留著不變,答案最小長度是 2

若最後剩下 cnt 個相同的最小值,操作則會剩下 (cnt + 1) / 2 個。
大功告成,提交答案。得到一個免費的 WA。


試想以下例子:

nums = [15,15,15,21]
剛才的作法會得到 [0,15],最小長度是 2。

但正確作法是:

選 21 % 15,得到 [15,15,15,6]
然後刪掉較大者,得到 [6]
最小長度 1

我靠,這個 6 是哪來的?仔細想想這個 a % b 好像有點眼熟,正是輾轉相除法
只要求整個 nums 的 gcd,就可以知道透過操作得到的最小值g,進而把其他大於 g 的數全刪掉。

這下又有兩種情況:

  • nums 中原本不存在 g,那全部的元素都會被刪掉,只剩下一個 g
  • nums 原本就有 g,那就先刪掉大的,剩下的倆倆相消

最終答案是 max(1, (cnt+1) / 2)。


對於 a, b 兩數求 gcd 的時間複雜度是 O(log min(a, b)),總共需要求 N-1 次。
但根據元素順序不同,如果由大到小求 gcd,則複雜度會稍微超過 O(N * log min(nums)),乾脆以最大值計算。

時間複雜度 O(N * log MX),其中 MX 是 max(nums)。
空間複雜度 O(1)。

class Solution:
    def minimumArrayLength(self, nums: List[int]) -> int:
        g = gcd(*nums)
        cnt = nums.count(g)
        
        return max(1, (cnt+1) // 2)

換個角度想,其時也不一定要是 gcd。
若原本 nums 中的最小值是 mn,只要隨便搞出一個小於 mn 的數,都可以把 nums 刪到剩下一個元素。

遍歷 nums,若存在任何一個不是 mn 倍數的元素 x,則餘數必定不為 0 且小於 mn。答案是 1。
否則只能剩下 cnt 個 mn 相消,答案 (cnt+1) / 2。

時間複雜度 O(N)。
空間複雜度 O(1)。

class Solution:
    def minimumArrayLength(self, nums: List[int]) -> int:
        mn = min(nums)
        for x in nums:
            if x % mn != 0:
                return 1
            
        cnt = nums.count(mn)
            
        return (cnt+1) // 2