目录
赠dp
在生活的舞台上,
再次遭遇这难题。
心头荆棘簇拥,
似乎总是重重堆。
犹如雨夜孤舟,
摇摆在黑暗河。
迷雾笼罩眼帘,
找寻方向无处依。
动态规划如梦,
每一步都是呻吟。
穷途末路的舞者,
舞动着希望的阴影。
初始化,寒风撕裂,
唯有寂静和绝望。
硬币之境无法言喻,
只剩无边的孤寂。
遍历硬币,步履蹒跚,
如同穿越生活的坎坷。
在迷茫的路上彷徨,
寻找一丝前行的勇气。
更新dp数组,泪滴湿润,
似流过岁月的沧桑。
悄然铺展出希望的羽翼,
却又被内卷的焦虑重重拖延。
最后答案在远方,
如同无数次追逐的梦幻。
在绝望与希望的边缘,
挣扎的灵魂重复徘徊。
在这算法的海洋里,
舞蹈的是心灵的烦忧。
每一步都是岁月的涟漪,
却又是人生的挣扎和期许。
完全背包与01背包的区别在于,01背包物品至多能选一次,完全背包的物品,如果背包能放下,可以选任意个数。
No.1 完全背包【模板】



第一问:

状态转移方程无非就是考虑i这个位置选择的次数。
当一个位置和前面一大堆位置有关的时候,可以考虑优化。注意到i-1位置不变,j的这个位置是均匀变化的,那么优化就很简单了。

此外注意判断j-v[i]是否越界

第二问:
状态表示为从前i个物品中选,恰好装满j个体积的最大价值。
状态转移方程第一问一样,但是为了区别是否能达到这个价值,不能达到的初始化为-1
初始化:

第二问输出的时候再判断一下是不是-1,这题就大功告成。
#include <iostream>
using namespace std;
#include<string.h>
const int N=1010;
int n,V,v[N],w[N],dp[N][N];
int main() {
cin>>n>>V;
for(int i=1;i<=n;++i)cin>>v[i]>>w[i];
//第一问
for(int i=1;i<=n;++i)
{
for(int j=1;j<=V;++j)
{
dp[i][j]=dp[i-1][j];
if(j>=v[i])dp[i][j]=max(dp[i][j],dp[i][j-v[i]]+w[i]);
}
}
cout<<dp[n][V]<<endl;
memset(dp,0,sizeof dp);
for(int j=1;j<=V;++j)dp[0][j]=-1;
//第二问
for(int i=1;i<=n;++i)
{
for(int j=1;j<=V;++j)
{
dp[i][j]=dp[i-1][j];
//中括号太多,小心括号匹配错误
if(j>=v[i]&&dp[i][j-v[i]]!=-1)dp[i][j]=max(dp[i][j],dp[i][j-v[i]]+w[i]);
}
}
cout<<(dp[n][V]==-1?0:dp[n][V])<<endl;
return 0;
}
空间优化:
填表顺序从左往右,这里和01背包不同。(01要用到更新之前的数据,只能采用从右往左顺序填表)
#include <iostream>
using namespace std;
#include<string.h>
const int N=1010;
int n,V,v[N],w[N],dp[N];
int main() {
cin>>n>>V;
for(int i=1;i<=n;++i)cin>>v[i]>>w[i];
//第一问
for(int i=1;i<=n;++i)
{
for(int j=v[i];j<=V;++j)
{
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
}
}
cout<<dp[V]<<endl;
memset(dp,0,sizeof dp);
for(int j=1;j<=V;++j)dp[j]=-1;
//第二问
for(int i=1;i<=n;++i)
{
for(int j=v[i];j<=V;++j)
{
if(dp[j-v[i]]!=-1)dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
}
}
cout<<(dp[V]==-1?0:dp[V])<<endl;
return 0;
}
第二问中或者当你初始化的时候,将这个数值设置为非常小的数(-0x3f3f3f3f,一般不用INT_MIN,INT_MAX,因为算术运算的时候很容易越界),可以省去一步判断的代码,然后输出的时候判断改成是不是小于0。
#include <iostream>
using namespace std;
#include<string.h>
const int N=1010;
int n,V,v[N],w[N],dp[N];
int main() {
cin>>n>>V;
for(int i=1;i<=n;++i)cin>>v[i]>>w[i];
//第一问
for(int i=1;i<=n;++i)
{
for(int j=v[i];j<=V;++j)
{
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
}
}
cout<<dp[V]<<endl;
memset(dp,0,sizeof dp);
for(int j=1;j<=V;++j)dp[j]=-0x3f3f3f3f;
//第二问
for(int i=1;i<=n;++i)
{
for(int j=v[i];j<=V;++j)
{
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
}
}
cout<<(dp[V]<0?0:dp[V])<<endl;
return 0;
}
No.2 零钱兑换

就是上一题第二问,

