- 看到这个问题,第一反应应该是用贪心算法,每一次都选最大的面值,AI给的思路提示一开始也是用贪心算法
-
解题思路
-
理解问题:
- 我们需要用最少数量的硬币凑出给定的总金额。
- 硬币的面值和数量是无限的。
-
贪心策略:
- 从面值最大的硬币开始,尽可能多地使用该硬币。
- 如果当前硬币的面值大于剩余金额,则尝试使用下一个面值较小的硬币。
- 重复这个过程,直到凑出总金额。
-
数据结构选择:
- 使用一个列表来存储硬币的面值。
- 使用一个变量来记录当前剩余的总金额。
-
算法步骤:
- 对硬币面值列表进行降序排序。
- 初始化一个空列表来存储结果。
- 遍历排序后的硬币面值列表,对于每个面值,尽可能多地使用该硬币,直到剩余金额小于该面值。
- 如果剩余金额为0,则返回结果列表;否则,返回一个表示无法凑出总金额的特殊值(例如空列表或
None
)。
- 但是会存在一个问题,贪心算法并不一定会求得最优解,每次局部最优并不一定最后是全局最优。比如第二个测试用例,硬币是[1,3,4],总数是6,按照贪心算法的话就会求得[4,1,1],而最优解应该是[3,3]。然后跟AI说了一下,于是AI重新提出使用动规来求解。 动态规划可以确保我们在所有可能的组合中找到使用最少硬币的方案。
动态规划思路
-
定义状态:
- 使用一个数组
dp
,其中dp[i]
表示凑出金额i
所需的最少硬币数量。
- 使用一个数组
-
初始化:
dp[0] = 0
,因为凑出金额0
不需要任何硬币。- 对于其他金额,初始化为一个较大的值(例如
float('inf')
),表示初始状态下无法凑出该金额。
-
状态转移:
-
对于每个硬币面值
coin
,更新dp
数组:dp[i] = min(dp[i], dp[i - coin] + 1)
,表示使用当前硬币后,凑出金额i
所需的最少硬币数量。
-
-
结果:
- 如果
dp[amount]
仍然是初始值(例如float('inf')
),则表示无法凑出总金额,返回[]
。 - 否则,根据
dp
数组回溯找到使用的硬币组合。
- 如果
def solution(coins, amount):
# 初始化dp数组,dp[i]表示凑出金额i所需的最少硬币数量
dp = [float('inf')] * (amount + 1)
dp[0] = 0 # 凑出金额0不需要任何硬币
# 动态规划计算最少硬币数量
for coin in coins:
for i in range(coin, amount + 1):
dp[i] = min(dp[i], dp[i - coin] + 1)
# 如果dp[amount]仍然是初始值,表示无法凑出总金额
if dp[amount] == float('inf'):
return []
# 回溯找到使用的硬币组合
result = []
while amount > 0:
for coin in coins:
if amount >= coin and dp[amount] == dp[amount - coin] + 1:
result.append(coin)
amount -= coin
break
return result
if __name__ == "__main__":
# 添加你的测试用例
print(solution([1, 2, 5], 18) == [5, 5, 5, 2, 1])
print(solution([1, 3, 4], 6) == [3, 3])
print(solution([5], 10) == [5, 5])
使用动规就能通过案例,但有一点需要注意,测试样例的输出是从大到小排列,虽然题目中并没有要求,但没按顺序排列列表也会判定错误。
时间复杂度分析:
- 动态规划部分: 每次遍历硬币面值时,对每个金额进行更新,因此时间复杂度是 O(n×m),其中
n
是目标金额amount
,m
是硬币的种类数。 - 回溯部分: 最多需要遍历所有硬币面值一次,因此时间复杂度是 O(m×k),其中
k
是最少硬币数量(即返回的硬币数量)。
总体来说,时间复杂度是 O(n×m+m×k),这对于一般的硬币问题来说是可接受的。