代码随想录算法训练营第38天 | LeetCode322.零钱兑换、LeetCode279.完全平方数、LeetCode139.单词拆分、56. 携带矿石资源

目录

LeetCode322.零钱兑换

LeetCode279.完全平方数 

LeetCode139.单词拆分 

1. 回溯算法

2. 动态规划

56. 携带矿石资源 

题目描述

输入描述

输出描述

输入示例

输出示例

提示信息


LeetCode322.零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

思路:抽象一下,给定背包容量,求满足将背包装满时所需的最小物品数量。

所以动态规划dp[j]的含义有了。

接着是递推公式,当要求dp[j]时,如果说当前硬币价值为value(同时所占空间也为value),那么就需要找到dp[j-value],然后加上value这个硬币数量1,所以就是dp[j-value]+1;

对于初始化,dp[0]=0,很容易理解,当所要凑满的金额为0时,确实只需要0个硬币;因为求的是最小值,所以不能使初始值在更新过程中将计算结果覆盖,所以将其他的初始值设为INT_MAX。

因为我们要的是数量,所以不管是组合还是排列,都是可以的,因此先遍历物品还是背包都是可以的。

下面是先遍历物品,再遍历背包的情况。

    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount + 1, INT_MAX);//dp[j]表示将面额j凑满所用的最少硬币个数
        dp[0] = 0;
        for(int i = 0; i < coins.size(); i ++){//先遍历物品
            for(int j = coins[i]; j < amount + 1; j ++){//再遍历背包
                //这里添加判断语句是因为如果dp[j]为初始值INT_MAX,那么再加1就会超限,所以如果遇到这种情况直接跳过就好了
                if(dp[j - coins[i]] != INT_MAX) dp[j] = min(dp[j], dp[j - coins[i]] + 1);
            }
        }
        if(dp[amount] == INT_MAX) return -1 ;
        return dp[amount];
    }

下面是先遍历背包,再遍历物品的情况。

    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount + 1, INT_MAX);//dp[j]表示将面额j凑满所用的最少硬币个数
        dp[0] = 0;
        for(int j = 0; j < amount + 1; j ++){//先遍历背包
            for(int i = 0; i < coins.size(); i ++){//再遍历物品
                if(j >= coins[i] && dp[j - coins[i]] != INT_MAX) dp[j] = min(dp[j], dp[j - coins[i]] + 1);
            }
        }
        if(dp[amount] == INT_MAX) return -1 ;
        return dp[amount];
    }

时间复杂度:O(amount*n)

空间复杂度:O(amount)

LeetCode279.完全平方数 

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,14916 都是完全平方数,而 311 不是。

思路:当解决了上面的题目后,再来看这道题,就变得简单起来了。

同样是给定背包容量,求装满的时候的最小数量。这里的背包就是给定的整数n,物品就是各种完全平方数。

相对比较简单。几乎和上面的题目一模一样,这里不再赘述。

下面是先遍历物品,再遍历背包的情况。

    int numSquares(int n) {
        vector<int> dp(n + 1, INT_MAX);//dp[j]表示满足j的完全平方和数的最小数量
        dp[0] = 0;
        for(int i = 1; i * i <= n; i ++){//先物品
            for(int j = i * i; j <= n; j ++){//后背包
                if(dp[j - i * i] != INT_MAX) dp[j] = min(dp[j], dp[j - i * i] + 1);
            }
        }
        return dp[n];
    }
    int numSquares(int n) {
        vector<int> dp(n + 1, INT_MAX);//dp[j]表示满足j的完全平方和数的最小数量
        dp[0] = 0;
        for(int j = 0; j < n + 1; j ++){//先背包
            for(int i = 1; i * i <= n; i ++){//再物品
                if(j >= i * i && dp[j - i * i] != INT_MAX) dp[j] = min(dp[j], dp[j - i * i] + 1);
            }
        }
        return dp[n];
    }

时间复杂度:O(n*\sqrt{n})

空间复杂度:O(n)

LeetCode139.单词拆分 

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

思路:这里介绍两种方法求解。

1. 回溯算法

之前做过一道题目,求解字符串是否可以分割成回文字符串。其实是相似的,不过这里需要注意的是,如果单纯进行分割然后开始递归回溯可能会超限,所以这里使用了一个memory数组尝试进行记忆化递归,也就是说将某些之前已经遍历过的状态记录下来,这样在进行递归或者循环前先查询一下,如果没有必要再向下进行了,就直接返回了,可以节省不少时间和空间的消耗。

