【leetcode刷题】动态规划 Part 3 背包
今天来做背包部分的总结
首先,我们先明确一下,背包就以下几种
0-1背包、完全背包、多重背包、分组背包
我们可能会碰到三种问题
- 1 .求背包能装下的最大价值(最常见、最简单)
- 2 .求背包满足某条件的方案数
- 3 .求
恰好装满背包时的最大价值(最不常见)
这里先来说一下第三种,通常见于0-1背包和完全背包:
我们要知道,在转移的时候,我们通常由上一个状态转移来
如果上一个状态未装满,那么这个状态当然也就未装满了
所以,我们需要打个标记,只有上一个状态装满的时候才能转移到下一个状态,其他无异
。
然后说一下分组背包的求方案问题
我们都知道,分组背包的第一类问题是直接先枚举分组数目,然后倒序枚举容量,接着再枚举本组中的物品
但是,如果到第二类问题中,我们的DP数组会被污染
怎么办?此时就需要新建一个临时数组,并且是初始化的,最后与DP数组交换
for(int i=1;i<=n;i++){
vector<int>temp(target+5,0);
for(int j=target;j>=0;j--){
for(int v=1;v<=k;v++){
if(j>=v){
temp[j]+=dp[j-v]%INF;
temp[j]%=INF;
}
}
}
dp.swap(temp);
}
接着我们来看题
3180:执行操作可获得的最大总奖励(1)
最初,你的总奖励 x 为 0,所有下标都是 未标记 的。你可以执行以下操作 任意次 :
- 从区间 [0, n - 1] 中选择一个 未标记 的下标 i。
- 如果 rewardValues[i] 大于 你当前的总奖励 x,则将 rewardValues[i] 加到 x 上(即 x = x + rewardValues[i]),并 标记 下标 i。
以整数形式返回执行最优操作能够获得的 最大 总奖励。
示例 1:
输入:rewardValues = [1,1,3,3]
输出:4
解释:
依次标记下标 0 和 2,总奖励为 4,这是可获得的最大值。
示例 2:
输入:rewardValues = [1,6,4,3,2]
输出:11
解释:
依次标记下标 0、2 和 1。总奖励为 11,这是可获得的最大值。
提示:
- 1 <= rewardValues.length <= 2000
- 1 <= rewardValues[i] <= 2000
Solution
这题有点像抖机灵,通过观察可知,应该先取出比较小的数
然后,我们采用一种类似0-1背包的处理手法,来推出转移方程
class Solution {
public:
int maxTotalReward(vector<int>& rewardValues) {
int n=rewardValues.size();
const int maxn=40005;
const int INF=1e9;
vector<int>dp(maxn,-INF);
sort(rewardValues.begin(),rewardValues.end());
dp[0]=0;
for(int i=0;i<=n-1;i++){
for(int j=0;j<rewardValues[i];j++){
dp[j+rewardValues[i]]=max(dp[j+rewardValues[i]],dp[j]+rewardValues[i]);
}
}
int res=0;
for(int i=0;i<=maxn-1;i++){
if(dp[i]>0) res=max(res,i);
}
return res;
}
};
474:一和零
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
示例 1:
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
示例 2:
输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。
提示:
- 1 <= strs.length <= 600
- 1 <= strs[i].length <= 100
- strs[i] 仅由 '0' 和 '1' 组成
- 1 <= m, n <= 100
Solution
两重费用的背包,跑的时候固定一维,跑另一维,跑两次
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
int l=strs.size();
const int INF=1e9;
vector<vector<int>>ma(l+5,vector<int>(2,0));
vector<vector<int>>dp(m+5,vector<int>(n+5,0));
for(int i=0;i<=l-1;i++){
int temp=0,temp_1=0;
for(int j=0;j<=strs[i].length()-1;j++){
if(strs[i][j]=='0') temp++;
else temp_1++;
}
ma[i][0]=temp;
ma[i][1]=temp_1;
}
for(int i=0;i<=l-1;i++){
for(int j=m;j>=ma[i][0];j--){
for(int k=n;k>=ma[i][1];k--){
dp[j][k]=max(dp[j-ma[i][0]][k-ma[i][1]]+1,dp[j][k]);
}
}
}
return dp[m][n];
}
};
1049:最后一块石头的重量(2)
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
- 如果 x == y,那么两块石头都会被完全粉碎;
- 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
示例 1:
输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
示例 2:
输入:stones = [31,26,33,21,40]
输出:5
提示:
- 1 <= stones.length <= 30
- 1 <= stones[i] <= 100
Solution
我们通过归纳法发现,其实当我们合并两块石头的石头,总质量减少了某块石头的2倍
那么,只需要计算所有石头质量2倍的背包中,最后剩下的最小空间是多少就可以了
1774: 最接近目标价格的甜点成本
- 必须选择 一种 冰激凌基料。
- 可以添加 一种或多种 配料,也可以不添加任何配料。
- 每种类型的配料 最多两份 。
给你以下三个输入:
- baseCosts ,一个长度为 n 的整数数组,其中每个 baseCosts[i] 表示第 i 种冰激凌基料的价格。
- toppingCosts,一个长度为 m 的整数数组,其中每个 toppingCosts[i] 表示 一份 第 i 种冰激凌配料的价格。
- target ,一个整数,表示你制作甜点的目标价格。
你希望自己做的甜点总成本尽可能接近目标价格 target 。
返回最接近 target 的甜点成本。如果有多种方案,返回 成本相对较低 的一种。
示例 1:
输入:baseCosts = [1,7], toppingCosts = [3,4], target = 10
输出:10
解释:考虑下面的方案组合(所有下标均从 0 开始):
- 选择 1 号基料:成本 7
- 选择 1 份 0 号配料:成本 1 x 3 = 3
- 选择 0 份 1 号配料:成本 0 x 4 = 0
总成本:7 + 3 + 0 = 10 。
示例 2:
输入:baseCosts = [2,3], toppingCosts = [4,5,100], target = 18
输出:17
解释:考虑下面的方案组合(所有下标均从 0 开始):
- 选择 1 号基料:成本 3
- 选择 1 份 0 号配料:成本 1 x 4 = 4
- 选择 2 份 1 号配料:成本 2 x 5 = 10
- 选择 0 份 2 号配料:成本 0 x 100 = 0
总成本:3 + 4 + 10 + 0 = 17 。不存在总成本为 18 的甜点制作方案。
示例 3:
输入:baseCosts = [3,10], toppingCosts = [2,5], target = 9
输出:8
解释:可以制作总成本为 8 和 10 的甜点。返回 8 ,因为这是成本更低的方案。
示例 4:
输入:baseCosts = [10], toppingCosts = [1], target = 1
输出:10
解释:注意,你可以选择不添加任何配料,但你必须选择一种基料。
提示:
- n == baseCosts.length
- m == toppingCosts.length
- 1 <= n, m <= 10
- 1 <= baseCosts[i], toppingCosts[i] <= \(10^4\)
- 1 <= target <= \(10^4\)
Solution
从数据范围可以看出,我们将其化成分组背包
同时,这种可行性的题目,我们的DP数组最好写成bool型变量,便于阅读
class Solution {
public:
int closestCost(vector<int>& baseCosts, vector<int>& toppingCosts, int target) {
int n=baseCosts.size();
int m=toppingCosts.size();
vector<vector<int>>ma(m+5,vector<int>(2,0));
vector<bool>dp(20005,false);
for(int i=0;i<=n-1;i++) dp[baseCosts[i]]=true;
for(int i=0;i<=m-1;i++){
ma[i][0]=toppingCosts[i];
ma[i][1]=toppingCosts[i]*2;
}
for(int i=0;i<=m-1;i++){
for(int j=2*target;j>=0;j--){
for(int k=0;k<=1;k++){
if(j>=ma[i][k]) dp[j]=dp[j]|dp[j-ma[i][k]];
}
}
}
int res=0;
for(int i=0;i<=target;i++){
if(dp[target-i]){
res=target-i;
break;
}
else if(dp[target+i]){
res=target+i;
break;
}
}
if(res!=0) return res;
for(int i=0;i<=20005;i++){
if(dp[i]){
res=i;
break;
}
}
return res;
}
};
879:盈利计划
第 i 种工作会产生 profit[i] 的利润,它要求 group[i] 名成员共同参与。如果成员参与了其中一项工作,就不能参与另一项工作。
工作的任何至少产生 minProfit 利润的子集称为 盈利计划 。并且工作的成员总数最多为 n 。
有多少种计划可以选择?因为答案很大,所以 返回结果模 10^9 + 7 的值。
示例 1:
输入:n = 5, minProfit = 3, group = [2,2], profit = [2,3]
输出:2
解释:至少产生 3 的利润,该集团可以完成工作 0 和工作 1 ,或仅完成工作 1 。
总的来说,有两种计划。
示例 2:
输入:n = 10, minProfit = 5, group = [2,3,5], profit = [6,7,8]
输出:7
解释:至少产生 5 的利润,只要完成其中一种工作就行,所以该集团可以完成任何工作。
有 7 种可能的计划:(0),(1),(2),(0,1),(0,2),(1,2),以及 (0,1,2) 。
提示:
- 1 <= n <= 100
- 0 <= minProfit <= 100
- 1 <= group.length <= 100
- 1 <= group[i] <= 100
- profit.length == group.length
- 0 <= profit[i] <= 100
Solution
这里就涉及到了一种我们很常用的思想,也就是正难则反
题目中让我们求大于等于minprofit的方案数
那么,我们可以把全部方案数求出来,再减去小于minprofit的方案数
class Solution {
public:
int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
int m=group.size();
const int INF=1e9+7;
vector<int>dp(n+5,0);
dp[0]=1;
for(int i=0;i<=m-1;i++){
for(int j=n;j>=group[i];j--){
dp[j]+=dp[j-group[i]]%INF;
dp[j]%=INF;
}
}
vector<vector<int>>fdp(n+5,vector<int>(minProfit+5,0));
int ans_1=0;
for(int i=0;i<=n;i++){
ans_1+=dp[i]%INF;ans_1%=INF;
}
fdp[0][0]=1;
for(int i=0;i<=m-1;i++){
for(int j=n;j>=group[i];j--){
for(int k=minProfit-1;k>=profit[i];k--){
fdp[j][k]+=fdp[j-group[i]][k-profit[i]]%INF;
fdp[j][k]%=INF;
}
}
}
int ans_2=0;
for(int i=0;i<=n;i++){
for(int j=0;j<=minProfit-1;j++){
ans_2+=fdp[i][j]%INF;
ans_2%=INF;
}
}
int res=ans_1-ans_2;
if(res<=0) res+=INF;
return res%INF;
}
};
3082:求出所有子序列的能量和
一个整数数组的 能量 定义为和 等于 k 的子序列的数目。
请你返回 nums 中所有子序列的 能量和 。
由于答案可能很大,请你将它对 109 + 7 取余 后返回。
示例 1:
输入: nums = [1,2,3], k = 3
输出: 6
解释:
总共有 5 个能量不为 0 的子序列:
- 子序列 [1,2,3] 有 2 个和为 3 的子序列:[1,2,3] 和 [1,2,3] 。
- 子序列 [1,2,3] 有 1 个和为 3 的子序列:[1,2,3] 。
- 子序列 [1,2,3] 有 1 个和为 3 的子序列:[1,2,3] 。
- 子序列 [1,2,3] 有 1 个和为 3 的子序列:[1,2,3] 。
- 子序列 [1,2,3] 有 1 个和为 3 的子序列:[1,2,3] 。
所以答案为 2 + 1 + 1 + 1 + 1 = 6 。
示例 2:
输入: nums = [2,3,3], k = 5
输出: 4
解释:
总共有 3 个能量不为 0 的子序列:
- 子序列 [2,3,3] 有 2 个子序列和为 5 :[2,3,3] 和 [2,3,3] 。
- 子序列 [2,3,3] 有 1 个子序列和为 5 :[2,3,3] 。
- 子序列 [2,3,3] 有 1 个子序列和为 5 :[2,3,3] 。
所以答案为 2 + 1 + 1 = 4 。
示例 3:
输入: nums = [1,2,3], k = 7
输出: 0
解释:不存在和为 7 的子序列,所以 nums 的能量和为 0 。
提示:
- 1 <= n <= 100
- 1 <= nums[i] <= 104
- 1 <= k <= 100
Solution
这道题,我们很容易看出,如果选出了和为k的满序列,剩下的取法就一目了然了
class Solution {
public:
int sumOfPower(vector<int>& nums, int k) {
int n=nums.size();
const int INF=1e9+7;
vector<vector<int>>dp(k+5,vector<int>(n+5,0));
dp[0][0]=1;
for(int i=0;i<=n-1;i++){
for(int j=k;j>=nums[i];j--){
for(int v=1;v<=n;v++){
dp[j][v]+=dp[j-nums[i]][v-1]%INF;
dp[j][v]%=INF;
}
}
}
long long ans=0;
long long temp=1;
for(int i=n;i>=1;i--){
temp=temp%INF;
ans+=(long long)((dp[k][i]%INF)*temp)%INF;
temp*=2;
}
int res=ans%INF;
return res;
}
};
956:最高的广告牌
你有一堆可以焊接在一起的钢筋 rods。举个例子,如果钢筋的长度为 1、2 和 3,则可以将它们焊接在一起形成长度为 6 的支架。
返回 广告牌的最大可能安装高度 。如果没法安装广告牌,请返回 0 。
示例 1:
输入:[1,2,3,6]
输出:6
解释:我们有两个不相交的子集 {1,2,3} 和 {6},它们具有相同的和 sum = 6。
示例 2:
输入:[1,2,3,4,5,6]
输出:10
解释:我们有两个不相交的子集 {2,3,5} 和 {4,6},它们具有相同的和 sum = 10。
示例 3:
输入:[1,2]
输出:0
解释:没法安装广告牌,所以返回 0。
提示:
- 0 <= rods.length <= 20
- 1 <= rods[i] <= 1000
- sum(rods[i]) <= 5000
Solution
这道题就是我们的-1-0-1背包,做的时候其实不会,是看了题解后才有所感悟的
因为它的状态设计之前从来没见过,说实话一般不会做也是卡在状态设计上面
我们令\(dp[i][j]\)的第一维是当前遍历到的物品,第二维是当前第一组与第二组的差,整个数组代表的是第一组的和
因为差有负数,我们需要一个偏移量,这个之前介绍过了
转移的时候,转移方程就按照是否取这个物品,以及取了放在哪一组来区分
最后取出\(dp[n][ans]\)就可以
这题还可以让\(dp[i][j]\)数组表示其它的,就是转移方程也会发生一点小变化
class Solution {
public:
int tallestBillboard(vector<int>& rods) {
int n=rods.size();
int ans=0;
for(int num:rods) ans+=num;
const int INF=1e9+7;
vector<vector<int>>dp(n+5,vector<int>(2*ans+5,-INF));
dp[0][ans]=0;
for(int i=1;i<=n;i++){
for(int j=2*ans;j>=0;j--){
dp[i][j]=dp[i-1][j];
if(j-rods[i-1]>=0) dp[i][j]=max(dp[i][j],dp[i-1][j-rods[i-1]]);
if(j+rods[i-1]<=2*ans) dp[i][j]=max(dp[i][j],dp[i-1][j+rods[i-1]]+rods[i-1]);
}
}
return dp[n][ans];
}
};
2518:好分区的数目
分区 的定义是:将数组划分成两个有序的 组 ,并满足每个元素 恰好 存在于 某一个 组中。如果分区中每个组的元素和都大于等于 k ,则认为分区是一个好分区。
返回 不同 的好分区的数目。由于答案可能很大,请返回对 109 + 7 取余 后的结果。
如果在两个分区中,存在某个元素 nums[i] 被分在不同的组中,则认为这两个分区不同。
示例 1:
输入:nums = [1,2,3,4], k = 4
输出:6
解释:好分区的情况是 ([1,2,3], [4]), ([1,3], [2,4]), ([1,4], [2,3]), ([2,3], [1,4]), ([2,4], [1,3]) 和 ([4], [1,2,3]) 。
示例 2:
输入:nums = [3,3,3], k = 4
输出:0
解释:数组中不存在好分区。
示例 3:
输入:nums = [6,6], k = 2
输出:2
解释:可以将 nums[0] 放入第一个分区或第二个分区中。
好分区的情况是 ([6], [6]) 和 ([6], [6]) 。
提示:
- 1 <= nums.length, k <= 1000
- 1 <= nums[i] <= \(10^9\)
Solution
还是正难则反的思想,如果两个都大于等于k,那么其他方案就是至少有一个小于等于k,而我们可以看出至多存在一个小于k的组,所以就显然了
class Solution {
public:
int countPartitions(vector<int>& nums, int k) {
int n=nums.size();
long long ans=0;
const int INF=1e9+7;
vector<int>dp(k+5,0);
dp[0]=1;
for(int num:nums) ans+=num;
if(k>ans/2) return 0;
for(int i=0;i<=n-1;i++){
for(int j=k;j>=nums[i];j--){
dp[j]+=dp[j-nums[i]]%INF;
dp[j]%=INF;
}
}
long long res=0;
for(int i=0;i<=k-1;i++){
res+=dp[i]%INF;
res%=INF;
}
res=res*2%INF;
long long temp=1;
for(int i=0;i<=n-1;i++){
temp*=2;
temp%=INF;
}
if(temp<res) return (temp-res)%INF+INF;
else return (temp-res)%INF;
}
};
2742:给墙壁刷油漆
- 一位需要 付费 的油漆匠,刷第 i 堵墙需要花费 time[i] 单位的时间,开销为 cost[i] 单位的钱。
- 一位 免费 的油漆匠,刷 任意 一堵墙的时间为 1 单位,开销为 0 。但是必须在付费油漆匠 工作 时,免费油漆匠才会工作。
请你返回刷完 n 堵墙最少开销为多少。
示例 1:
输入:cost = [1,2,3,2], time = [1,2,3,2]
输出:3
解释:下标为 0 和 1 的墙由付费油漆匠来刷,需要 3 单位时间。同时,免费油漆匠刷下标为 2 和 3 的墙,需要 2 单位时间,开销为 0 。总开销为 1 + 2 = 3 。
示例 2:
输入:cost = [2,3,4,2], time = [1,1,1,1]
输出:4
解释:下标为 0 和 3 的墙由付费油漆匠来刷,需要 2 单位时间。同时,免费油漆匠刷下标为 1 和 2 的墙,需要 2 单位时间,开销为 0 。总开销为 2 + 2 = 4 。
提示:
- 1 <= cost.length <= 500
- cost.length == time.length
- 1 <= cost[i] <= \(10^6\)
- 1 <= time[i] <= 500
Solution
要让费用最小,那么免费的工程师干的活就要最多
我们设免费的工程师选了\(m\)堵墙,分别为\(a_1,\ldots a_m\),那么有以下公式
移项得
就是一个简单的0-1背包问题了
class Solution {
public:
int paintWalls(vector<int>& cost, vector<int>& time) {
int n=cost.size();
int ans=0;
int sum=0;
const int INF=1e9+7;
for(int num:cost) sum+=num;
for(int num:time) ans+=num;
vector<int>dp(ans+5,-INF);
dp[0]=0;
for(int i=0;i<=n-1;i++){
for(int j=ans;j>=time[i]+1;j--){
dp[j]=max(dp[j],dp[j-time[i]-1]+cost[i]);
}
}
int res=0;
for(int i=0;i<=ans;i++) res=max(res,dp[i]);
return sum-res;
}
};
3287:求出数组中最大序列值
定义长度为 2 * x 的序列 seq 的 值 为:
- (seq[0] OR seq[1] OR ... OR seq[x - 1]) XOR (seq[x] OR seq[x + 1] OR ... OR seq[2 * x - 1]).
请你求出 nums 中所有长度为 2 * k 的 子序列 的 最大值 。
示例 1:
输入:nums = [2,6,7], k = 1
输出:5
解释:
子序列 [2, 7] 的值最大,为 2 XOR 7 = 5 。
示例 2:
输入:nums = [4,2,5,6,7], k = 2
输出:2
解释:
子序列 [4, 5, 6, 7] 的值最大,为 (4 OR 5) XOR (6 OR 7) = 2 。
提示:
- 2 <= nums.length <= 400
- 1 <= nums[i] < 27
- 1 <= k <= nums.length / 2
Solution
对于异或的题目,我们一般考虑按位来做
然后这题就卡死了,为什么?
因为这题实际上还是个序列划分问题
对于这种题目的处理策略一般还是,找出分界点,左右两边分别计算
这题就是 我们定义一个\(dp[i][j][k]\) 表示遍历到第i个时,已经加入了j个,此时的异或值为k是否可能
同样,反方向再跑一次,最后枚举分界点就可以了
class Solution {
public:
int maxValue(vector<int>& nums, int k) {
int n=nums.size();
const int INF=1e9+7;
vector<vector<vector<bool>>>dp(n+5,vector<vector<bool>>(k+5,vector<bool>(129,false)));
vector<vector<vector<bool>>>fdp(n+5,vector<vector<bool>>(k+5,vector<bool>(129,false)));
dp[0][0][0]=true;
fdp[n+1][0][0]=true;
for(int i=0;i<=n-1;i++){
for(int j=0;j<=k;j++){
for(int v=0;v<=127;v++){
if(!dp[i][j][v]) continue;
dp[i+1][j][v]=dp[i][j][v]|dp[i+1][j][v];
if(j<=k-1) dp[i+1][j+1][v|nums[i]]=dp[i+1][j+1][v|nums[i]]|dp[i][j][v];
}
}
}
for(int i=n+1;i>=2;i--){
for(int j=0;j<=k;j++){
for(int v=0;v<=127;v++){
if(!fdp[i][j][v]) continue;
fdp[i-1][j][v]=fdp[i-1][j][v]|fdp[i][j][v];
if(j<=k-1) fdp[i-1][j+1][v|nums[i-2]]=fdp[i][j][v]|fdp[i-1][j+1][v|nums[i-2]];
}
}
}
int ans=0;
for(int i=k;i<=n-k;i++){
for(int j=0;j<=127;j++){
for(int v=0;v<=127;v++){
if(dp[i][k][j]&&fdp[i+1][k][v]) ans=max(ans,j^v);
}
}
}
return ans;
}
};
1449:数位成本和为目标值的最大数字
- 给当前结果添加一个数位(i + 1)的成本为 cost[i] (cost 数组下标从 0 开始)。
- 总成本必须恰好等于 target 。
- 添加的数位中没有数字 0 。
由于答案可能会很大,请你以字符串形式返回。
如果按照上述要求无法得到任何整数,请你返回 "0" 。
示例 1:
输入:cost = [4,3,2,5,6,7,2,5,5], target = 9
输出:"7772"
解释:添加数位 '7' 的成本为 2 ,添加数位 '2' 的成本为 3 。所以 "7772" 的代价为 23+ 31 = 9 。 "977" 也是满足要求的数字,但 "7772" 是较大的数字。
数字 成本
1 -> 4
2 -> 3
3 -> 2
4 -> 5
5 -> 6
6 -> 7
7 -> 2
8 -> 5
9 -> 5
示例 2:
输入:cost = [7,6,5,5,5,6,8,7,8], target = 12
输出:"85"
解释:添加数位 '8' 的成本是 7 ,添加数位 '5' 的成本是 5 。"85" 的成本为 7 + 5 = 12 。
示例 3:
输入:cost = [2,4,6,2,4,6,4,4,4], target = 5
输出:"0"
解释:总成本是 target 的条件下,无法生成任何整数。
示例 4:
输入:cost = [6,10,15,40,40,40,40,40,40], target = 47
输出:"32211"
提示:
- cost.length == 9
- 1 <= cost[i] <= 5000
- 1 <= target <= 5000
Solution
其实就是上面所说的第三类问题的具体题目
一个问题:怎么定义“小于”?
class Solution {
public:
string largestNumber(vector<int>& cost, int target) {
int n=cost.size();
vector<string>dp(target+5,"");
dp[0]="0";
for(int i=0;i<=8;i++){
for(int j=cost[i];j<=target;j++){
if(dp[j-cost[i]]=="") continue;
if(dp[j-cost[i]]=="0"){
dp[j]=(char)((i+1)+'0');
continue;
}
if(dp[j].length()<dp[j-cost[i]].length()+1||(dp[j].length()==dp[j-cost[i]].length()+1&&dp[j]<(char)((i+1)+'0')+dp[j-cost[i]])){
dp[j]=(char)((i+1)+'0')+dp[j-cost[i]];
}
}
}
return dp[target]==""?"0":dp[target];
}
};
3592:硬币面值还原
给你一个 从 1 开始计数 的整数数组 numWays,其中 numWays[i] 表示使用某些 固定 面值的硬币(每种面值可以使用无限次)凑出总金额 i 的方法数。每种面值都是一个 正整数 ,并且其值 最多 为 numWays.length。
然而,具体的硬币面值已经 丢失 。你的任务是还原出可能生成这个 numWays 数组的面值集合。
返回一个按从小到大顺序排列的数组,其中包含所有可能的 唯一 整数面值。
如果不存在这样的集合,返回一个 空 数组。
示例 1:
输入: numWays = [0,1,0,2,0,3,0,4,0,5]
输出: [2,4,6]
解释:
金额 方法数 解释 1 0 无法用硬币凑出总金额 1。 2 1 唯一的方法是 [2]。 3 0 无法用硬币凑出总金额 3。 4 2 可以用 [2, 2] 或 [4]。 5 0 无法用硬币凑出总金额 5。 6 3 可以用 [2, 2, 2]、[2, 4] 或 [6]。 7 0 无法用硬币凑出总金额 7。 8 4 可以用 [2, 2, 2, 2]、[2, 2, 4]、[2, 6] 或 [4, 4]。 9 0 无法用硬币凑出总金额 9。 10 5 可以用 [2, 2, 2, 2, 2]、[2, 2, 2, 4]、[2, 4, 4]、[2, 2, 6] 或 [4, 6]。
示例 2:
输入: numWays = [1,2,2,3,4]
输出: [1,2,5]
解释:
金额 方法数 解释 1 1 唯一的方法是 [1]。 2 2 可以用 [1, 1] 或 [2]。 3 2 可以用 [1, 1, 1] 或 [1, 2]。 4 3 可以用 [1, 1, 1, 1]、[1, 1, 2] 或 [2, 2]。 5 4 可以用 [1, 1, 1, 1, 1]、[1, 1, 1, 2]、[1, 2, 2] 或 [5]。
示例 3:
输入: numWays = [1,2,3,4,15]
输出: []
解释:
没有任何面值集合可以生成该数组。
提示:
- 1 <= numWays.length <= 100
- 0 <= numWays[i] <= 2 * 108
Solution
就是完全背包第二类问题的反向版
一开始想的是,找到每个为1的值,接着更新dp数组
但是,如果是一直这样,就有点像递归了,而且不知道深度的情况下很可怕
事实上,我们从左到右遍历原有的dp数组,找出一个当前最小的硬币,直接更新现有的dp数组,就可以避免这个问题
class Solution {
public:
vector<int> findCoins(vector<int>& numWays) {
int n=numWays.size();
vector<int>dp(n+5,0);
dp[0]=1;
vector<int>ma;
for(int i=1;i<=n;i++){
if(numWays[i-1]==dp[i]) continue;
if(numWays[i-1]-1!=dp[i]) return {};
ma.push_back(i);
for(int j=i;j<=n;j++){
dp[j]+=dp[j-i];
}
}
return ma;
}
};
2902:和带限制的子多重集合的数目
给你一个下标从 0 开始的非负整数数组 nums 和两个整数 l 和 r 。
请你返回 nums 中子多重集合的和在闭区间 [l, r] 之间的 子多重集合的数目 。
由于答案可能很大,请你将答案对 109 + 7 取余后返回。
子多重集合 指的是从数组中选出一些元素构成的 无序 集合,每个元素 x 出现的次数可以是 0, 1, ..., occ[x] 次,其中 occ[x] 是元素 x 在数组中的出现次数。
注意:
- 如果两个子多重集合中的元素排序后一模一样,那么它们两个是相同的 子多重集合 。
- 空 集合的和是 0 。
示例 1:
输入:nums = [1,2,2,3], l = 6, r = 6
输出:1
解释:唯一和为 6 的子集合是 {1, 2, 3} 。
示例 2:
输入:nums = [2,1,4,2,7], l = 1, r = 5
输出:7
解释:和在闭区间 [1, 5] 之间的子多重集合为 {1} ,{2} ,{4} ,{2, 2} ,{1, 2} ,{1, 4} 和 {1, 2, 2} 。
示例 3:
输入:nums = [1,2,1,3,5,2], l = 3, r = 5
输出:9
解释:和在闭区间 [3, 5] 之间的子多重集合为 {3} ,{5} ,{1, 2} ,{1, 3} ,{2, 2} ,{2, 3} ,{1, 1, 2} ,{1, 1, 3} 和 {1, 2, 2} 。
提示:
- 1 <= nums.length <= 2 * 104
- 0 <= nums[i] <= 2 * 104
- nums 的和不超过 2 * 104 。
- 0 <= l <= r <= 2 * 104
Solution
这里就要介绍一下多重背包第二类问题的一种优化方法了,即前缀和优化
首先,我们回到朴素的多重背包,应有以下公式
那么,我们是否可以使用一个前缀和数组来优化(使用同余)?
这个式子变为
然后,因为\(dp[i-1][j]\)在加前缀和之后就没用了,所以我们还是可以采用滚动数组优化一下空间
于是有以下代码(注意滚动数组优化后要倒序遍历)
class Solution {
public:
int countSubMultisets(vector<int>& nums, int l, int r) {
const int INF=1e9+7;
sort(nums.begin(),nums.end());
int len=nums.size();
vector<vector<int>>ma(len+5,vector<int>(2,0));
int temp=0;
int n=0;
int cntzero=0;
for(int i=0;i<=len-1;i++){
temp++;
if(i==len-1||nums[i]!=nums[i+1]){
if(nums[i]==0){
cntzero=temp;
temp=0;
continue;
}
ma[n][0]=nums[i];
ma[n][1]=temp;
n++;temp=0;
}
}
vector<int>dp(20005,0);
dp[0]=cntzero+1;
for(int i=0;i<=n-1;i++){
for(int j=ma[i][0];j<=r;j++){
dp[j]=(dp[j]+dp[j-ma[i][0]])%INF;
}
for(int j=r;j>=ma[i][0]*(ma[i][1]+1);j--){
dp[j]=(dp[j]-dp[j-(ma[i][1]+1)*ma[i][0]]+INF)%INF;
}
}
long long ans=0;
for(int i=l;i<=r;i++){
ans+=dp[i]%INF;
ans%=INF;
}
return ans%INF;
}
};
1981:最小化目标值与所选元素的差
给你一个大小为 m x n 的整数矩阵 mat 和一个整数 target 。
从矩阵的 每一行 中选择一个整数,你的目标是 最小化 所有选中元素之 和 与目标值 target 的 绝对差 。
返回 最小的绝对差 。
a 和 b 两数字的 绝对差 是 a - b 的绝对值。
示例 1:

