前景引入
有 N 件物品和一个容量为 V 的背包
放入第 i 件物品耗费的费用是 1 ,得到的价值是 Wi 。每个物品最多可放入一次,求最大的价值总和
- 无需背包思维,只要排序取前V个即可
01背包(每种物品仅有一件,可以选择放或不放)
有 N 件物品和一个容量为 V 的背包
放入第 i 件物品耗费的费用是 Ci ,得到的价值是 Wi 。一件物品最多放入一次,求最大的价值总和
不可取:性价比思维
n=3 V=3
W1=2 C1=1
W2=3 C2=1
W3=3 C3=2
W1+W2=5 < W2+W3=6
- 这种情况下,性价比3>2>1.5
- 当放入前两者后,虽然背包未放满,但也放不下第三者,只能舍弃
- 如此得到的价值之和,倒不如放第二者和第三者更大些
- 所以性价比思维的局限在于价值大+耗费多+背包未满仍放不下的情况
正解:动态规划
- dp[i][j] 表示前 i 个物品放入容量为 j 的最大价值(放入i后的容量为j)
- 不放第i个物品:dp[i-1][j]
- 放入第i个物品:dp[i-1][j-cost[i]]+value[i](处理好第i-1个物品后的最大价值+第i个的价值)
- 状态转移方程:dp[i][j] = max(dp[i-1][j],dp[i-1][j-cost[i]]+value[i])
- 时空复杂度都是O(nV)
空间优化
- dp[j] 表示放入容量为j的最大价值
- 不放第i个物品:dp[j]
- 放入第i个物品:dp[j-cost[i]]+value[i]
- 状态转移方程:dp[j] = max(dp[j] , dp[j-cost[i]] + value[i])
- 时间复杂度O(nV),空间复杂度O(V)
小贴士
- 空间优化后,遍历到i,就表示处理第i个物品,从而优化二维数组变为一维数组
- 内层循环逆序,以防大体积用到的小体积dp优化过并选择放入的情况,从而一个物品被多次放入
- 例如:第1和2个物品各体积均已放好,处理第3个物品时,若从小体积开始
- 假设第3个物品体积为5,容器体积为5时已能放下第3个物品
- 遍历到容器体积为10时,调用了容器体积为5的结果,第3个物品再次被放入
- 但01背包中每个物品只有一个,最多只能被放入一次
代码模板
int knapsack_01()//01背包
{
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;++i)
{//遍历到i,就表示遍历到第i个物品,优化二维数组变为一位数组
for(int j=v;j>=cost[i];--j)//从大到小遍历!!!
/*
以防同一个物品从小到大的话
先把小体积优化好,再优化大体积
那么大体积用的小体积dp可能是放这个物品优化过的
但01背包,每个物品只有一个,不可重复放
*/
{
if(cost[i]<=j) dp[j]=max(dp[j],dp[j-cost[i]]+value[i]);
}
}
return dp[v];
/*
二维数组code写法
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;++i)
{
for(int j=0;j<=v;++j)
{
if(j<c[i]) dp[i][j]=dp[i-1][j];
else dp[i][j]=max(dp[i-1][j],dp[i-1][j-cost[i]]+value[i]);
}
}
return dp[n][v];
*/
}
多重背包(每件物品可放置指定的多次,非无限次)
基础思维:转化为简单01背包
- 要求:一件价值Wi,大小Ci的可放置Mi次的物品
- 转化为:Mi件价值Wi,大小Ci的只可取一次的物品
- 时间复杂度:O(VΣMi)
高级思维:转化为01背包+二进制优化
- 把Mi个拆成 20-21-22-23…2(k-1)-(Mi-2k-1)个物品
- Mi=29按照1 2 4 8 14拆分
- 时间复杂度:O(VΣlogMi)
- 比如5张5块钱的纸币,可以拆分为
- 20个5元(1个5元)+21个5元(1个10元)+(5-22)个5元(1个15元)
- 如果是求最少用几张纸币凑出指定金额,即应当记录20 21 (5-22)(实际耗费的纸币量)
代码模板
int knapsack_multiple()//多重背包
{
int n1=0,m[maxn];//实际m数组应当在main函数内输入完毕
for(int i=1;i<=n;++i)
{
for(int j=1;m[i];j*=2)//二进制优化拆数1 2 4 8 16 32等等
{
j=min(j,m[i]);
/*
比如29拆成1 2 4 8 14,此步骤处理类似剩余14,不足16的情况
即当前的j与剩余的总数量取最小值
全部取完的话m[i]==0为假,循环就退出了
m[i]是输入的i物品有几个
*/
m[i]-=j;
n1++;
cost1[n1]=cost1[i]*j;
value1[n1]=value1[i]*j;
}
}
for(int i=1;i<=n1;++i)
{
for(int j=v;j>=cost1[i];--j)
{
dp[j]=max(dp[j],dp[j-cost1[i]]+value1[i]);
}
}
return dp[v];
}
完全背包(每件物品可放置无限次)
有 N 件物品和一个容量为 V 的背包。
放入第 i 件物品耗费的费用是 Ci ,得到的价值是 Wi
基础思维:转化为多重背包
- 转化为: Mi=V/Ci 的多重背包
- 时间复杂度:O(VΣV/Ci)
巧妙思维:类似于01背包
- 还记得01背包的时候我们要求内层循环逆序嘛
- 那时的目的就是防止一件物品被放入多次
- 但是完全背包就是需要可放置无限次,这个防止的操作就可以省去啦
- 于是我们的完全背包就是将01背包的内层循环改为正序
代码模板
int knapsack_full()//完全背包
{
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;++i)
{
for(int j=cost[i];j<=v;++j)
{
dp[j]=max(dp[j],dp[j-cost[i]]+value[i]);
}
}
return dp[v];
}
分组背包(每组中的物品互相冲突,最多选一件)
有 N 件物品和一个容量为 V 的背包
第 i 件物品的费用是 Ci,价值是 Wi
这些物品被划分为 K 组,每组中的物品互相冲突,最多选一件
求最大的价值总和
关键思维:把每一组看成一个大的01物品(每组只能挑一个组员)
- 三层循环嵌套
- 第一层:组数(正序)
- 第二层:容量(逆序-类似于01背包)
- 第三层:组内成员(正序)
代码模板
int knapsack_divide()//分组背包
{
memset(dp,0,sizeof(dp));
//如n门课有m天,花k天数学第i门课的价值是Value[i][k],要求价值最大
for(int i=1;i<=n;i++)//第i组,即每门课1组,组内成员是天数
{
for(int j=v;j>=1;j--)//背包容量j,即容量是天数
{
for(int k=1;k<=j;k++)//这组中第k个,选几天
{
dp[j]=max(dp[j],dp[j-k]+Value[i][k]);
}
}
}
return dp[v];
}
混合背包
- 有的物品只能放一次,有的物品能放Mi次,有的物品能放无限次(调用多个函数即可)
补充
memset(dp,0,sizeof(dp));//背包可以不装满
memset(dp,-inf,sizeof(dp));//背包必须被装满(memset后勿忘dp[0]=0;)
- 当背包可以不装满的时候,如果dp[j-cost[i]]没装满还是可以装的
- 但当背包必须被装满的时候,如果dp[j-cost[i]]若是没装满则不可能比装满的大
- 就比如体积为5的袋子,装体积为2的东西,之前没装过体积为3的东西
- 这个时候dp[5]只是-inf+value[i]
- 但如果与装过体积为3的东西的dp[5]比较,那么装过的值一定更大,自然取它
- 勿忘memset后还要初始化dp[0]=0,不然无论如何都没装满哈哈哈