这里就是将记录下每个结点作为起始结点是否可以有子串能够在wordDict中找到,也就是出现过的话,可以进行划分,接着继续进行递归划分;但是如果记录下以该下标作为起始下标没有办法构成wordDict中的任何单词,直接返回所记录的状态,避免了时间和空间的消耗,同时也避免了超限。

    bool backtracking(string  s, int startIndex, unordered_set<string>& word, vector<bool>& memory){
        if(startIndex == s.size()){//当下标到达了最后的位置,直接返回true,表示分割完成
            return true;
        }
        if(!memory[startIndex]) return memory[startIndex];//这里如果memory[startIndex]不等于初始值了,直接返回它,其实也就意味着以startIndex开始的字符串没有办法进行分割了,在word字典里面找不到,返回的memory[startIndex]也就是false;
        for(int i = startIndex; i < s.size(); i ++){
            string sub = s.substr(startIndex, i - startIndex + 1);//获取子串
            if(word.find(sub) != word.end() && backtracking(s, i + 1, word, memory)){//判断子串组成单词是否出现在word中,并且接收递归返回值,一起作为条件进行判断
                return true;
            }
        }
        memory[startIndex] = false;//记录本次递归的结果值,方便后续直接使用
        return false;
    }
    bool wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> word(wordDict.begin(), wordDict.end());//将wordDict转换为集合,方便查找
         vector<bool> memory(s.size(), true);//初始化memory数组,初始都为true
         return backtracking(s, 0, word, memory);
    }

时间复杂度:O(2^n)

空间复杂度:O(n)

2. 动态规划

还可以使用动态规划来做这道题。dp[j]就表示对于字符串s中从0到j的字符串是否能够在wordDict中找到可以划分为一个或多个的单词情况,如果有,那么就是true,反之则是false。

那么递归方程是什么呢?这样想,我想要求dp[j],这个时候从下标i到j有一个子串,想要使得dp[j]为true,那么就是要使得i到j的子串在wordDict中能找到&&dp[i]为true,只有这样,才能说明s中的0到j的子串能够被wordDict中的单词构成。

那么如何初始化呢?这里dp[0]=true,帮助理解的话可以这样想,空字符串可以有wordDict中的空字符串构成,这是必然的,但是这也只是帮助理解,没有实际意义,因为题目中明确说明不会出现空字符串,但是dp[0]必须为true,否则后面递推的时候会出问题,所以可以将其看成一个简单的辅助求值的赋值。

如何遍历呢?还是可以先物品或者先背包都行吗?答案是不行

我们要求的比如“applepenapple”,只能出现apple,pen,apple,这样一个顺序,而apple,apple,pen或者pen,apple,apple等都是错误的,也就是说,这里是需要强调顺序的,所以按照组合的先物品后背包就会出现问题,只能按照先背包后物品的排列顺序。

当然还可以从另外一个角度理解,如果先物品再背包,还是以上面applepenapple为例子,先出现一个apple,会将dp[5]赋值为true,然后出现一个pen,会将dp[8]赋值为true,但是此时已经外层循环已经结束了,dp[13]依然为false,没有更新为true,这也是为什么会出错。但是如果这时候再出现一个apple,那么就能将dp[13]更新为true,但是先物品后背包肯定不行了,那怎么办呢?所以只能先背包后物品,能够保证再来一个apple,这也说明了需要使用排列的这种遍历顺序。

如果对于上面的解释没有理解,可以尝试手动模拟,毕竟实践出真知,手动模拟直观且清晰,说不定一下就明白了。

    bool wordBreak(string s, vector<string>& wordDict) {
        vector<bool> dp(s.size() + 1, false);//dp[j]表示字符串s中第0到j的字符是否可以由wordDict中的单词填充完成
        dp[0] = true;//这里可以理解为空字符串是可以由空字符串构成,只是方便理解,对于本题其实无实际意义,因为题目明确说明不会是空字符串,但是这里dp[0]需要等于true,否则后面公式无法推导
        for(int j = 1; j < s.size() + 1; j ++){//先背包
            for(int i = 0; i < wordDict.size(); i ++){//再物品
                if(j >= wordDict[i].size()){
                    string sub = s.substr(j - wordDict[i].size(), wordDict[i].size());//记录子串
                    if(sub == wordDict[i] && dp[j - wordDict[i].size()]) dp[j] = true;
                }
            }
        }
        return dp[s.size()];        
    }

时间复杂度:O(n^2*m)(m为wordDict的大小)

空间复杂度:O(n)

当然了,还可以稍微改一下,将遍历物品层尝试使用下标来遍历,代码要简洁一点。

    bool wordBreak(string s, vector<string>& wordDict) {
        vector<bool> dp(s.size() + 1, false);//dp[j]表示字符串s中第0到j的字符是否可以由wordDict中的单词填充完成
        dp[0] = true;//这里可以理解为空字符串是可以由空字符串构成,只是方便理解,对于本题其实无实际意义,因为题目明确说明不会是空字符串,但是这里dp[0]需要等于true,否则后面公式无法推导
        unordered_set<string> word(wordDict.begin(), wordDict.end());//将wordDict转换成set集合,方便查找
        for(int j = 1; j < s.size() + 1; j ++){//先背包
            for(int i = 0; i < j; i ++){//再物品,这里用下标表示,方便截取子串
                string sub = s.substr(i, j - i);//获取子串
                if(word.find(sub) != word.end() && dp[i]) dp[j] = true;//能够找到子串并且前面d[i]为true,那么这里也为true
            }
        }
        return dp[s.size()];        
    }

时间复杂度:O(n^3)(获取子串的函数的时间复杂度为O(n))

空间复杂度:O(n)