输入:mat = [ [1,2,3],[4,5,6],[7,8,9] ], target = 13
输出:0
解释:一种可能的最优选择方案是:
- 第一行选出 1
- 第二行选出 5
- 第三行选出 7
所选元素的和是 13 ,等于目标值,所以绝对差是 0 。
示例 2:

输入:mat = [ [1],[2],[3] ], target = 100
输出:94
解释:唯一一种选择方案是:
- 第一行选出 1
- 第二行选出 2
- 第三行选出 3
所选元素的和是 6 ,绝对差是 94 。
示例 3:

输入:mat = [ [1,2,9,8,7] ], target = 6
输出:1
解释:最优的选择方案是选出第一行的 7 。
绝对差是 1 。
提示:
- m == mat.length
- n == mat[i].length
- 1 <= m, n <= 70
- 1 <= mat[i][j] <= 70
- 1 <= target <= 800
Solution
事实上,我们在分组背包中进行bool型dp的时候也要使用第二类的处理方法
class Solution {
public:
int minimizeTheDifference(vector<vector<int>>& mat, int target) {
int m=mat.size();
int n=mat[0].size();
vector<bool>dp(2*target+5,false);
dp[0]=true;
for(int i=0;i<=m-1;i++){
vector<bool>temp(2*target+5,false);
for(int j=2*target;j>=0;j--){
for(int v=0;v<=n-1;v++){
if(j>=mat[i][v]) temp[j]=temp[j]|dp[j-mat[i][v]];
}
}
dp.swap(temp);
}
int sum=0;
for(int i=0;i<=m-1;i++){
int temp=1e9+7;
for(int j=0;j<=n-1;j++) temp=min(temp,mat[i][j]);
sum+=temp;
}
int res=1e9+7;
for(int i=0;i<=target;i++){
if(dp[target+i]||dp[target-i]){
res=i;
break;
}
}
return res==1e9+7?sum-target:res;
}
};
今天写了还蛮多的


浙公网安备 33010602011771号