class Solution {
const int INF=0x3f3f3f3f;
public:
int coinChange(vector<int>& coins, int amount) {
int n=coins.size();
vector<vector<int>> dp(n+1,vector<int>(amount+1));
//dp[i][j]表示从前i个数里挑数字,恰好凑成j的最小硬币数
//凑不成的不能用-1表示,因为需要求最小值
for(int j=1;j<=amount;++j)dp[0][j]=INF;
for(int i=1;i<=n;++i)
{
for(int j=1;j<=amount;++j)
{
dp[i][j]=dp[i-1][j];
//注意映射关系
if(j>=coins[i-1])dp[i][j]=min(dp[i][j],dp[i][j-coins[i-1]]+1);
}
}
return dp[n][amount]==0x3f3f3f3f?-1:dp[n][amount];
}
};
空间优化:
class Solution {
const int INF=0x3f3f3f3f;
public:
int coinChange(vector<int>& coins, int amount) {
int n=coins.size();
vector<int> dp(amount+1);
//dp[i][j]表示从前i个数里挑数字,恰好凑成j的最小硬币数
//凑不成的不能用-1表示,因为需要求最小值
for(int j=1;j<=amount;++j)dp[j]=INF;
for(int i=1;i<=n;++i)
{
for(int j=coins[i-1];j<=amount;++j)
{
//注意映射关系
dp[j]=min(dp[j],dp[j-coins[i-1]]+1);
}
}
return dp[amount]==0x3f3f3f3f?-1:dp[amount];
}
};
No.3 零钱兑换II


dp[i][j]表示从前i个数中选,恰好凑成j的组合总数
范围得设置成unigned long long(评论区一堆人才哈哈(~ ̄▽ ̄)~)

class Solution {
public:
int change(int amount, vector<int>& coins) {
int n=coins.size();
vector<vector<unsigned long long>> dp(n+1,vector<unsigned long long>(amount+1));
dp[0][0]=1;
for(int i=1;i<=n;++i)
{
for(int j=0;j<=amount;++j)
{
dp[i][j]+=dp[i-1][j];
//注意映射关系!!!
if(j>=coins[i-1])dp[i][j]+=dp[i][j-coins[i-1]];
}
}
return dp[n][amount];
}
};
空间优化:
class Solution {
public:
int change(int amount, vector<int>& coins) {
int n=coins.size();
vector<unsigned long long> dp(amount+1);
dp[0]=1;
for(int i=1;i<=n;++i)
{
for(int j=coins[i-1];j<=amount;++j)
{
//注意映射关系!!!
dp[j]+=dp[j-coins[i-1]];
}
}
return dp[amount];
}
};
No.4 完全平方数

无需多言,聪明的你看到这一定想得出来
class Solution {
public:
int numSquares(int n) {
int m=sqrt(n);
const int INF=0x3f3f3f3f;
//dp[i][j]表示前i个数里,平方数和恰好为j的最少平方数数量
vector<vector<int>> dp(m+1,vector<int>(n+1,INF));
for(int i=0;i<=m;++i)dp[i][0]=0;
for(int i=1;i<=m;++i)
{
for(int j=1;j<=n;++j)
{
dp[i][j]=dp[i-1][j];
//完全背包二次考虑[i]不是[i-1]
if(j>=i*i)dp[i][j]=min(dp[i][j],dp[i][j-i*i]+1);
}
}
return dp[m][n];
}
};
空间优化:
class Solution {
public:
int numSquares(int n) {
int m=sqrt(n);
const int INF=0x3f3f3f3f;
//dp[i][j]表示前i个数里,平方数和恰好为j的最少平方数数量
vector<int> dp(n+1,INF);
dp[0]=0;
for(int i=1;i<=m;++i)
{
for(int j=i*i;j<=n;++j)
{
//完全背包二次考虑[i]不是[i-1]
dp[j]=min(dp[j],dp[j-i*i]+1);
}
}
return dp[n];
}
};
No.5 数位成本和为目标值的最大数字
1449. 数位成本和为目标值的最大数字 - 力扣(LeetCode)




class Solution {
public:
string largestNumber(vector<int>& cost, int target) {
//dp[i][j]表示前i个数中恰好凑成j的最大长度
int n=cost.size();
const int INF=0x3f3f3f3f;
//存在凑不成的情况,用-1表示
vector<int> dp(target+1,-INF);
dp[0]=1;
for(int i=1;i<=n;++i)
{
for(int j=cost[i-1];j<=target;++j)
{
//注意映射
dp[j]=max(dp[j],dp[j-cost[i-1]]+1);
}
}
//特殊判断
if(dp[target]<0)return "0";
//输出最大的情况,为了保证最大,贪心倒着遍历,查看有没有这个状态即可
string ret;
for(int i=9;i>=1;--i)
{
int tmp=cost[i-1];
//表示可以添加这个数,并且这个情况是存在的
while(target>=tmp&&dp[target]==dp[target-tmp]+1)
{
ret+=to_string(i);
target-=tmp;
}
}
return ret;
}
};
No.6 获得分数的方法数
多重背包就是选择有限次数,但不是1,其实就是介于01背包和完全背包之间的一种情况。