56. 携带矿石资源 

题目描述

你是一名宇航员,即将前往一个遥远的行星。在这个行星上,有许多不同类型的矿石资源,每种矿石都有不同的重要性和价值。你需要选择哪些矿石带回地球,但你的宇航舱有一定的容量限制。 

给定一个宇航舱,最大容量为 C。现在有 N 种不同类型的矿石,每种矿石有一个重量 w[i],一个价值 v[i],以及最多 k[i] 个可用。不同类型的矿石在地球上的市场价值不同。你需要计算如何在不超过宇航舱容量的情况下,最大化你所能获取的总价值。

输入描述

输入共包括四行,第一行包含两个整数 C 和 N,分别表示宇航舱的容量和矿石的种类数量。 

接下来的三行,每行包含 N 个正整数。具体如下: 

第二行包含 N 个整数,表示 N 种矿石的重量。 

第三行包含 N 个整数,表示 N 种矿石的价格。 

第四行包含 N 个整数,表示 N 种矿石的可用数量上限。

输出描述

输出一个整数,代表获取的最大价值。

输入示例

10 3
1 3 4
15 20 30
2 3 2

输出示例

90

提示信息

数据范围:
1 <= C <= 10000;
1 <= N <= 10000;
1 <= w[i], v[i], k[i] <= 10000;

思路:这是一道多重背包的问题。

给定背包容量,给定物品,给定物品的价值,就有可能根据使用物品的次数分为01背包或者完全背包。

如果在前提假设上给定每个物品最多能够使用的次数,那么就成为了多重背包问题。

那怎么求呢?

很简单,转换一下思路,将多重背包里面的元素全部摊开,比如一件物品最多可以使用2次,那就把它拿出来,分为1次加1次,将数量维度减小,直至消失。

于是,这样就变成了我们熟悉的01背包问题了。

对于01背包讲解太多题了,所以转换后也就简单了。

当然有的时候这样转换会出现超时问题,所以这里有两种解决办法。

第一种,计算总的物品数量,然后创建空间;因为超时很大原因就是因为频繁扩增存放数据的数组大小导致;

第二种,在01背包二层循环基础上,新增一层遍历各物品个数的循环。

第一种其实比较简单,所以这里提供第二种方法的解决办法。

#include<bits/stdc++.h>
using namespace std;
int main(){
    int c, n;
    cin >> c >> n;
    vector<int> weight(n);
    vector<int> values(n);
    vector<int> nums(n);
    //接收重量
    for(int i = 0; i < n; i ++){
        cin >> weight[i];
    }
    //接收价格
    for(int i = 0; i < n; i ++){
        cin >> values[i];
    }
    //接收可用数量上限
    for(int i = 0; i < n; i ++){
        cin >> nums[i];
    }
    
    
    vector<int> dp(c + 1, 0);
    for(int i = 0; i < n; i ++){//先物品
        for(int j = c; j >= weight[i]; j --){//再背包,注意只能按照这个顺序
            //上面为01背包的主要部分
            //下面开始遍历每个物品个数
            for(int k = 1; k <= nums[i] && j >= k * weight[i]; k ++){
                dp[j] = max(dp[j], dp[j - k * weight[i]] + k * values[i]);
            }
        }
    }
    cout << dp[c] << endl;
    return 0;
}

时间复杂度:O(k*n*c)

空间复杂度:O(c)

感谢你的阅读,希望我的文章能够给你帮助,如果有帮助,麻烦点赞加收藏,或者点点关注,非常感谢。

如果有什么问题欢迎评论区讨论!

  • 28
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第二十二算法训练营主要涵盖了Leetcode题目中的三道题目,分别是Leetcode 28 "Find the Index of the First Occurrence in a String",Leetcode 977 "有序数组的平方",和Leetcode 209 "长度最小的子数组"。 首先是Leetcode 28题,题目要求在给定的字符串中找到第一个出现的字符的索引。思路是使用双指针来遍历字符串,一个指向字符串的开头,另一个指向字符串的结尾。通过比较两个指针所指向的字符是否相等来判断是否找到了第一个出现的字符。具体实现的代码如下: ```python def findIndex(self, s: str) -> int: left = 0 right = len(s) - 1 while left <= right: if s[left == s[right]: return left left += 1 right -= 1 return -1 ``` 接下来是Leetcode 977题,题目要求对给定的有序数组中的元素进行平方,并按照非递减的顺序返回结果。这里由于数组已经是有序的,所以可以使用双指针的方法来解决问题。一个指针指向数组的开头,另一个指针指向数组的末尾。通过比较两个指针所指向的元素的绝对值的大小来确定哪个元素的平方应该放在结果数组的末尾。具体实现的代码如下: ```python def sortedSquares(self, nums: List[int]) -> List[int]: left = 0 right = len(nums) - 1 ans = [] while left <= right: if abs(nums[left]) >= abs(nums[right]): ans.append(nums[left ** 2) left += 1 else: ans.append(nums[right ** 2) right -= 1 return ans[::-1] ``` 最后是Leetcode 209题,题目要求在给定的数组中找到长度最小的子数组,
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值