零钱兑换
本题是完全背包问题
dp数组表示组成amount金额所需的最少硬币个数。
考虑dp数组的推导公式,由于是计算最少硬币的个数,所以需要考虑dp[i-coins[j]+1和dp[i]的较小值。所以dp[i] = min(dp[i-coins[j]]+1,dp[i]),其中i为遍历过程中的amout值,coins[j]为硬币的面值。
已知推导公式,我们需要对dp数组赋值,由于dp推导式中求的是较小值,所以我们设定dp[0] = 0,其余值都为INT_MAX。
之后是对dp数组的遍历顺序,这里由于我们考虑的是最少银币个数,并不在乎排列或是组合的情况(组成的数目),所以对背包或是物品进行遍历都是可以的,这里我使用先背包后物品的遍历方式。
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
// 创建一个动态规划数组dp,大小为amount+1,初始化为INT_MAX(表示无法凑成的金额)
vector<int>dp(amount+1,INT_MAX);
// 找零0元需要0个硬币
dp[0] = 0;
// 遍历从1到amount的每一个金额
for(int i = 1; i<=amount;i++){
// 遍历每一种硬币
for(int j = 0;j<coins.size();j++){
// 如果当前金额大于或等于当前硬币面额,并且当前金额减去当前硬币面额的找零方式存在
if(i-coins[j]>=0 and dp[i-coins[j]]!=INT_MAX){
// 更新当前金额的最少硬币数量为min(当前最少硬币数量, 减去当前硬币面额的金额的硬币数量+1)
dp[i] = min(dp[i],dp[i-coins[j]]+1);
}
}
}
// 如果amount的找零方式不存在,返回-1
if(dp[amount] == INT_MAX)return -1;
// 返回amount的最少硬币找零数量
return dp[amount];
}
};
算法的时间复杂度为O(n*m),n为coins数组的长度,m为amount+1,空间复杂度为O(m),需要维护一个dp数组,长度为amount+1.
完全平方数
感觉本题和上题比较类似,唯一的不同在于coins数组需要我们自己获取。
dp数组定义等都和上题相同。
class Solution {
public:
int numSquares(int n) {
// 创建一个动态规划数组dp,大小为n+1,初始化为INT_MAX(表示无法组成的情况)
vector<int> dp(n+1, INT_MAX);
// 创建一个数组T_S_N,用于存储小于等于n的所有完全平方数
vector<int> T_S_N; // total Square numbers
// 计算小于等于n的所有完全平方数并存储到T_S_N中
for (int i = 1; i * i <= n; i++) {
T_S_N.push_back(i * i);
}
// 组成0需要0个完全平方数
dp[0] = 0;
// 遍历从1到n的每一个金额
for (int i = 1; i <= n; i++) {
// 遍历每一种完全平方数
for (int j = 0; j < T_S_N.size(); j++) {
// 如果当前数值大于或等于当前完全平方数,并且当前数值减去当前完全平方数的组成方式存在
if (i - T_S_N[j] >= 0 && dp[i - T_S_N[j]] != INT_MAX) {
// 更新当前数值的最少完全平方数数量为min(当前最少完全平方数数量, 减去当前完全平方数的金额的完全平方数数量+1)
dp[i] = min(dp[i - T_S_N[j]] + 1, dp[i]);
}
}
}
// 返回n的最少完全平方数组成数量
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 = i * i; j <= n; j++) { // 遍历背包
dp[j] = min(dp[j - i * i] + 1, dp[j]);
}
}
return dp[n];
}
};
算法的时间复杂度为O(n*(3/2)),空间复杂度为O(n)。
单词拆分
具体参考代码随想录 代码随想录 (programmercarl.com)。
考虑单词为物品,所要匹配的字符串为背包,单词可以重复使用,就是一个使用单词匹配字符串(单词是否能完全构成字符串)的完全背包问题。
这里我们考虑将单词数组存入哈希集合,因为可以方便快速寻找
dp[i]中i表示字符串的长度,dp[i]表示为是否可以拆分为单词数组中的单词,值为true 或false.
dp[i]的递推公式我们应这样考虑,若dp[before_i]为true,且before_i至i的位置的字符串存在于单词数组中,则dp[i]为true,否则为false。
考虑到dp[i]取决于前面的值,则dp[0] = true,否则后续值递推全为false。
遍历方式:这里需注意应为排列,每个单词元素的顺序是有意义的。因此考虑先背包后物品的遍历方式
最后返回数组的末尾元素即知道拆分是否可能实现。
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
// 创建一个哈希集合word_set,用于存储wordDict中的所有单词,以便快速查找
unordered_set<string> word_set{};
// 将wordDict中的所有单词插入到word_set中
for (auto word : wordDict) {
word_set.insert(word);
}
// 创建一个动态规划数组dp,大小为s.size()+1,初始化为false
// dp[i]表示字符串s的前i个字符是否可以被拆分成wordDict中的单词
vector<bool> dp(s.size() + 1, false);
// 初始化dp[0]为true,因为空字符串可以被拆分成空集合
dp[0] = true;
// 遍历字符串s的每一个位置
for (int i = 1; i <= s.size(); i++) {
// 对于每个位置i,尝试从0到i的所有分割点j
for (int j = 0; j < i; j++) {
// 取出从j到i的子串
string word = s.substr(j, i - j);
// 如果子串word在word_set中,并且dp[j]为true(前j个字符可以拆分)
if (word_set.find(word) != word_set.end() && dp[j]) {
// 则dp[i]为true,表示前i个字符可以拆分
dp[i] = true;
}
}
}
// 返回dp[s.size()]
return dp[s.size()];
}
};
- 时间复杂度:O(n^3),因为substr返回子串的副本是O(n)的复杂度(这里的n是substring的长度)
- 空间复杂度:O(n)