[Leetcode解題] 3592. Inverse Coin Change
12 September 2026
medium
dp
dynamic-programming
greedy
knapsack
3592. Inverse Coin Change
題目
給定一個長度為 $n-1$ 的整數陣列 numWays,其中 numWays[i] 表示構造出總金額為 $i + 1$ 的硬幣組合數(組合中不考慮硬幣順序)。
請推算出構造出此 numWays 陣列的硬幣面額列表 coins。如果無法找到一組硬幣使其構造出的組合數與 numWays 完全吻合,則回傳空陣列 []。
解題思路
本題屬於逆向推導(Inverse Problem)結合完全背包動態規劃(Unbounded Knapsack DP)的經典題目。
題目給定每個金額 $1, 2, \dots, n-1$ 的合法組合數 numWays[i],我們需要由小到大逐一判斷每個金額 $s = i + 1$ 是否為一枚全新的硬幣面額。
數學與規律觀察
假設我們維護一個動態規劃陣列 dp,其中 dp[s] 代表僅使用目前已經確定的硬幣集合,湊出金額 $s$ 的組合數。
當我們從小到大遍歷到金額 $s = i + 1$ 時:
- 僅用較小硬幣:所有只包含面額 $< s$ 的硬幣組合,其湊出金額 $s$ 的方法數已經被完全計算在
dp[s](即dp[i+1])中。 - 包含面額 $s$ 的硬幣:如果硬幣集合中包含面額 $s$ 的硬幣,由於硬幣面額皆為正整數,使用一枚面額 $s$ 的硬幣後,剩餘金額為 $s - s = 0$。湊出金額 0 的方法數固定為 $\text{dp}[0] = 1$(即什麼都不選)。
- 不可能使用 $> s$ 的硬幣:因為硬幣面額大於金額 $s$,無法用來湊出金額 $s$。
因此,對於金額 $s = i + 1$:
- 湊出金額 $s$ 的總方法數只可能為: \(\text{numWays}[i] = \text{dp}[i+1] \quad (\text{無面額 } s \text{ 的硬幣})\) 或 \(\text{numWays}[i] = \text{dp}[i+1] + 1 \quad (\text{存在面額 } s \text{ 的硬幣})\)
貪心與 DP 狀態更新
觀察 numWays[i] - dp[i+1] 的值:
numWays[i] - dp[i+1] == 0:說明單靠目前已知的較小硬幣就已經剛好湊出numWays[i]種方法。因此不需要面額為 $s = i + 1$ 的硬幣,直接檢查下一個金額(i += 1)。numWays[i] - dp[i+1] == 1:說明目前已知的硬幣組合數比目標少 1 種,這代表必須存在一枚面額為 $s = i + 1$ 的硬幣。我們將 $s$ 加入coins列表中,並透過完全背包 DP 更新dp陣列: \(\text{dp}[j] = \text{dp}[j] + \text{dp}[j - s] \quad (\forall j \in [s, n-1])\) 更新完後,保持當前i繼續檢查(若有多枚相同面額硬幣可連續處理,直至numWays[i] - dp[i+1] == 0時才增長i)。numWays[i] - dp[i+1] != 0且numWays[i] - dp[i+1] != 1:說明目前已知硬幣組合數與目標不符合,且無法透過新增硬幣來修正,因此無解,直接回傳[]。
Python 實作
class Solution:
def findCoins(self, numWays: List[int]) -> List[int]:
coins = []
n = len(numWays) + 1
dp = [0 for i in range(n)]
dp[0] = 1
i = 0
while i < len(numWays):
if numWays[i] - dp[i+1] == 0:
i += 1
continue
if numWays[i] - dp[i+1] != 1:
return []
coin = i + 1
coins.append(coin)
for j in range(coin, n):
dp[j] = dp[j] + dp[j-coin]
return coins
複雜度分析
- 時間複雜度:$O(n \times m)$
其中 $n = \text{len(numWays)} + 1$,$m$ 為最終找到的硬幣數量
len(coins)。每次確定一枚硬幣面額時,更新 DP 陣列需要 $O(n)$ 的時間,最多更新 $m$ 次。 - 空間複雜度:$O(n)$
維護長度為 $n$ 的一維
dp陣列以及記錄結果的coins列表。