class Solution {
public:
int waysToReachTarget(int target, vector<vector<int>>& types) {
const int MOD = 1e9 + 7;
int n = types.size();
// dp[i][j]表示前i个种类题里边选,恰好凑成j的方法数
vector<vector<int>> dp(n + 1, vector<int>(target + 1));
dp[0][0] = 1;
for (int i = 1; i <= n; ++i) {
//注意映射
int count = types[i - 1][0], mark = types[i - 1][1];
//从0开始,或者初始化时将第一列全部初始化为1
for (int j = 0; j <= target; ++j) {
dp[i][j] += dp[i - 1][j];
for (int k = 1; k <= count; ++k) {
if (j >= k * mark)
//注意不是i,是i-1
dp[i][j] = (dp[i][j]+dp[i-1][j - k * mark])%MOD;
}
}
}
return dp[n][target];
}
};
空间优化:
class Solution {
public:
int waysToReachTarget(int target, vector<vector<int>>& types) {
const int MOD = 1e9 + 7;
int n = types.size();
// dp[i][j]表示前i个种类题里边选,恰好凑成j的方法数
vector<int> dp(target + 1);
dp[0] = 1;
for (int i = 1; i <= n; ++i) {
//注意映射
int count = types[i - 1][0], mark = types[i - 1][1];
//从0开始,或者初始化时将第一列全部初始化为1
//和01背包那样,因为要访问左上方的那个数据所以横向要倒着遍历
for (int j = target; j >=0; --j) {
count=min(count,j/mark);
for (int k = 1; k <= count; ++k) {
if (j >= k * mark)
//注意不是i,是i-1
dp[j] = (dp[j]+dp[j - k * mark])%MOD;
}
}
}
return dp[target];
}
};
No.7 和带限制的子多重集合的数目
2902. 和带限制的子多重集合的数目 - 力扣(LeetCode)



首先我们肯定不能像之前一样从开始到末尾遍历数组更新dp了,因为这里要求不能重复,按原先的方法必然导致重复。为了避免,可以用哈希表存数据,遍历哈希表,对每一个元素单独进行讨论即可,如此就可以避免出现相同结果。
此外可以发现这是一个多重背包,对于在区间内l,r最后返回dp[l~r]即可,很好解读。
直接写空间优化代码:
j倒着遍历也是因为需要访问 i-1 j-XXX 位置的数据,只能倒着遍历,按顺序遍历会覆盖我后续需要的值
class Solution {
public:
int countSubMultisets(vector<int>& nums, int l, int r) {
//dp[i]表示恰好达到i的子集合总数
const int MOD=1e9+7;
int n=nums.size();
unordered_map<int,int> hash;
int TotalSum=0;
for(auto&x:nums)
{
TotalSum+=x;
++hash[x];
}
r=min(TotalSum,r);//更新一下能抵达的最右边界
vector<int> dp(r+1);
dp[0]=hash[0]+1;
hash.erase(0);
for(auto&[x,count]:hash)
{
for(int j=r;j>=x;--j)
{
count=min(count,j/x);
for(int k=1;k<=count;++k)
{
dp[j]=(dp[j]+dp[j-k*x])%MOD;
}
}
}
int ret=0;
for(int j=l;j<=r;++j)ret+=dp[j];
return ret;
}
};
运行通过,代码没有问题,但是一运行就会超时报错,意味着还有优化空间


class Solution {
public:
int countSubMultisets(vector<int>& nums, int l, int r) {
//dp[i]表示恰好达到i的子集合总数
const int MOD=1e9+7;
int n=nums.size();
unordered_map<int,int> hash;
int TotalSum=0;
for(auto&x:nums)
{
TotalSum+=x;
++hash[x];
}
r=min(TotalSum,r);//更新一下能抵达的最右边界
vector<int> dp(r+1);
dp[0]=hash[0]+1;
hash.erase(0);
int sum;
for(auto&[x,count]:hash)
{
//当前能达到的最大和
sum=min(sum+x*count,r);
//先统一计算,而后删除重复项
for(int j=x;j<=sum;++j)dp[j]=(dp[j]+dp[j-x])%MOD;
for(int j=sum;j>=x*(count+1);--j)
//保证结果为符合题意的正数
dp[j]=(dp[j]-dp[j-x*(count+1)]+MOD)%MOD;
}
int ret=0;
for(int j=l;j<=r;++j)ret=(ret+dp[j])%MOD;
return ret;
}
};
此篇完。
1259

被折叠的 条评论
为什么被折叠?



