0、参考资料
稍微总结一下我最近遇到的背包问题题解以及一些非常详细的资料讲解:
0-1背包问题:dp[i][j]表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值(每件物品最多放一次)
分割等和子集问题:定义 dp[i][j]表示从前 i 个数字中选出若干个,刚好可以使得被选出的数字其和为 j。
完全背包问题:dp[i][j] 表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值(每件物品有无限个)。
本题 属于完全背包问题:dp[i][j] 表示:从前 i 种硬币中组成金额 j 所需最少的硬币数量。
更加详细的总结与背包问题详解请看:
一、题目描述
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
二、 代码思路分析
这是一种经典的完全背包问题,完全背包问题就是给定特定容量的背包,让你从特定重量与价格的物品中选出一些填满背包,同时保证价值最大。之所以叫做完全背包,是因为每个物品可以被重复选。
在本题中,转换为完全背包问题:给定总金额(背包容量)从多个硬币中(选择的物品)选出占满特定金额所需的最少硬币(想要达成的目的)。
本题的状态转换公式为:
dp[i][j] 表示:从前 i 种硬币中组成金额 j 所需最少的硬币数量。
稍微总结一下我最近遇到的背包问题题解以及一些非常详细的资料讲解:
0-1背包问题:dp[i][j]表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值(每件物品最多放一次)
分割等和子集问题:定义 dp[i][j]表示从前 i 个数字中选出若干个,刚好可以使得被选出的数字其和为 j。
完全背包问题:dp[i][j] 表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值(每件物品有无限个)。
本题 属于完全背包问题:dp[i][j] 表示:从前 i 种硬币中组成金额 j 所需最少的硬币数量。
更加详细的总结与背包问题详解请看:
三、代码题解
一共有四种题解来不断优化背包问题,可以看我上面引用的参考资料,讲的很棒!目前我只写了第一种最简单的解法。
3.1 从0 - 1背包演化过来的完全背包解法
此解法是最简单理解的,但是也是复杂度最高的
func coinChange(coins []int, amount int) int {
//经典背包问题,并非是0-1背包,
//0-1背包问题:dp[i][j]表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值(每件物品最多放一次)
//分割等和子集问题:定义 dp[i][j]表示从前 i 个数字中选出若干个,刚好可以使得被选出的数字其和为 j。
//完全背包问题:dp[i][j] 表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值(每件物品有无限个)。
//本题:dp[i][j] 表示:从前 i 种硬币中组成金额 j 所需最少的硬币数量。
var dp [][]int
for i := 0; i < len(coins) + 1; i++ {
ar := make([]int, amount + 1)
//切片赋值会更好一些,但是下面是错误的
//ar[:] = amount + 1
for j := 0; j < amount + 1; j++ {
ar[j] = amount + 1
}
dp = append(dp, ar)
}
fmt.Println(dp)
dp[0][0] = 0
for i := 1; i < len(coins) + 1; i++ {
for j := 0; j < amount + 1; j++ {
for k := 0; k < (j / coins[i - 1]) + 1; k++ {
//fmt.Println(k)
//fmt.Println(dp[i - 1][j - k * coins[i - 1]] + k)
if dp[i][j] > (dp[i - 1][j - k * coins[i - 1]] + k) {
dp[i][j] = dp[i - 1][j - k * coins[i - 1]] + k
}
}
}
}
if dp[len(coins)][amount] != amount + 1 {
return dp[len(coins)][amount]
} else {
return -1
}
}
3.2 【二维DP,两层循环】 基于优化后的状态转移方程,可省去第三层循环
func coinChange(coins []int, amount int) int {
//经典背包问题,并非是0-1背包,
//0-1背包问题:dp[i][j]表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值(每件物品最多放一次)
//分割等和子集问题:定义 dp[i][j]表示从前 i 个数字中选出若干个,刚好可以使得被选出的数字其和为 j。
//完全背包问题:dp[i][j] 表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值(每件物品有无限个)。
//本题:dp[i][j] 表示:从前 i 种硬币中组成金额 j 所需最少的硬币数量。
var dp [][]int
for i := 0; i < len(coins) + 1; i++ {
ar := make([]int, amount + 1)
//切片赋值会更好一些,但是下面是错误的
//ar[:] = amount + 1
for j := 0; j < amount + 1; j++ {
ar[j] = amount + 1
}
dp = append(dp, ar)
}
//fmt.Println(dp)
dp[0][0] = 0
for i := 1; i < len(coins) + 1; i++ {
for j := 0; j < amount + 1; j++ {
//fmt.Println(k)
//fmt.Println(dp[i - 1][j - k * coins[i - 1]] + k)
if j < coins[i - 1] {
dp[i][j] = dp[i - 1][j]
} else {
if dp[i - 1][j] > dp[i][j - coins[i - 1]] + 1 {
dp[i][j] = dp[i][j - coins[i - 1]] + 1
} else {
dp[i][j] = dp[i - 1][j]
}
}
}
}
if dp[len(coins)][amount] != amount + 1 {
return dp[len(coins)][amount]
} else {
return -1
}
}
参考:https://leetcode.cn/problems/coin-change/solutions/1412324/by-flix-su7s/
3.3 滚动数组,一维dp
func coinChange(coins []int, amount int) int {
//经典背包问题,并非是0-1背包,
//0-1背包问题:dp[i][j]表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值(每件物品最多放一次)
//分割等和子集问题:定义 dp[i][j]表示从前 i 个数字中选出若干个,刚好可以使得被选出的数字其和为 j。
//完全背包问题:dp[i][j] 表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值(每件物品有无限个)。
//本题:dp[i][j] 表示:从前 i 种硬币中组成金额 j 所需最少的硬币数量。
var dp = make([]int, amount + 1)
for i := 1; i < amount + 1; i++ {
dp[i] = amount + 1
}
dp[0] = 0
for i := 1; i < len(coins) + 1; i++ {
var dp2 = make([]int, amount + 1)
for k := 0; k < amount + 1; k++ {
dp2[k] = amount + 1
}
for j := 0; j < amount + 1; j++ {
//fmt.Println(k)
//fmt.Println(dp[i - 1][j - k * coins[i - 1]] + k)
if j < coins[i - 1] {
dp2[j] = dp[j]
} else {
if dp[j] > dp2[j - coins[i - 1]] + 1 {
dp2[j] = dp2[j - coins[i - 1]] + 1
} else {
dp2[j] = dp[j]
}
}
}
dp = dp2
}
if dp[amount] != amount + 1 {
return dp[amount]
} else {
return -1
}
}