目录
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
的完全平方数的最少数量 。完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,
1
、4
、9
和16
都是完全平方数,而3
和11
不是。
思路:当解决了上面的题目后,再来看这道题,就变得简单起来了。
同样是给定背包容量,求装满的时候的最小数量。这里的背包就是给定的整数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*
)
空间复杂度: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)
感谢你的阅读,希望我的文章能够给你帮助,如果有帮助,麻烦点赞加收藏,或者点点关注,非常感谢。
如果有什么问题欢迎评论区讨论!