题目:给你 k 种面值的硬币,面值分别为 c1, c2 ... ck,再给一个总金额 n,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,则回答 -1 。
比如说,k = 3,面值分别为 1,2,5,总金额 n = 11,那么最少需要 3 枚硬币,即 11 = 5 + 5 + 1 。
按照递归暴力解法->带备忘录的递归解法->非递归的动态规划解法的流程走一下子:
1.暴力递归解法:
首先也是最困难的找出状态转移矩阵,也就是递推公式:
这个方程就用到了「最优子结构」性质:原问题的解由子问题的最优解构成。即 f(11) 由 f(10), f(9), f(6) 的最优解转移而来。
要符合「最优子结构」,子问题间必须互相独立。
def coinChange(coins, amount):
if amount==0:
return 0
res = -1
for coin in coins:
if amount-coin<0:
continue
subProb = coinChange(coins, amount - coin)
if subProb == -1:
continue
if res == -1:
res = subProb+1
else:
res = min(res,subProb+1)
return res
2.带备忘录的递归解法:
int coinChange(vector<int>& coins, int amount) {
// 备忘录初始化为 -2
vector<int> memo(amount + 1, -2);
return helper(coins, amount, memo);
}
int helper(vector<int>& coins, int amount, vector<int>& memo) {
if (amount == 0) return 0;
if (memo[amount] != -2) return memo[amount];
int ans = INT_MAX;
for (int coin : coins) {
// 金额不可达
if (amount - coin < 0) continue;
int subProb = helper(coins, amount - coin, memo);
// 子问题无解
if (subProb == -1) continue;
ans = min(ans, subProb + 1);
}
// 记录本轮答案
memo[amount] = (ans == INT_MAX) ? -1 : ans;
return memo[amount];
}
3.动态规划:
其实这个题和跳台阶的思路是一样的,首先从结果(也就是最后一个状态),逆向往回考虑,只考虑一步,也就是之前的状态怎么样能一步到位,得出最终结果。这样就可以得到状态转移矩阵。但是不要用递归的方式,这回要用正向的考虑,从最简单的状态开始正向迭代,得到最终结果。
以凑零钱为例:比如最后要10元,那10之前的状态可以是5元,8元,9元;它们分别加上5元,2元,1元就得到了10元。以此类推就可以得到状态转移矩阵 dp[i] = 1+ min (dp[i-coins[0]], dp[i-coins[1]]......) ,得到了状态转移矩阵之后,我们要从最简单的状态开始:也就是0元,什么都没有。从0元开始凑钱。
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [float("inf") for i in range(amount+1)] #初始化dp数组
dp[0] = 0 # 当amount=0时,硬币所需数为0
# 在同一个总额的情况下遍历所有的硬币金额
for i in range(1, amount + 1):
for coin in coins:
if i - coin >= 0:
# 选取最小的硬币数
dp[i] = min(dp[i], dp[i - coin] + 1)
if dp[-1] == float("inf"):
return -1
else:
return dp[-1]
另一道题:
题目描述:给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成[1, amount]中所有面值所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。(你可以认为每种硬币的数量是无限的。)
dp[ i ] 代表凑成从1到 i 所有的金额所需的最少硬币个数。对于 dp[ s ] = dp[ i ]+dp[ j ]时候,如果 j 在coins里,比如 j 是5,i 是
3,i 保证1,2,3都可以被表示,j 是一个单独的硬币,它只能保证 j 自己本身被表示,这样4就没有可被表示的方式了。所以就要使得i >=j-1,这要就保证s内所有的金额都能被表示。
s = i + j and i >= j-1 => j< = s - j + 1 (程序中用 i 表示 s )
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [float("inf") for i in range(amount+1)] #初始化dp数组
dp[0] = 0 # 当amount=0时,硬币所需数为0
if 1 not in coins: # 如果硬币里面没有面值为1的硬币,则无法组成所有的硬币
return -1
dp[1] = 1
# 遍历总金额
for i in range(2,amount+1):
min_ = dp[i]
# 遍历之前已经凑成的金额
for j in range(1,i):
# 直接拿单个的硬币凑钱
if j in coins and j<=i-j+1: # 如果另一部分直接可以用一个硬币代替
min_ = min(min_, dp[i-j]+1)
else:
min_ = min(min_,dp[i-j]+dp[j])
dp[i]=min_
return dp[-1]