双非刷leetcode备战2023年蓝桥杯,qwq加油吧,无论结果如何总会有收获!一起加油,我是跟着英雄哥的那个思维导图刷leetcode的,大家也可以看看所有涉及到的题目用leetcode搜索就可以哦,因为避让添加外链,一起加油!!!
动态规划将分为五个板块来讲,本篇为背包问题
文章目录
- 五步走战略
- 推导公式总结:
- 遍历顺序总结:
- 初始化
- 背包问题:
- 01背包:
- 01背包:leetcode相关题目
- 416. 分割等和子集
- 1049. 最后一块石头的重量 II
- 494. 目标和
- 474. 一和零
- 完全背包:
- 好了,很好懂吧,笑死,做做题试试吧。
- 完全背包:leetcode相关题目
- 518. 零钱兑换 II
- 377. 组合总和 Ⅳ
- 70. 爬楼梯(完全背包dp版)
- 322. 零钱兑换
- 279. 完全平方数
- 139. 单词拆分
- 多重背包
- 宝物筛选
- 题目描述
- 输入格式
- 输出格式
- 样例 #1
- 样例输入 #1
- 样例输出 #1
- 提示
五步走战略
-
- 确定dp数组下标含义
-
- 递推公式
-
- 初始化
-
- 遍历顺序
-
- 推导结果
啥也别说了看看自己会走五步了嘛~
推导公式总结:
根据我做题的经验所有的题目的递推公式大概可分为两种;
dp代表最大价值: dp[v]=max(dp[v],dp[v-cost]+weight)
dp代表达到v价值的最大次数: dp[v]=dp[v]+dp[v-cost]
dp代表能不能行: dp[v]=dp[v]||dp[v-cost]
遍历顺序总结:
01背包:物品顺序,容量逆序;
完全背包:物品顺序,容量顺序;
排列先容量再物品,组合先物品再容量;
初始化
最大,初始化为0即可
最小,初始化为能取到的最大值,不然会被覆盖,
然后其他的根据题意取那些特殊的
背包问题:
01背包问题
这是最基本的背包问题,每个物品最多只能放一次。
完全背包问题
第二个基本的背包问题模型,每种物品可以放无限多次。
多重背包问题
每种物品有一个固定的次数上限。
01背包:
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
思路:
这是最基本的背包问题,每个物品最多只能放一次
然后如果是二维的相信大家都会写那一维的怎么写呢? f[i][j]=max(f[i-1][j],f[i-1][j-weight[i]]+value[i]);
这里我们只用考虑放和不放就可以了如果用f[v]来表示这个i的价值的话,放就是f[v-cost]+weight不放的话就是f[v],只有这两种情况因为f[v-cost]已经是在v容量下的最大价值了
所以状态转移方程就是 f[v]=max(f[v],f[v-cost]+weight)
那循环咋写呢:外层正序遍历物品,内层逆序遍历容量
这里我们可以发现在二维数组的时候内层正序逆序都可以但是在一维数组的时候只能逆序了。
这是为什么呢???
倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!
举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15
如果正序遍历
dp[1] = dp[1 - weight[0]] + value[0] = 15
dp[2] = dp[2 - weight[0]] + value[0] = 30
此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。
为什么倒序遍历,就可以保证物品只放入一次呢?
倒序就是先算dp[2]
dp[2] = dp[2 - weight[0]] + value[0] = 15
(dp数组已经都初始化为0)
dp[1] = dp[1 - weight[0]] + value[0] = 15
所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
那么问题又来了,为什么二维dp数组历的时候不用倒序呢?
因为对于二维dp,dp[i][j]
都是通过上一层即dp[i - 1][j]
计算而来,本层的dp[i][j]
并不会被覆盖!
要是不理解可以依靠记忆力,但是十分不建议依靠记忆力,会越用记忆力越不会,千万能理解的别背
好了,很好懂吧,笑死,做做题试试吧。
01背包:leetcode相关题目
416. 分割等和子集
思路:首先,我是真的没看出来这个题用dp做。有点小扯(菜)
这个题我们吧num[i]抽象为物品的重量和价值,而背包的容量为sum/2,转化为01背包问题,在01背包dp后我们便可以得到dp[sum/2]位置上的值,只要这个值和原值一样就return true else return false;
五步走:
-
- 确定dp数组下标含义: dp[i]表示这个数的和,下标i表示的是背包容量
-
- 遍历顺序: 先遍历物品(i),再遍历容量(j)
-
- 然后 递推公式:max(dp[j],dp[j-num[i]]+num[i])
-
- 初始化: 首先:要是数小于两个就分不了了,直接return false;要是和为奇数也分不了了,就出来小数了,直接return false;dp数组全都初始化为0
-
- 推导结果:要是最后我们找到的这个1/2 sum 上的值不等于 1/2sum的话就false否则true
代码:
class Solution
{
public:
bool canPartition(vector<int> &nums)
{
/* 定义dp数组 */
vector<int> dp(10001, 0);
/* 数组大小判断 */
if (nums.size() < 2)
return false;
/* 计算目标值即背包容量 */
int sum = 0;
int bagWight = 0;
for (int i = 0; i < nums.size(); i++)
{
sum += nums[i];
}
/* 和不能为奇数 */
if (sum % 2 == 1)
return false;
/* 计算背包容量 */
bagWight = sum / 2;
/* 遍历 */
for (int i = 0; i < nums.size(); i++)
{ // 遍历物品
for (int j = bagWight; j >= 0; j--)
{ // 遍历容量
/* j是背包容量nums[i]代表物品i所占用的容量 需要判断j >= nums[i]才能放进背包 */
if (j >= nums[i])
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
else
continue;
}
}
if (dp[bagWight] == bagWight)
return true;
return false;
}
};
1049. 最后一块石头的重量 II
思路:首先我知道这个是一个01背包问题了然后关键是我知道了,我也不知道怎么转换。。。
哦哦哦哦哦,我会了,我会转换了,太简单了,哈哈哈哈哈你还不会吧,哈哈哈哈哈
你想想哈要是这个大了就返回相减的相等就消掉的话,我们可以理解为什么呢,让这个石头大的那一半放在一边,小的那一半放在另一边,肯定是大的那一半-小的那一半吧,那这个题是不是就是转换为求如何让大的那一堆-小的那一堆出来的值最小。
如何让大的减小的最小呢,我们这两个堆肯定是平均分的,让一个堆-另一个堆最小就让在不超过sum/2的情况下找出最大的来不就的了
那不超过sum/2找最大值不就是我们十分熟悉的01背包嘛
注意:这个题因为有小数的存在所以最后我们要考虑到用总和来减去求出来的dp[sum/2]的最大值的二倍;
五步走:
-
- 确定dp数组下标含义: dp[i]表示这个石头的和,下标i表示的是背包容量
-
- 遍历顺序: 先遍历石头(i),再遍历容量(j)
-
- 然后 递推公式:max(dp[j],dp[j-stones[i]]+stones[i])
-
- 初始化: dp数组全都初始化为0
-
- 推导结果: return totle-2*dp[sum];
代码部分
- 推导结果: return totle-2*dp[sum];
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int sum = 0;
for(int i=0;i<stones.size();i++)
{
sum+=stones[i];
}
int totle=sum;
sum=sum/2;
vector<int>dp(10001,0);
for(int i=0;i<stones.size();i++)
{
for(int j=sum;j>=0;j--)
{
if(j>=stones[i])
{
dp[j]=max(dp[j],dp[j-stones[i]]+stones[i]);
}
}
}
return totle-2*dp[sum];
}
};
总结:你要不知道这是个01背包真挺难想的。
494. 目标和
思路:这个题和上一个题是一样的,我们首先可以想到把这个数组可以分为+(suma)的和-(sumb)的他们的总和就是sum而我们要找的target=suma-sumb
sum=suma+sumb
那么我们可以得到suma=(target+sum)/2;
那么就可以求让这个数组=suma的情况有多少种,也就转换到了上一个题的思路了;就是01背包的动态规划直接五步走战略,这里让球的是最大值出现的次数,所以可以分为下五步
五步走:
-
- 确定dp数组下标含义: dp[i]表示到这出现的可能的次数,下标i表示的是背包容量
-
- 遍历顺序: 先遍历数(i),再遍历容量(j)
-
- 然后 递推公式: dp[j]=dp[j]+dp[j-nums[i]];//因为我们要求的是最大出现的次数所以这个地方是不选i的次数和选i次数的最大值的求和
-
- 初始化: dp数组全都初始化为0,在dp[0]上初始化为1只有一种可能性
-
- 推导结果: return dp[suma];
代码:
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for(int i=0;i<nums.size();i++)
{
sum+=nums[i];
}//算和
if(sum+target<0)
{
return 0;
}
if((sum+target)%2!=0)
{
return 0;
}
int suma=(sum+target)/2;//背包量
sum=sum/2;
vector<int>dp(10001,0);
dp[0]=1;
for(int i=0;i<nums.size();i++)
{
for(int j=suma;j>=0;j--)
{
if(j>=nums[i])
{
dp[j]=dp[j]+dp[j-nums[i]];
}
}
}
return dp[suma];
}
};
474. 一和零
思路:把背包扩成二维,当测试题做吧不给你们思路了先自己写吧,没啥脑筋急转弯都告诉你是01背包了那肯定就简单了自己写吧!!不会了就从头开始看吧!
代码
class Solution
{
public:
int findMaxForm(vector<string> &strs, int m, int n)
{
vector<int> ling(strs.size(), 0);
vector<int> yi(strs.size(), 0);
for (int i = 0; i < strs.size(); i++)
{
for (int j = 0; j < strs[i].length(); j++)
{
if (strs[i][j] == '0')
{
ling[i]++;
}
if (strs[i][j] == '1')
{
yi[i]++;
}
}
}
//初始化;
int dp[601][601] = {0}; // dp数组代表最大子集个数
for (int i = 0; i < strs.size(); i++)
{
for (int j = m; j >=0; j--)
{
for (int k = n; k >=0; k--)
{
if (ling[i] <= j && yi[i] <= k)
{
dp[j][k] = max(dp[j][k], dp[j - ling[i]][k - yi[i]] + 1);
}
}
}
}
return dp[m][n];
}
};
骗你玩的小笨蛋五步走思路如下:
五步走:
-
- 确定dp数组下标含义: dp[j][k]]表示到这出现的可能的次数,下标j,k示的是存0和存1的背包容量
-
- 遍历顺序: 先遍历价值(j,k),再遍历容量(i)
-
- 然后 递推公式:dp[j][k] = max(dp[j][k], dp[j - ling[i]][k - yi[i]] + 1);
-
- 初始化: dp数组全都初始化为0
-
- 推导结果: return dp[m][n];
好了01背包到这里~ 你学会了嘛~ 我们来进行下一个专题 完全背包
完全背包:
有 N 种物品和一个容量为 V 的背包,每种物品都有无限件可用。放入第 i 种物品
的费用是 Ci,价值是 Wi。求解:将哪些物品装入背包,可使这些物品的耗费的费用总
和不超过背包容量,且价值总和最大。
你想咋做?
反正我想的是既然这个可以无限使用那把这个的特点直接加入到01背包的转移公式中不就ok了
F[i, v] = max{F[i − 1, v − kCi] + kWi) 0 ≤ kCi ≤ v}
就是原来的乘以k倍
这跟 01 背包问题一样有 O(V N) 个状态需要求解,但求解每个状态的时间已经不
是常数了,求解状态 F[i, v] 的时间是 O(v/Ci),总的复杂度可以认为是 O(NV ΣV/Ci),是
比较大的。
还能怎么做?
直接把那无限的使用的物品拆开成他的最大件数件然后直接使用ok不
太ok了~但是时间复杂度没有改变
那再想想为什么我们之前01背包的循环里的第二层 对v的循环要使用逆序的方法来做呢?
是为了只选一次
而这次要选多次
直接把01算法的逆序改为顺序就直接解决了这个完全背包的问题!
背包九讲里是这样讲的:
就是把01背包第二层循环的逆序改为顺序就成功能解决了完全背包的问题
好了,很好懂吧,笑死,做做题试试吧。
完全背包:leetcode相关题目
518. 零钱兑换 II
没啥好讲的:这个是组合问题,就是不考虑各个之间的顺序所以要先遍历物品再遍历容量或者先遍历容量在遍历物品都可以
五步走:
-
- 确定dp数组下标含义: 表示在当前金额下的组合数的个数,下标表示金额容量
-
- 遍历顺序: 先遍历物品(i),再遍历容量(j)(顺序)
-
- 然后 递推公式: dp[j]=dp[j]+dp[j-coins[i]];(和目标和那个一样)
-
- 初始化: 金额为0,那就能选1个,就是不选
-
- 推导结果: return dp[amount];
代码:
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int>dp(amount+1,0);//表示在当前金额下的组合数的个数,下标表示金额容量
dp[0]=1;
for(int i=0;i<coins.size();i++)
{
for(int j=coins[i];j<=amount;j++)
{
dp[j]=dp[j]+dp[j-coins[i]];
}
}
return dp[amount];
}
};
377. 组合总和 Ⅳ
这个是排列问题所以容量循环要在外面,不然就是组合问题了
举个栗子:
如果把遍历nums(物品)放在外循环,遍历target的作为内循环的话,举一个例子:计算dp[4]的时候,结果集只有 {1,3} 这样的集合,不会有{3,1}这样的集合,因为nums遍历放在外层,3只能出现在1后面!
所以就是上个题把循环顺序改一下就好了;
但是这个题还有一个
题目数据保证答案符合 32 位整数范围。
SomeBody leetcode 不讲武德就非让我WA一次
一定要加上dp[j-nums[i]] < INT_MAX - dp[j]来保证dp[j-nums[i]] + dp[j]< INT_MAX 不超出整数范围
dp[j-nums[i]] + dp[j]< INT_MAX直接这样写没有用因为也会超范围
保证过程不超出整数范围!
代码:
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int>dp(target+1,0);//表示在当前金额下的组合数的个数,下标表示金额容量
dp[0]=1;
for(int j=0;j<=target;j++)
{
for(int i=0;i<nums.size();i++)
{
if(nums[i]<=j&& dp[j-nums[i]] < INT_MAX - dp[j])
dp[j]=dp[j]+dp[j-nums[i]];
}
}
return dp[target];
}
};
70. 爬楼梯(完全背包dp版)
思路:排列,完全背包,到n,表示次数,1,2为价格,一共多少台阶就是容量·,懂?
五步走:自己走;
代码:
class Solution {
public:
int climbStairs(int n) {
int dp[46]={0};//方法
dp[0]=1;
for(int i=0;i<=n;i++)
{
for(int j=1;j<=2;j++)
{
if(i-j>=0)
dp[i]=dp[i]+dp[i-j];
}
}
return dp[n];
}
};
要是这个题改成一次能上好几阶台阶,你有超能力了;那就把第二个循环里的2换成m就ac了
322. 零钱兑换
思路:像这种求的是最小次数而不是最大次数的题啊,那个dp数组啊,就要初始化为能取到的最大值,这里我初始化的值是amont+1;然后和之前一样做就行了;
五步走:
-
- 确定dp数组下标含义: dp[i]表示这个数的最小次数,下标i表示的是背包容量
-
- 遍历顺序: 先遍历物品(i),再遍历容量(j)(这个题都行)
-
- 然后 递推公式: dp[j]=min(dp[j],dp[j-coins[i]]+1);
-
- 初始化: 求最小初始化最大。dp[0]为0
-
- 推导结果:这里我是自己又建立了个求最大相加的值,我看看他和amount相等吗相等就返回不相等说明组不成,直接-1别的方案可以评论区
代码:
- 推导结果:这里我是自己又建立了个求最大相加的值,我看看他和amount相等吗相等就返回不相等说明组不成,直接-1别的方案可以评论区
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int>dp(amount+1,);
vector<int>dp1(amount+1,0);
for(int j=1;j<=amount;j++)
{
for(int i=0;i<coins.size();i++)
{
if(coins[i]<=j)
{
dp[j]=min(dp[j],dp[j-coins[i]]+1);
dp1[j]=max(dp1[j],dp1[j-coins[i]]+coins[i]);
}
}
}
if(dp1[amount]==amount)
return dp[amount];
else
return -1;
}
};
279. 完全平方数
思路:自己做,我2分钟一把过
提示:和林俊杰有关;
代码:
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n+1,n+1);
dp[0]=0;
for(int i=0;i<=n;i++)
{
for(int j=0;j*j<=n;j++)
{
if(j*j<=i)
{
dp[i]=min(dp[i],dp[i-j*j]+1);
}
}
}
return dp[n];
}
};
139. 单词拆分
我个人是十分烦这种字符串的题的,我字符串的那种基础比较低我连substr()都不知道是什么鬼东西。。。substr(字符串起点,字符串长度),获取子串;
思路:字符串的完全背包,判断能不能行的问题,选上这个串看看后缀还能不能行,或者不选看看能不能行
五步走:
-
- 确定dp数组下标含义: dp[i]到达i时想不想的等
-
- 遍历顺序: 先遍历物品(i),再遍历容量(j)(这个题都行)(能选多次)
-
- 然后 递推公式: dp[i]=dp[i]||dp[i-wordDict[j].length()];//选或者不选
-
- 初始化:初始化false。dp[0]为true因为没有肯定在这里面
-
- 推导结果:看看到达s.length()位置的时候相不相等;返回return dp[s.length()];
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
vector<bool> dp(s.length(),false);
dp[0]=true;
for(int i=0;i<=s.length();i++)
{
for(int j=0;j<wordDict.size();j++)
{
if(wordDict[j].length()<=i&&s.substr(i-wordDict[j].length(),wordDict[j].length())==wordDict[j])
{
dp[i]=dp[i]||dp[i-wordDict[j].length()];
}
}
}
return dp[s.length()];
}
};
好了,完全背包的题也刷完了,基本上背包的题都刷完了,反正leetcode上是没有多重背包的题了。
多重背包
有 N 种物品和一个容量为 V 的背包。第 i 种物品最多有 Mi 件可用,每件耗费的空间是 Ci,价值是 Wi。求解将哪些物品装入背包可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。
我们可以把他拆成01背包或者01背包和完全背包,就是把一个个的分开就行了,当然这种方法十分的复杂
#include<bits/stdc++.h>
using namespace std;
const int MAXN=101;
int n,V;
int v[MAXN],w[MAXN],s[MAXN];
int f[MAXN];
int main()
{
cin>>n>>V;
for(int i=1;i<=n;i++)
{
cin>>v[i]>>w[i]>>s[i];
}
for(int i=1;i<=n;i++)
{
if(s[i]*v[i]>=V)//转化为完全背包 如果大于容量了了就能随便选了就是完全背包
{
for(int j=v[i];j<=V;j++)
{
f[j]=max(f[j-v[i]]+w[i],f[j]);
}
}
else //转化为 01背包
{
for(int j=V;j>=v[i];j--)
for(int k=s[i];k>=0;k--)
if(j>=k*v[i])
f[j]=max(f[j-k*v[i]]+k*w[i],f[j]);
}
}
cout<<f[V];
return 0;
但是:大神出了新方法了,二进制优化
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5;
int n,V;
int v[MAXN],w[MAXN];
int f[MAXN];
int main()
{
cin>>n>>V;
int cnt=0;//记录新的物体数
for(int i=1,a,b,s;i<=n;i++)
{
cin>>a>>b>>s;
int k=1;
while(k<=s)//实现1,2,4,8件原物品 合成为新物品
{
v[++cnt]=k*a;
w[cnt]=k*b;
s-=k;
k*=2;
}
if(s)
{
v[++cnt]=s*a;
w[cnt]=s*b;
}
}
for(int i=1;i<=cnt;i++)//01背包
{
for(int j=V;j>=v[i];j--)
{
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
cout<<f[V];
return 0;
}
这两个方法我们接着下面的题讲吧
下面我们来做个题提交去洛谷P1776 宝物筛选提交!!我就不放连接了你们都能找到~
宝物筛选
题目描述
终于,破解了千年的难题。小 FF 找到了王室的宝物室,里面堆满了无数价值连城的宝物。
这下小 FF 可发财了,嘎嘎。但是这里的宝物实在是太多了,小 FF 的采集车似乎装不下那么多宝物。看来小 FF 只能含泪舍弃其中的一部分宝物了。
小 FF 对洞穴里的宝物进行了整理,他发现每样宝物都有一件或者多件。他粗略估算了下每样宝物的价值,之后开始了宝物筛选工作:小 FF 有一个最大载重为 W W W 的采集车,洞穴里总共有 n n n 种宝物,每种宝物的价值为 v i v_i vi,重量为 w i w_i wi,每种宝物有 m i m_i mi 件。小 FF 希望在采集车不超载的前提下,选择一些宝物装进采集车,使得它们的价值和最大。
输入格式
第一行为一个整数 n n n 和 W W W,分别表示宝物种数和采集车的最大载重。
接下来 n n n 行每行三个整数 v i , w i , m i v_i,w_i,m_i vi,wi,mi。
输出格式
输出仅一个整数,表示在采集车不超载的情况下收集的宝物的最大价值。
样例 #1
样例输入 #1
4 20
3 9 3
5 9 1
9 4 2
8 1 3
样例输出 #1
47
提示
对于 30 % 30\% 30% 的数据, n ≤ ∑ m i ≤ 1 0 4 n\leq \sum m_i\leq 10^4 n≤∑mi≤104, 0 ≤ W ≤ 1 0 3 0\le W\leq 10^3 0≤W≤103。
对于 100 % 100\% 100% 的数据, n ≤ ∑ m i ≤ 1 0 5 n\leq \sum m_i \leq 10^5 n≤∑mi≤105, 0 ≤ W ≤ 4 × 1 0 4 0\le W\leq 4\times 10^4 0≤W≤4×104, 1 ≤ n ≤ 100 1\leq n\le 100 1≤n≤100。
思路:直接多重背包做,纯纯模板题,接着这个题讲讲思路哈;
代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 5000000;
int n, k, v[maxn], w[maxn], m[maxn],cnt,V[maxn],W[maxn],dp[maxn];
int main() {
//初始化
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> v[i] >> w[i] >> m[i];
}
//拆分的另一种写法
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m[i]; j <<= 1) {
V[++cnt] = j * v[i];
W[cnt] = j * w[i];
m[i] -= j;
}
if (m[i] != 0) {
V[++cnt] = m[i] * v[i];
W[cnt] = m[i] * w[i];
}
}
//01背包
for (int i = 1; i <= cnt; i++) {
for (int j =k; j>=W[i]; j--) {
dp[j] = max(dp[j], dp[j - W[i]] + V[i]);
}
}
cout << dp[k] << endl;
}
好了好了~ 就到这里吧,以后在有题的时候再补充~我们的背包dp问题先到这里吧!
Love is worth years.❤
热爱可抵岁月漫长。
本文部分思路来源于网络(做力扣看题解!)如有侵权联系删除~背包部分的内容思路基本来自于大神的背包九讲