DAY48:动态规划(十二)完全平方数(类似零钱兑换)+单词拆分(注意背包思路!)

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 是单词的平均长度。原因是这里有两个嵌套的 for 循环,最外层循环的复杂度是 O(n),最内层循环在最坏情况下(即 j = n)的复杂度也是 O(n),然后对于每一个 i,都有一次 substr 的操作,这个操作的时间复杂度是 O(m),因此总的时间复杂度是 O(n^2 * m)。
  • 两个for循环的时间复杂度计算,本题是j<i的情况,因此是O(n*n),因此就是n^2。
  • 当我们使用s.substr(i-len,len)我们实际上是从原始字符串中复制出一段新的字符串。这个复制过程需要遍历并复制每个字符,所以它的时间复杂度是O(m),其中m是单词的长度
  • 空间复杂度:O(n + k),其中n是字符串s的长度,k是wordDict的大小。空间复杂度主要是dp数组和字典的大小。

思路二:

  • 代码的时间复杂度是 O(n * l * m),其中 n 是字符串 s 的长度,l是字典 wordDict 的大小,m 是单词的平均长度。原因是这里有两个嵌套的 for 循环,最外层循环的复杂度是 O(n),最内层循环的复杂度是 O(l),然后对于每一个 j,都有substr 的操作,这个操作的时间复杂度是 O(m),因此总的时间复杂度是 O(n * l * m)
  • 两个for循环的时间复杂度计算,内层每次都会遍历l的情况,因此是O(n*l)
  • wordD.find(res)操作的平均时间复杂度是O(1),但这是平均情况的时间复杂度,最坏情况下,如果哈希冲突严重,它可能会退化到O(n),这里的n是哈希表中元素的数量。但我们计算一般用的是平均时间复杂度,这里还是O(1)
  • O(m)是源自substr操作,其中m是提取的子字符串的长度。因为substr函数的工作方式是从源字符串中复制字符来创建一个新的字符串,所以它的时间复杂度与子字符串的长度成正比,也就是O(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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值