2020.7.3

Priority
322 零钱兑换

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
示例 2:
输入: coins = [2], amount = 3
输出: -1
说明:
你可以认为每种硬币的数量是无限的

方法一:暴力递归

# 伪代码框架
def coinChange(coins: List[int], amount: int):
	
	# 定义函数: 要凑出金额 n, 至少要dp(amount) 个硬币
	def dp(n):
		# base case
		if n == 0: return 0
		if n < 0: return -1
		# 求最小值, 所以初始化为正无穷
		res = float('INF')
		# 做出选择, 选择需要硬币最少的那个结果
		for coin in coins:
			subproblem = dp(n - coin)
			#子问题无解,跳过
			if subproblem == -1: continue
			res = min(res, 1 + subproblem)
		# 当coins = [2], amount = 3时,  3 - 2 = 1, 再dp(1) subproblem = dp(1 - 2) == dp(-1),这时候跳过, 
		# 返回结果是res == float('INT'), res = -1, 再跳过, res == float('INT') 依然返回 -1,表示无硬币可以组成这个金额.
			
		return res if res != float('INF') else -1  
 return dp(amount)

数学形式的状态转移方程和递归树如下
2020.7.3_第1张图片

时间复杂度分析:子问题总数 x 每个子问题的时间。
子问题总数为递归树节点个数,这个比较难看出来,是 O(n^k),k表示硬币数总之是指数级别的。每个子问题中含有一个 for 循环,复杂度为 O(k)。所以总时间复杂度为 O(k * n^k),指数级别。

方法二:带备忘录的递归

def coinChange(coins: List[int], amount: int):
	#备忘录
	memo = dict()
	def dp(n):
		# 查备忘录, 避免重复计算
		if n in memo: return memo[n]
		if n == 0: return 0
		if n < 0: return -1
		res = float('INF')
		for coin in coins:
			subproblem = dp(n - coin)
			if subproblem == -1: cotinue
			res = min(res, 1 + subproblem)
	
		# 记入备忘录
		memo[n] = res if res != float('INF') else -1
		return memo[n]
		
	return dp(amount)

很显然「备忘录」大大减小了子问题数目,完全消除了子问题的冗余,所以子问题总数不会超过金额数 n,即子问题数目为 O(n)。处理一个子问题的时间不变,仍是 O(k),所以总的时间复杂度是 O(kn)。

方法三:dp数组的迭代解法

当然,我们也可以自底向上使用 dp table 来消除重叠子问题,dp 数组的定义和刚才 dp 函数类似,定义也是一样的
dp[i] = x 表示,当目标金额为 i 时,至少需要 x 枚硬币。
时间复杂度:O(Sn)其中 S 是金额,n 是面额数空间复杂度:O(S)
dp[i] = Math.min(dp[i], 1 + dp[i - coins[j]]); // 这段代码的理解 比如coins = [1, 2, 5]
为什么除了跟1 + dp[i - coins[j]] 比较还要跟dp[i]自己比较呢,因为如果i = 2时,
dp[2] = 1 + dp[2 - 2] == 1 + 0 = 1,
而1 + dp[2 - 1] = 1 + dp[1] = 1 + 1 = 2,
所以要跟以前的自己dp[i] 作比较得出最小的dp[i]的值赋给dp[i], 这里dp[2] = 1是最小的

public int coinChange(int[] coins, int amount) {
	int max = amount + 1;
	int[] dp = new int[max];
	Arrays.fill(dp, max);
	// base case
	dp[0] = 0;
	for (int i = 1; i <= amount; i++) {
		for (int j = 0; j < coins.length; j++) {
			if (coins[j] <= i) {  //  因为dp[i - coins[j]]它的下标最小取dp[0] 必须满足 i >= coins[j],
				dp[i] = Math.min(dp[i], 1 + dp[i - coins[j]]); 
			}
		}
	}
	return dp[amount] > amount ? -1 : dp[amount];
}
int coinChange(vector<int> & coins, int amount) {
// 数组大小为 amount + 1,初始值也为 amount + 1
    vector<int> dp(amount + 1, amount + 1);
    // base case
    dp[0] = 0;
    for (int i = 0; i < dp.size(); i++) {
        // 内层 for 在求所有子问题 + 1 的最小值
        for (int coin : coins) {
            // 子问题无解,跳过
            if (i - coin < 0) continue;
            dp[i] = min(dp[i], 1 + dp[i - coin]);
        }
    }
    return (dp[amount] == amount + 1) ? -1 : dp[amount];
}

你可能感兴趣的:(日程执行计划和实现结果)