Day38-动态规划,leetcode322,279,139
- 零钱兑换
- 给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
- 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
- 你可以认为每种硬币的数量是无限的。
- 思路
- 装满这个背包,最少可以用多少物品,每种硬币的数量是无限的,完全背包问题
- 1.确定dp数组定义及下标的含义:dp[j],凑足金额为j所需钱币的最少个数为dp[j]
- 2.确定递推公式:dp[j] = Math.min(dp[j-coins[i]]+1, dp[j])
- 3.dp数组如何初始化:dp[0]=0,考虑到递推公式的特性,dp[j]必须初始化为一个最大的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖。所以下标非0的元素都是应该是最大值。
- 4.确定遍历顺序:coins(物品)放在外循环,target(背包)在内循环。且内循环正序。
- 5.举例推导dp数组,打印dp数组:
/**
1. dp数组定义
dp[j] 表示凑出金额 j 所需的最少硬币数。
2. 初始化
dp[0] = 0,其余初始化为 Infinity(表示暂时无法凑出)。
3. 状态转移方程
dp[j] = Math.min(dp[j - coins[i]] + 1, dp[j])
表示:
如果用当前硬币 coins[i],则需要在 j-coins[i] 的基础上再加 1 个硬币。
取用或不用当前硬币的最小值。
4. 遍历顺序
外层遍历硬币,内层遍历金额(正序),保证每种硬币可以用多次。
5. 返回结果
如果 dp[amount] 还是 Infinity,说明无法凑出,返回 -1;否则返回最少硬币数。
*/
const coinChange = (coins, amount) => {
if(!amount) {
return 0;
}
let dp = Array(amount + 1).fill(Infinity); // 初始化dp数组,dp[j]表示凑出金额j的最少硬币数
dp[0] = 0; // 凑出0元需要0个硬币
for(let i = 0; i < coins.length; i++) { // 遍历每种硬币
for(let j = coins[i]; j <= amount; j++) { // 遍历金额,从当前硬币面值到amount
dp[j] = Math.min(dp[j - coins[i]] + 1, dp[j]);
// 状态转移:用当前硬币,或者不用当前硬币,取最小值
}
}
return dp[amount] === Infinity ? -1 : dp[amount]; // 如果无法凑出,返回-1,否则返回最少硬币数
}
// 遍历背包
/**
1. dp数组定义
dp[i] 表示凑出金额 i 所需的最少硬币数。
2. 初始化
dp[0] = 0,其余初始化为 Infinity(表示暂时无法凑出)。
3. 状态转移方程
对每个金额 i,遍历所有硬币 coins[j],如果 i 能用当前硬币凑出(即 i >= coins[j]),并且 dp[i - coins[j]] 可达,则
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1)
表示:用当前硬币,或者不用当前硬币,取最小值。
4. 返回结果
如果 dp[amount] 还是 Infinity,说明无法凑出,返回 -1;否则返回最少硬币数。
*/
var coinChange = function(coins, amount) {
// dp[i] 表示凑出金额i的最少硬币数
const dp = Array(amount + 1).fill(Infinity)
dp[0] = 0 // 凑出0元需要0个硬币
for (let i = 1; i <= amount; i++) {
for (let j = 0; j < coins.length; j++) {
if (i >= coins[j] && dp[i - coins[j]] !== Infinity) {
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1)
}
}
}
// 如果无法凑出,返回-1,否则返回最少硬币数
return dp[amount] === Infinity ? -1 : dp[amount]
}
- 完全平方数
- 给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
- 完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
- 思路
- 完全平方数就是物品(可以无限件使用),凑个正整数n就是背包,问凑满这个背包最少有多少物品?
- 1.确定dp数组定义及下标的含义:dp[j]:和为j的完全平方数的最少数量为dp[j]
- 2.确定递推公式:dp[j] = min(dp[j - i * i] + 1, dp[j])
- 3.dp数组如何初始化:dp[0]表示 和为0的完全平方数的最小数量,那么dp[0]一定是0。从递归公式dp[j] = min(dp[j - i * i] + 1, dp[j]);中可以看出每次dp[j]都要选最小的,所以非0下标的dp[j]一定要初始为最大值,这样dp[j]在递推的时候才不会被初始值覆盖。
- 4.确定遍历顺序:先遍历物品还是先遍历背包都可以,内层要正序,不限次数
- 5.举例推导dp数组,打印dp数组:
// 先遍历物品,再遍历背包
/**
1. dp数组定义
dp[j] 表示和为 j 的最少完全平方数数量。
2. 初始化
dp[0] = 0,其余初始化为 Infinity(表示暂时无法凑出)。
3. 状态转移方程
对每个完全平方数 val(如 1, 4, 9...),对每个容量 j,从 val 到 n:
dp[j] = Math.min(dp[j], dp[j - val] + 1)
表示:用当前平方数 val,或者不用,取最小值。
4. 遍历顺序
外层遍历物品(完全平方数),内层遍历背包(目标和),正序遍历,保证每个平方数可以用多次。
5. 返回结果
dp[n] 即为和为 n 的最少完全平方数数量。
*/
var numSquares1 = function(n) {
// dp[j] 表示和为 j 的最少完全平方数数量
let dp = new Array(n + 1).fill(Infinity)
dp[0] = 0 // 和为0时不需要任何数
// 外层遍历所有可能的完全平方数(物品)
for(let i = 1; i**2 <= n; i++) {
let val = i**2 // 当前完全平方数
// 内层遍历背包容量(从 val 到 n)
for(let j = val; j <= n; j++) {
// 状态转移:用当前平方数或不用,取最小
dp[j] = Math.min(dp[j], dp[j - val] + 1)
}
}
return dp[n]
};
// 先遍历背包,再遍历物品
/**
1. dp数组定义
dp[i] 表示和为 i 的最少完全平方数数量。
2. 初始化
dp[0] = 0,其余初始化为 Infinity(表示暂时无法凑出)。
3. 状态转移方程
对每个目标和 i,遍历所有不超过 i 的完全平方数 j*j:
dp[i] = Math.min(dp[i], dp[i - j*j] + 1)
表示:用当前平方数 j*j,或者不用,取最小值。
4. 遍历顺序
外层遍历背包(目标和),内层遍历物品(完全平方数),正序遍历,保证每个平方数可以用多次。
5. 返回结果
dp[n] 即为和为 n 的最少完全平方数数量。
*/
var numSquares2 = function(n) {
// dp[i] 表示和为 i 的最少完全平方数数量
let dp = new Array(n + 1).fill(Infinity)
dp[0] = 0 // 和为0时不需要任何数
// 外层遍历背包容量(目标和)
for(let i = 1; i <= n; i++) {
// 内层遍历所有可能的完全平方数(物品)
for(let j = 1; j * j <= i; j++) {
// 状态转移:用当前平方数或不用,取最小
dp[i] = Math.min(dp[i - j * j] + 1, dp[i])
}
}
return dp[n]
};
- 单词拆分
- 给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。
- 注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
- 思路
- 单词就是物品,字符串s就是背包,单词能否组成字符串s,就是问物品能不能把背包装满。拆分时可以重复使用字典中的单词,说明就是一个完全背包!
- 1.确定dp数组定义及下标的含义:dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。
- 2.确定递推公式:如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。
- 3.dp数组如何初始化:从递推公式中可以看出,dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递推的根基,dp[0]一定要为true,否则递推下去后面都都是false了。dp[0]表示如果字符串为空的话,说明出现在字典里。但题目中说了“给定一个非空字符串 s” 所以测试数据中不会出现i为0的情况,那么dp[0]初始为true完全就是为了推导公式。下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词。
- 4.确定遍历顺序: 先遍历 背包,再遍历物品,求排列
- 5.举例推导dp数组,打印dp数组:
/**
1. dp数组定义
dp[i] 表示前 i 个字符能否被字典中的单词拆分。
2. 初始化
dp[0] = true,空字符串可以被拆分。
3. 状态转移
外层循环 i 遍历目标字符串长度。
内层循环 j 遍历字典单词。
如果 s[i - wordDict[j].length, i] 等于字典单词,且前面部分 dp[i - wordDict[j].length] 可拆分,则 dp[i] = true。
4. 返回结果
dp[s.length] 表示整个字符串能否被字典拆分。
*/
const wordBreak = (s, wordDict) => {
// dp[i] 表示前i个字符能否被字典拆分
let dp = Array(s.length + 1).fill(false);
// 空字符串可以被拆分
dp[0] = true;
// 外层遍历背包(目标长度)
for(let i = 0; i <= s.length; i++){
// 内层遍历物品(字典单词)
for(let j = 0; j < wordDict.length; j++) {
// 如果当前i能覆盖wordDict[j]的长度
if(i >= wordDict[j].length) {
// 判断s的末尾是否等于字典中的单词,且前面部分可拆分
if(s.slice(i - wordDict[j].length, i) === wordDict[j] && dp[i - wordDict[j].length]) {
dp[i] = true
}
}
}
}
return dp[s.length];
}
// dp[i] 表示前 i 个字符能否被字典拆分。
// 外层 i 遍历目标长度,内层 j 枚举分割点,判断 s[j:i] 是否在字典且 dp[j] 为 true。
function wordBreak(s, wordDict) {
const wordSet = new Set(wordDict);
const dp = new Array(s.length + 1).fill(false);
dp[0] = true;
for (let i = 1; i <= s.length; i++) {
for (let j = 0; j < i; j++) {
const word = s.substring(j, i); // substring(起始, 结束) 不含结束
if (wordSet.has(word) && dp[j]) {
dp[i] = true;
break; // 找到即可,不用再继续
}
}
}
return dp[s.length];
}
参考&感谢各路大神
宝剑锋从磨砺出,梅花香自苦寒来。

浙公网安备 33010602011771号