文章目录
- 279.完全平方数(类似零钱兑换)
- 思路
- DP数组含义
- 递推公式
- 初始化
- 遍历顺序
- 最开始的写法:有1个用例没过
- 修改完整版
- 总结
- 139.单词拆分(递推公式注意)
- 思路1:遍历单词分割点
- DP数组含义
- 递推公式
- 初始化
- 遍历顺序
- 思路1完整版
- debug测试:解答错误,原因是substr参数错误
- 思路2:完全背包(遍历顺序注意)
- 递推公式
- 遍历顺序
- 思路2完整版
- 两种思路时间复杂度对比
- 优缺点:
- 总结
279.完全平方数(类似零钱兑换)
- 本题也是求装满背包的最小物品个数,和零钱兑换一样。涉及到初始值的问题。
- 本题注意思路,物品自带限制的情况,这个限制可以加到递推公式里!
给你一个整数 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。
DP数组含义
dp[j]
表示:装满容量为j的背包,最少需要dp[j]
个完全平方数。
递推公式
递推公式同上一题零钱兑换
dp[j]=min(dp[j],dp[j-i*i]+1);//物品直接用i*i来表示
初始化
本题的初始化因为是最小值,所以也是全部初始化为INT_MAX,再令dp[0]=0
。
dp[0]=0
这个初始化非常重要,所有的递推都是从0开始,相当于最开始只有j=coins[i]的时候,才能更新dp[j]的数值。
遍历顺序
本题求最小物品个数,和方案数目无关,组合or排列都不影响物品的个数,因此都可以。
最开始的写法:有1个用例没过
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=i*i;j<=n;j++){//j在里层可以直接合并边界条件
if(dp[j-i*i]==INT_MAX) continue;//为了保证背包装满,装不满的情况直接continue
dp[j]=min(dp[j],dp[j-i*i]+1);
}
}
if(dp[n]==INT_MAX) return -1;
return dp[n];
}
};
修改完整版
针对这个用例的情况,只需要修改for循环的起始值和终止,令其包括j=1,也就是n=1情况即可
class Solution {
public:
int numSquares(int n) {
vector<int>dp(n+1,INT_MAX);
dp[0]=0;
//修改了起始值,把j=i*i也就是n=1的情况包括了
for(int i=1;i<=n;i++){
for(int j=i*i;j<=n;j++){//j在里层可以直接合并边界条件
if(dp[j-i*i]==INT_MAX) continue;//为了保证背包装满,装不满的情况直接continue
dp[j]=min(dp[j],dp[j-i*i]+1);
}
}
if(dp[n]==INT_MAX) return -1;
return dp[n];
}
};
- 时间复杂度: O(n * √n)
- 空间复杂度: O(n)
或者写成
- 完全平方数的情况,物品循环应该是i*i<=n,这样的话i从0开始也可以
class Solution {
public:
int numSquares(int n) {
vector<int>dp(n+1,INT_MAX);
dp[0]=0;
for(int i=0;i*i<=n;i++){
for(int j=i*i;j<=n;j++){//j在里层可以直接合并边界条件
if(dp[j-i*i]==INT_MAX) continue;//为了保证背包装满,装不满的情况直接continue
dp[j]=min(dp[j],dp[j-i*i]+1);
}
}
if(dp[n]==INT_MAX) return -1;
return dp[n];
}
};
- 先遍历背包再遍历物品的写法
class Solution {
public:
int numSquares(int n) {
vector<int>dp(n+1,INT_MAX);
dp[0]=0;
for(int i=0;i*i<=n;i++){
for(int j=i*i;j<=n;j++){//j在里层可以直接合并边界条件
if(dp[j-i*i]==INT_MAX) continue;//为了保证背包装满,装不满的情况直接continue
dp[j]=min(dp[j],dp[j-i*i]+1);
}
}
if(dp[n]==INT_MAX) return -1;
return dp[n];
}
};
总结
本题的重要注意点就是,不要局限于判断数字(物品)是不是完全平方数,物品本身的限制条件并不复杂,完全可以直接在递推公式里面进行替换,也就是把递推公式换成dp[j]=min(dp[j],dp[j-i*i]+1)
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
提示:
- 1 <=
s.length
<= 300 - 1 <=
wordDict.length
<= 1000 - 1 <=
wordDict[i].length
<= 20 - s 和
wordDict[i]
仅有小写英文字母组成 wordDict
中的所有字符串 互不相同
思路1:遍历单词分割点
因为本题并不是要求给出所有的拼接结果,只是判断能不能进行拼接,因此可以用背包问题进行解决。
目标字符串就是背包,字符串列表就是物品列表。
大致思路就是按照长度挨个遍历目标字符串,遍历到j的时候,判断当前长度的字符串,能不能在字典里找到。
DP数组含义
dp[j]表示,长度为j的字符串,能不能被字典中的单词组成。
字典中的单词可以重复使用,且**dp[j]
遍历的一定是原字符串**,为了防止"apple"“pen”"apple"出现乱序的问题!
递推公式
遍历到j的时候,主要考虑的dp[j]
是由dp[i]
和dp[j-i]
的状态推出来的,i是枚举0–j中的分割点。
也就是说,我们需要枚举[0,j)中的分割点i,看[0,i-1]和[i,j-1]这两个子串,是不是都符合能在字典中找到的要求。
如果两个字符串都合法,那么最后的结果也合法。
也就是说,递推公式为:
dp[j]=dp[i]&&dp[j-i];//i是枚举的[0,j-1]所有分割点
单词拆分官方题解:单词拆分 - 单词拆分 - 力扣(LeetCode)
初始化
全部初始化为false,只有dp[0]初始化为true
遍历顺序
因为i是j枚举过程中的子数组分割点,因此循环的嵌套是j在外,i在内
思路1完整版
- 对于检查一个字符串是否出现在给定的字符串列表里,一般可以考虑哈希表来快速判断,因为哈希表带有.find()函数而数组没有
- 枚举所有可能的分割点,然后对原字典中的单词进行查找和对比
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
//先构造哈希表把word字典放进去,方便find查找
unordered_set<string>wordD;
for(auto word:wordDict){
wordD.insert(word);//word这里就是数组元素本身
}
vector<bool>dp(s.size()+1,false);
dp[0]=true;
for(int j=0;j<=s.size();j++){
//内层是枚举[0,j)以来的所有分割点
for(int i=0;i<j;i++){
//先看dp[j-i]能不能放进去
auto res = s.substr(i,j-i);//注意substr的参数
if(wordD.find(res)!=wordD.end()&&dp[i]==true){//如果找到了res
dp[j]=true;//再判断dp[j]是不是true
}
}
}
return dp[s.size()];
}
};
debug测试:解答错误,原因是substr参数错误
最开始的写法是:
auto res = s.substr(j-i,j);//参数错误,是起点+长度
但是实际上substr这么用是错误的!substr函数的两个参数分别是起始位置和子串的长度,而不是结束位置。所以应该是
auto res = s.substr(i,j-i);//一定要注意两个参数是起点+长度
思路2:完全背包(遍历顺序注意)
上面这种思路实际上不算是完全背包了,因为和字典里面的物品已经没啥关系了。
实际上这道题也可以用完全背包方法做,也就是遍历物品。遍历物品的大致思路是,对于每一个背包容量(字符串长度)[0--j]
,都进行是否能被拼成的判断,即遍历所有的单词,并判断当前长度的字符串末尾是否是这个单词。
DP数组含义和思路1一样,动态规划都是求什么,DP数组的含义就是什么。
dp[j]依旧代表长度为j的字符串,能不能被字符数组中的元素组成。
递推公式
递推公式也属于先判断dp[i-len]
是不是符合要求,符合要求才返回true的类型
if(dp[i - len]==true)
dp[i] = true;
遍历顺序
本题实际上属于排列问题,因为将字典数组内的元素看作物品的话,物品"apple"“pen”“apple"填满背包,和"pen”"apple"apple"填满背包是不一样的!因此这是一个排列问题,必须背包在外,物品在内。
思路2完整版
- 第一种写法,直接对比s字符串的末尾的符合每个单词长度的元素,去对比是不是能在字典里找到
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
//dp数组和思路1相同
vector<bool> dp(s.size()+1,false);
//dp[0]初始化
dp[0] = true;
//背包在外物品在内
for(int i=0;i<=s.size();i++){
//物品就是字典里的元素
for(int j=0;j<wordDict.size();j++){
int len=wordDict[j].size();
if(i>=len){
auto res = s.substr(i-len,len);
//只要末尾元素和当前元素相同,且前面的部分也是true,就返回true
if(res==wordDict[j]&&dp[i-len]==true) dp[i]=true;
}
}
}
return dp[s.size()];
}
};
- 或者另一种做法,建立哈希表,也是找末尾元素,看字符串符合单词长度的末尾元素能不能在字典里找到,这里用了find()用法,更好理解一些
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
//直接把string数组的内容复制到set中
unordered_set<string> wordD(wordDict.begin(), wordDict.end());
//dp数组和思路1相同
vector<bool> dp(s.size()+1,false);
//dp[0]初始化
dp[0] = true;
//背包在外物品在内
for(int i=0;i<=s.size();i++){
//物品就是字典里的元素
for(int j=0;j<wordDict.size();j++){
int len=wordDict[j].size();
if(i>=len){
auto res = s.substr(i-len,len);
//只要末尾这一段元素能和字典单词对应,且前面的部分也是true,就返回true
if(wordD.find(res)!=wordD.end()&&dp[i-len]==true) dp[i]=true;
}
}
}
return dp[s.size()];
}
};
两种思路时间复杂度对比
思路一:
- 时间复杂度:
O(n^2 * m)
,其中n是字符串s的长度,m是wordDict中最大字符串的长度。这是因为我们需要检查所有的子字符串,其复杂度为O(n^2),并且对每个子字符串我们都需要在字典中进行查找,其复杂度为O(m)。 - 空间复杂度:O(n + k),其中n是字符串s的长度,k是wordDict的大小。空间复杂度主要是dp数组和字典的大小。
思路二:
- 时间复杂度:
O(n * l * m)
,其中n是字符串s的长度,l是字典的大小,m是字典中最大字符串的长度。对于每个位置,我们都需要检查所有的单词是否可以作为前缀,所以时间复杂度是O(n * l * m)。 - 空间复杂度:O(n + k),其中n是字符串s的长度,k是wordDict的大小。空间复杂度主要是dp数组和字典的大小。
优缺点:
思路一:
- 优点:一的优点在于其相对直观,通过检查所有可能的子串和字典中的单词进行比较。
- 缺点:如果字典中的单词长度非常大,那么时间复杂度将会非常高。
思路二:
- 优点:二遍历字典的方式会比方法一更有效,因为我们直接跳过了那些长度大于当前检查子串长度的单词,这降低了不必要的计算。
- 缺点:如果字典的大小非常大,那么时间复杂度将会非常高。
总结
这道题目的关键在于正确理解dp数组的含义。在这个代码中,dp[j]
表示字符串s的前j个字符是否可以用wordDict中的词语拆分。
完全背包的做法是建立哈希表,找挨个增加长度的过程中,字符串的末尾元素。看字符串里符合单词长度的末尾元素能不能在字典里找到,能找到则[i-j]
这一段为true,如果dp[i-j]
也是true,那么dp[i]就是true。(i是字符串长度)
遍历单词分割点的做法是,我们枚举所有可能的分割点j,如果找到一个有效的分割点(即字符串s的子串在字典中且dp[j]
为true),就更新dp[i]
为true。