322. 零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 - 1 。
你可以认为每种硬币的数量是无限的。
示例 1 :
输入:coins = [ 1 , 2 , 5 ] , amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2 :
输入:coins = [ 2 ] , amount = 3
输出:- 1
思路
不同面额的硬币 coins, 一个总金额amount。 凑成dp[ amout] 的方法。
dp[ j] : 背包j的最金额。
dp[ j] = max ( dp[ j] , dp[ j - coin] + coin)
dp[ 0 ] = [ 0 ] * ( amount + 1 )
for j in range ( 1 , amount + 1 ) :
for coin in coins:
if j > = coin:
dp[ j] = max ( dp[ j] , dp[ j - coin] + coin] )
if dp[ j] == j: continue
j = 1 , coin = 1
dp[ 1 ] = 1
j = 2 - - - coin = 1 , 2
j = 2 , coin = 1
dp[ 2 ] = max ( dp[ 2 ] , dp[ 2 - 1 ] + 1 ) = max ( 0 , 1 + 1 ) = 1
j = 2 , coin = 2
dp[ 2 ] = max ( dp[ 2 ] , dp[ 2 - 2 ] + 2 ) = max ( 2 , 2 ) = 2
j = 3 , coin = 1 , 2 ,
dp[ 3 ] = max ( dp[ 3 ] , dp[ 3 - 1 ] + 1 ) = max ( 0 , 2 + 1 ) = 3
j = 3 , coin = 2
dp[ 3 ] = max ( dp[ 3 ] , dp[ 3 - 2 ] + 2 ) = max ( 3 , 1 + 2 ) = 3
j = 4 , coin = 1 , 2 ,
dp[ 4 ] = max ( dp[ 4 ] , dp[ 4 - 1 ] + 1 ) = max ( 0 , 3 + 1 ) = 4
j = 3 , coin = 2
dp[ 4 ] = max ( dp[ 4 ] , dp[ 4 - 2 ] + 2 ) = max ( 4 , 2 + 2 ) = 4
j = 5 , coin = 1 , 2 , 5
dp[ 5 ] = max ( dp[ 5 ] , dp[ 5 - 1 ] + 1 ) = max ( 0 , 4 + 1 ) = 5
j = 5 , coin = 2
dp[ 5 ] = max ( dp[ 5 ] , dp[ 5 - 2 ] + 2 ) = max ( 5 , 3 + 2 ) = 5
j = 5 , coin = 5
dp[ 5 ] = max ( dp[ 5 ] , dp[ 5 - 5 ] + 5 ) = max ( 5 , 5 ) = 5
j = 6 , coin = 1 , 2 , 5
dp[ 6 ] = max ( dp[ 6 ] , dp[ 6 - 1 ] + 1 ) = max ( 0 , 5 + 1 ) = 6
. . .
j = 6 , coin = 1 , 2 , 5
dp[ 11 ] = max ( dp[ 11 ] , dp[ 11 - 1 ] + 1 ) = max ( 0 , dp[ 10 ] + 1 ) = . .
dp[ 11 ] = max ( dp[ 11 ] , dp[ 11 - 2 ] + 2 ) = max ( 0 , dp[ 9 ] + 2 ) = . .
dp[ 11 ] = max ( dp[ 11 ] , dp[ 11 - 5 ] + 5 ) = max ( 0 , dp[ 6 ] + 5 ) = max ( dp[ 11 ] , 11 ) = 11
能否凑出 amount
class Solution :
def coinChange ( self, coins: List[ int ] , amount: int ) - > int :
dp = [ 0 ] * ( amount + 1 )
for j in range ( 1 , amount + 1 ) :
for coin in coins:
if j >= coin:
dp[ j] = max ( dp[ j] , dp[ j - coin] + coin)
if dp[ j] == j:
continue
if dp[ amount] == amount: return amount
else : return - 1
再读一遍题目:
322. 零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 - 1 。
你可以认为每种硬币的数量是无限的。
示例 1 :
输入:coins = [ 1 , 2 , 5 ] , amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2 :
输入:coins = [ 2 ] , amount = 3
输出:- 1
示例 3 :
输入:coins = [ 1 ] , amount = 0
输出:0
示例 4 :
输入:coins = [ 1 ] , amount = 1
输出:1
示例 5 :
输入:coins = [ 1 ] , amount = 2
输出:2
提示:
1 <= coins. length <= 12
1 <= coins[ i] <= 2 ^ 31 - 1
0 <= amount <= 10 ^ 4
思路
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 - 1 。
你可以认为每种硬币的数量是无限的。
天菩萨,人家要的是凑成金额的最少的银币数, 不是能不能凑出。
dp[ j] : 凑出 j 需要的最少硬币为dp[ j]
dp[ j] = min ( dp[ j] , dp[ j- coin] + 1 )
dp[ j] 如何初始化
dp = [ float ( 'inf' ) ] * ( amount + 1 )
dp[ 0 ] = 0
for j in range ( 1 , amount + 1 ) :
for coin in coins:
if j >= coin and dp[ j- coin] != 'inf' :
dp[ j] = min ( dp[ j] , dp[ j - coin] + 1 )
if dp[ amount] == 'inf' :
return - 1
return dp[ amount]
coins = [ 1 , 2 , 5 ] , amount = 11
j = 1 , coins = 1
dp[ 1 ] = 1
j = 2 , coin = 1
dp[ 2 ] = min ( inf, 1 + 1 ) = 2
j = 2 , coin = 1
dp[ 2 ] = min ( 2 , 1 ) = 1
j = 3
dp[ 3 ] = ( dp[ 2 ] + 1 ) = 2
dp[ 3 ] = ( 2 , dp[ 3 - 2 ] + 1 ) = 2
j = 4
dp[ 4 ] = 2
j = 5
dp[ 5 ] = 1
j = 11
dp[ 11 ] = 3
code python
class Solution :
def coinChange ( self, coins: List[ int ] , amount: int ) - > int :
dp = [ float ( 'inf' ) ] * ( amount + 1 )
dp[ 0 ] = 0
for j in range ( 1 , amount + 1 ) :
for coin in coins:
if j >= coin and dp[ j - coin] != float ( 'inf' ) :
dp[ j] = min ( dp[ j] , dp[ j - coin] + 1 )
if dp[ amount] == float ( 'inf' ) :
return - 1
return dp[ amount]
279.完全平方数
给定正整数 n,找到若干个完全平方数(比如 1 , 4 , 9 , 16 , . . . )使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1 、4 、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1 :
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2 :
输入:n = 13
输出:2
解释:13 = 4 + 9
提示:
1 <= n <= 10 ^ 4
思路
n, 返回和为 n 的完全平方数的 最少数量
和: 至少两个数
假设完全平方数 1 <= 完全平方数 < n
coins = [ v** 2 for v in range ( 1 , n // 2 + 1 ) if v ** 2 < n]
dp[ j] 返回和为 j 的完全平方数的 最少数量
coins = [ v** 2 for v in range ( 1 , n // 2 + 1 ) if v ** 2 < n]
dp[ j] = min ( dp[ j] , dp[ j - coin] + 1 )
for coin in coins:
dp[ coin] = 1
for j in range ( 2 , n+ 1 ) :
for coin in coins:
if j > coin and dp[ j - coin] != float ( 'inf' ) :
dp[ j] = min ( dp[ j] , dp[ j- coin] + 1 )
return dp[ n]
n = 12
coins = [ 1 , 4 , 9 ]
j = 2 , coin = 1
dp[ 2 ] = 1 + 1 = 2
j = 3
dp[ 3 ] = dp[ 2 ] + 1 = 3
dp[ 4 ] = 1
dp[ 5 ] = 2
code python
class Solution :
def numSquares ( self, n: int ) :
coins = [ v ** 2 for v in range ( 0 , ( n + 1 ) // 2 + 1 ) if v ** 2 <= n]
dp = [ float ( 'inf' ) ] * ( n+ 1 )
dp[ 0 ] = 0
for coin in coins:
dp[ coin] = 1
for j in range ( 1 , n + 1 ) :
for coin in coins:
if j >= coin and dp[ j - coin] != float ( 'inf' ) :
dp[ j] = min ( dp[ j] , dp[ j - coin] + 1 )
if dp[ j] == 1 : continue
return dp[ n]
139.单词拆分
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
示例 1 :
输入: s = "leetcode" , wordDict = [ "leet" , "code" ]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code" 。
示例 2 :
输入: s = "applepenapple" , wordDict = [ "apple" , "pen" ]
输出: true
解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple" 。
注意你可以重复使用字典中的单词。
示例 3 :
输入: s = "catsandog" , wordDict = [ "cats" , "dog" , "sand" , "and" , "cat" ]
输出: false
思路
s - - 字符串, 非空单词列表wordDict, s的拆分为字典的一个或者多个在字典中出现的单词。
拆分原则,可以重复使用字典中的单词,假设字典中没有重复的单词。
是否可以被拆分, return True , False
dp[ j] : 长度为j的字符串能否被拆解 dp[ j]
dp[ j] = ( str [ i: j] in wordDict) and dp[ j- i]
dp = [ False ] * ( len ( s) + 1 )
dp[ 0 ] = True
if i = 4 , s[ : i+ 1 ] in wordDict, dp[ i] = ( str [ i: j] in wordDict) and dp[ j- i] = True and dp[ 0 ] = True - - - > dp[ 0 ] = True
lenonly = sorted ( set ( [ len ( v) for v in wordDict] ) )
for j in range ( 1 , len ( s) ) :
for i in lenonly:
if j >= i and dp[ j- i] == True and not dp[ j] :
dp[ j] = ( str [ j- i : j] in wordDict)
code python
class Solution:
def wordBreak(self, s: str, wordDict):
dp = [False] * (len(s) + 1)
dp[0] = True
# dictDict的单词长度
lenonly = sorted(set([len(v) for v in wordDict]))
for j in range(1, len(s) + 1):
for i in lenonly:
if j >= i and dp[j - i] and not dp[j]:
dp[j] = s[j - i:j] in wordDict ### s = "catsandog",
return dp[-1]
总结 from代码随想录
周一
动态规划:377. 组合总和 Ⅳ 中给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数(顺序不同的序列被视作不同的组合)。
题目面试虽然是组合,但又强调顺序不同的序列被视作不同的组合,其实这道题目求的是排列数!
递归公式:dp[ i] += dp[ i - nums[ j] ] ;
这个和前上周讲的组合问题又不一样,关键就体现在遍历顺序上!
在动态规划:518. 零钱兑换II
如果求组合数就是外层for 循环遍历物品,内层for 遍历背包。
如果求排列数就是外层for 遍历背包,内层for 循环遍历物品。
如果把遍历nums(物品)放在外循环,遍历target的作为内循环的话,举一个例子:计算dp[ 4 ] 的时候,结果集只有 { 1 , 3 } 这样的集合,不会有{ 3 , 1 } 这样的集合,因为nums遍历放在外层,3 只能出现在1 后面!
所以本题遍历顺序最终遍历顺序:target(背包)放在外循环,将nums(物品)放在内循环,内循环从前到后遍历。
class Solution {
public :
int combinationSum4 ( vector< int > & nums, int target) {
vector< int > dp ( target + 1 , 0 ) ;
dp[ 0 ] = 1 ;
for ( int i = 0 ; i <= target; i++ ) {
for ( int j = 0 ; j < nums. size ( ) ; j++ ) {
if ( i - nums[ j] >= 0 && dp[ i] < INT_MAX - dp[ i - nums[ j] ] ) {
dp[ i] += dp[ i - nums[ j] ] ;
}
}
}
return dp[ target] ;
}
} ;
周二
爬楼梯之前我们已经做过了,就是斐波那契数列,很好解,但动态规划:70. 爬楼梯进阶版(完全背包)我们进阶了一下。
改为:每次可以爬 1 、 2 、. . . . . 、m 个台阶。问有多少种不同的方法可以爬到楼顶呢?
1 阶,2 阶,. . . . m阶就是物品,楼顶就是背包。
每一阶可以重复使用,例如跳了1 阶,还可以继续跳1 阶。
问跳到楼顶有几种方法其实就是问装满背包有几种方法。
此时大家应该发现这就是一个完全背包问题了!
和昨天的题目动态规划:377. 组合总和 Ⅳ ( opens new window) 基本就是一道题了,遍历顺序也是一样一样的!
代码如下:
class Solution {
public :
int climbStairs ( int n) {
vector< int > dp ( n + 1 , 0 ) ;
dp[ 0 ] = 1 ;
for ( int i = 1 ; i <= n; i++ ) {
for ( int j = 1 ; j <= m; j++ ) {
if ( i - j >= 0 ) dp[ i] += dp[ i - j] ;
}
}
return dp[ n] ;
}
} ;
代码中m表示最多可以爬m个台阶,代码中把m改成2 就是本题70. 爬楼梯可以AC的代码了。
周三
动态规划:322. 零钱兑换 ( opens new window) 给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数(每种硬币的数量是无限的)。
这里我们都知道这是完全背包。
递归公式:dp[ j] = min ( dp[ j - coins[ i] ] + 1 , dp[ j] ) ;
关键看遍历顺序。
本题求钱币最小个数,那么钱币有顺序和没有顺序都可以,都不影响钱币的最小个数。
所以本题并不强调集合是组合还是排列。
那么本题的两个for 循环的关系是:外层for 循环遍历物品,内层for 遍历背包或者外层for 遍历背包,内层for 循环遍历物品都是可以的!
外层for 循环遍历物品,内层for 遍历背包:
class Solution {
public :
int coinChange ( vector< int > & coins, int amount) {
vector< int > dp ( amount + 1 , INT_MAX) ;
dp[ 0 ] = 0 ;
for ( int i = 0 ; i < coins. size ( ) ; i++ ) {
for ( int j = coins[ i] ; j <= amount; j++ ) {
if ( dp[ j - coins[ i] ] != INT_MAX) {
dp[ j] = min ( dp[ j - coins[ i] ] + 1 , dp[ j] ) ;
}
}
}
if ( dp[ amount] == INT_MAX) return - 1 ;
return dp[ amount] ;
}
} ;
外层for 遍历背包,内层for 循环遍历物品:
class Solution {
public :
int coinChange ( vector< int > & coins, int amount) {
vector< int > dp ( amount + 1 , INT_MAX) ;
dp[ 0 ] = 0 ;
for ( int i = 1 ; i <= amount; i++ ) {
for ( int j = 0 ; j < coins. size ( ) ; j++ ) {
if ( i - coins[ j] >= 0 && dp[ i - coins[ j] ] != INT_MAX ) {
dp[ i] = min ( dp[ i - coins[ j] ] + 1 , dp[ i] ) ;
}
}
}
if ( dp[ amount] == INT_MAX) return - 1 ;
return dp[ amount] ;
}
} ;
周四
动态规划:279. 完全平方数 ( opens new window) 给定正整数 n,找到若干个完全平方数(比如 1 , 4 , 9 , 16 , . . . )使得它们的和等于 n。你需要让组成和的完全平方数的个数最少(平方数可以重复使用)。
如果按顺序把前面的文章都看了,这道题目就是简单题了。 dp[ i] 的定义,递推公式,初始化,遍历顺序,都是和动态规划:322. 零钱兑换 ( opens new window) 一样一样的。
要是没有前面的基础上来做这道题,那这道题目就有点难度了。
这也体现了刷题顺序的重要性。
先遍历背包,再遍历物品:
class Solution {
public :
int numSquares ( int n) {
vector< int > dp ( n + 1 , INT_MAX) ;
dp[ 0 ] = 0 ;
for ( int i = 0 ; i <= n; i++ ) {
for ( int j = 1 ; j * j <= i; j++ ) {
dp[ i] = min ( dp[ i - j * j] + 1 , dp[ i] ) ;
}
}
return dp[ n] ;
}
} ;
先遍历物品,再遍历背包:
class Solution {
public :
int numSquares ( int n) {
vector< int > dp ( n + 1 , INT_MAX) ;
dp[ 0 ] = 0 ;
for ( int i = 1 ; i * i <= n; i++ ) {
for ( int j = 1 ; j <= n; j++ ) {
if ( j - i * i >= 0 ) {
dp[ j] = min ( dp[ j - i * i] + 1 , dp[ j] ) ;
}
}
}
return dp[ n] ;
}
} ;
总结
cpp本周的主题其实就是背包问题中的遍历顺序!
求组合数:
动态规划:518. 零钱兑换II 求排列数:
动态规划:377. 组合总和 Ⅳ 、
动态规划:70. 爬楼梯进阶版(完全背包)求最小数:
动态规划:322. 零钱兑换
动态规划:279. 完全平方数
此时我们就已经把完全背包的遍历顺序研究的透透的了!