題目描述:給定一個已排序的正整數陣列 nums 和一個正整數 n。可以向陣列中新增任意正整數,使得 [1, n] 範圍內的每個整數都能由陣列的某個子集的和表示。求最少需要新增幾個數。
解題思路:
核心不變量(Invariant):維護變數 reach,表示當前子集和能覆蓋的連續範圍 [1, reach]。初始 reach = 0,意味著什麼都覆蓋不了。
可達範圍的擴展規則:若目前可以表示 [1, reach] 內所有數,當我們加入一個新數 x(x ≤ reach + 1),則可以表示的範圍擴展至 [1, reach + x]。這是因為任何在 [1, reach] 內的數原本就可達,加上 x 後,[x+1, reach+x] 內的每個數也可以用「原子集 + x」表示。
貪心策略:
逐一考慮 nums[i]:
nums[i] 有用,直接合併:reach += nums[i]。reach + 1 這個數無法被表示,出現「覆蓋漏洞」。此時必須新增一個數,最優選擇是 reach + 1 本身(能最大化覆蓋範圍),reach 更新為 2 * reach + 1,patch 計數加一,並不消耗 nums[i](它可能在之後有用)。重複上述過程,直到 reach >= n 為止。
為什麼 patch reach+1 最優?若選擇任何小於 reach+1 的數,覆蓋範圍相同但浪費了一次 patch 機會;若選擇大於 reach+1 的數,reach+1 仍然是漏洞,patch 無效。所以 reach+1 是唯一最優選擇。
時間複雜度:O(m + log n) — 其中 m 為 nums 的長度。遍歷 nums 需 O(m);每次 patch 後 reach 至少翻倍,故最多 patch O(log n) 次。總體 O(m + log n)。
空間複雜度:O(1) — 只使用常數個額外變數(reach、patches、i)。注意 reach 需使用 long long,因為 2 * reach + 1 在過程中可能超過 int 範圍。
函數 minPatches(nums, n):
reach = 0 (目前可覆蓋 [1, reach])
patches = 0 (新增數字個數)
i = 0 (nums 的指針)
當 reach < n 時重複:
若 i < nums 長度 且 nums[i] <= reach + 1:
情況一:直接使用 nums[i]
reach = reach + nums[i]
i = i + 1
否則:
情況二:漏洞,patch 最優數字 reach+1
reach = 2 * reach + 1
patches = patches + 1
回傳 patches定義 dp[s] = true 表示和為 s 可以由子集達到。對每個加入的數更新 dp 表。
時間 O(n × m),空間 O(n)。當 n ≤ 10⁵ 時尚可,但本題 n 可達 2³¹ - 1,完全不可行。
二分搜尋最終需要 patch 的次數 ans,對每個候選答案驗證是否可行。
但驗證「patch ans 次能否覆蓋 [1, n]」本身的邏輯和貪心幾乎相同,且多了二分外層,不如直接用貪心。時間複雜度反而更差。
可以用數學歸納法嚴格證明貪心的最優性:
若已可覆蓋 [1, r],任何可行方案必須包含某個能覆蓋 r+1 的元素 x(x ≤ r+1)。選 x = r+1 是所有合法選擇中能最大化新覆蓋範圍的,因此貪心選擇不劣於任何最優方案。此為「交換論證(Exchange Argument)」的標準應用。
回傳實際新增的數:本題只要求新增數的個數,若要求同時輸出最優的新增方案(即具體的數字列表),應如何修改演算法?答案是在每次 patch 時記錄 reach + 1 即可,因為貪心策略唯一確定了每次 patch 的最佳選擇。
乘積覆蓋版本:若改為要求 [1, n] 內每個數都能作為子集的乘積,問題如何變化?乘積沒有類似「覆蓋範圍單調擴展」的性質,需要完全不同的方法。
加權子集和:若每個數有使用次數限制(例如每個數最多用 c 次),問題成為有限制的硬幣找零問題(Coin Change 變形),貪心不再適用,需要 DP 解決。