零钱兑换
题目描述
给你一个整数数组 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
解题思路
-
确定dp数组以及下标的含义
dp[j]
:凑足总额为j的金额需要的最少钱币个数为dp[j]
-
确定递推公式
- 递推公式:
dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
- 凑足总额为
j - coins[i]
的钱币最少个数为dp[j - coins[i]]
,因此只需要加上一个钱币coins[i]
就能够凑足总额为j
的金额,此时所需要的最小金额就是dp[j]
和dp[j - coins[i]] + 1
总的最小值。
- 递推公式:
-
dp数组初始化
- 凑足金额j为0的钱币个数一定是0,因此dp[0] = 0,根据递推公式特性,因为我们需要取得较小值,因此可以将其他金额的情况初始化为整数最大值
-
确定遍历顺序
- 本题中先遍历钱币在遍历背包都是可以的,无非是写法不同而已,本题使用的先遍历钱币后遍历背包的方式,遍历方式都是正序遍历。
-
举例推导dp数组
-
以输入:coins = [1, 2, 5], amount = 5为例,手动推举dp数组:
-
代码实现
测试地址:https://leetcode.cn/problems/coin-change/
class Solution {
public:
int coinChange(vector<int> &coins, int amount) {
// 初始化 dp 数组
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++) {
// 如果可以组成金额 j - coins[i]
if (dp[j - coins[i]] != INT_MAX) {
// 更新 dp[j]
dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
}
}
}
// 如果无法组成该金额,返回 -1
if (dp[amount] == INT_MAX)
return -1;
// 否则返回组成该金额所需的最小硬币数量
return dp[amount];
}
};
完全平方数
题目描述
给你一个整数 n
,返回 和为 n
的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1
、4
、9
和 16
都是完全平方数,而 3
和 11
不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
解题思路
-
确定dp数组以及下标的含义
dp[j]
:要组合和为j
的完全平方数所需要使用的最少数量为dp[j]
-
确定递推公式
- dp[j]可以由
dp[j-i*i]
所推出,因此dp[j - i * i] + 1
便可以凑成dp[j]
,由于我们需要求的是使用的最小平方数的个数 - 递推公式:
dp[j] = min(dp[j - i * i] + 1, dp[j]);
- dp[j]可以由
-
dp数组初始化
- 题目提到了n从1开始,而且并没有明确表示平方数从0开始,因此我们可以初始化
dp[0]=0
,因为我们需要求的是最少个数,因此可以将非0下标的其他值初始化为最大值。
- 题目提到了n从1开始,而且并没有明确表示平方数从0开始,因此我们可以初始化
-
确定遍历顺序
- 先遍历背包和先遍历物品都是可以的,本题采用的是先遍历背包后遍历物品的方式,顺序均为正序。
-
举例推导dp数组
-
已输入n为5例,dp状态图如下:
-
代码实现
测试地址:https://leetcode.cn/problems/perfect-squares/
class Solution {
public:
int numSquares(int n) {
// 创建一个 dp 数组,大小为 n+1, 初始值设为 INT_MAX 表示无限大
vector<int> dp(n + 1, INT_MAX);
// 递归基,0的最小分解为0个完全平方数
dp[0] = 0;
// 遍历从1到n的所有数字
for (int j = 0; j <= n; j++) {
// 遍历所有可能的完全平方数,从1开始,i*i是完全平方数
for (int i = 1; i * i <= j; i++) {
// 更新 dp[j],选取使用当前完全平方数i*i和不使用时的较小值
dp[j] = min(dp[j - i * i] + 1, dp[j]); // dp[j - i*i] + 1 表示如果选用当前的完全平方数,则需加1(因为使用了一次)
}
}
// 返回组成整数n所需的最少完全平方数
return dp[n];
}
};
单词拆分
题目描述
给你一个字符串 s
和一个字符串列表 wordDict
作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s
则返回 true
。
注意: 不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 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
解题思路
-
确定dp数组以及下标的含义
dp[i]
表示字符串s
的前i
个字符能否被成功分割成一个或多个在字典中的单词。这里i
是从0到字符串s
的长度。
-
确定递推公式
- 如果存在一个
j
(0 <= j < i),使得从j
到i
的子字符串(即s.substr(j, i - j)
)是字典中的一个词,并且dp[j]
是true
(表示s
的前j
个字符可以被成功分割),那么dp[i]
应该是true
。 - 递推公式:
dp[i]=dp[j] and s[j:i] in wordDict for any j such that 0≤j<i
- 如果存在一个
-
dp数组初始化
- 初始化
dp[0] = true
,因为空字符串可以被认为是任何字典的子集,不需要分割。其余dp[i]
初始化为false
,表示开始时假设没有有效分割。
- 初始化
-
确定遍历顺序
- 外层循环遍历字符串
s
从1到长度n
(这里n是s.size()
),内层循环遍历从0到i-1
,尝试找到一个j
,使得s.substr(j, i - j)
是字典中的词。这样确保我们在决定dp[i]
前,已经计算了所有的dp[j]
。
- 外层循环遍历字符串
-
举例推导dp数组
-
以
s = "leetcode"
,wordDict = ["leet", "code"]
为例,手推dp数组如下:dp[0] = true
(基础情况)dp[4]
在j = 0
时,s.substr(0, 4) = "leet"
是字典中的词,dp[0]
是true
,所以dp[4]
成为true
。dp[8]
在j = 4
时,s.substr(4, 4) = "code"
是字典中的词,dp[4]
是true
,所以dp[8]
成为true
dp[s.size()]
即dp[8]
是true
,表示字符串可以完全分割使用字典中的词。
-
代码实现
测试地址:https://leetcode.cn/problems/word-break/
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
// 将字典转换为无序集合,以便快速查找
unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
// 初始化dp数组,大小为s.size() + 1,所有元素初始化为false
vector<bool> dp(s.size() + 1, false);
// 基础情况:空字符串可以被成功分割
dp[0] = true;
// 外层循环遍历字符串s的每个位置i,表示当前考虑的子字符串的结束位置
for (int i = 1; i <= s.size(); i++) { // 遍历背包
// 内层循环遍历从0到i-1的每个位置j,表示当前考虑的子字符串的起始位置
for (int j = 0; j < i; j++) { // 遍历物品
// 获取从j到i的子字符串
string word = s.substr(j, i - j); //substr(起始位置,截取的个数)
// 检查子字符串是否在字典中,并且dp[j]是否为true
if (wordSet.find(word) != wordSet.end() && dp[j]) {
// 如果条件满足,设置dp[i]为true
dp[i] = true;
}
}
}
// 返回dp数组的最后一个元素,表示整个字符串是否可以被成功分割
return dp[s.size()];
}
};