该篇文章从网络搜刮的+自己的理解;
自己在学。
本文主要参考资料——传送门
练习题:
零钱兑换322
第一讲 01背包问题
这是最基本的背包问题,每个物品最多只能放一次。
第二讲 完全背包问题
第二个基本的背包问题模型,每种物品可以放无限多次。
第三讲 多重背包问题每种物品有一个固定的次数上限。
第四讲 混合三种背包问题将前面三种简单的问题叠加成较复杂的问题。
第五讲 二维费用的背包问题一个简单的常见扩展。
第六讲 分组的背包问题一种题目类型,也是一个有用的模型。后两节的基础。
第七讲 有依赖的背包问题另一种给物品的选取加上限制的方法。
第八讲 泛化物品 第九讲 背包问题问法的变化试图触类旁通、举一反三。
附录一:USACO中的背包问题给出 USACO Training 上可供练习的背包问题列表,及简单的解答。 附录二:背包问题的搜索解法
除动态规划外另一种背包问题的解法。
文章目录
1. 01背包问题
1.1 问题描述
题目
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
1.2 基本思路
-
基本思路
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。 -
动规
用子问题定义状态:即f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。
这个地方可以参考 背包九讲——全篇详细理解与代码实现
该方法的时间复杂度为O(N*V)
具体的原理就不在这里介绍,这里主要用来整理归纳实现方法;
- 未优化代码
//假定 物品数量为N(Num简写 方便记忆),背包容量为V(Volumn简写)
//假定 第i个物品重量和价值分别为 W[i](Weight简写)、C[i](Cost简写)
//初始化
vector< vector<int> > dp(n,vector<int>(V,0)); //所有值初始化为0
//int dp[1000][1000] = 0;
for(int i=1;i<=N;i++)
{
for(int j=1;j<=V;j++)
{
//如果第i个物品 不选的话,前i个商品放入容积为j最大价值 与 前i-1个商品放入背包中的价值一样;
//此时背包应该没有约束 容量为j
f[i][j] = f[i-1][j];
if(j>=W[i])//如果当前容量 还够用 则可以选择第i个物品
{
//选的话,在前i-1个商品时,背包容量需要为[j-W[i]];
//此时的f[i][j]在上面已经赋值了f[i-1][j]
f[i][j] = max(f[i][j],f[i-1][j-W[i]]+C[i]);
}
}
}
更新顺序图例:
刷表的时候顺序:
前1个物品放入0的背包时最大价值
前1个物品放入1的背包时最大价值
前1个物品放入2的背包时最大价值
… … … . …
前1个物品放入V的背包时最大价值
|
前2个物品放入1的背包时最大价值
前2个物品放入2的背包时最大价值
前2个物品放入V的背包时最大价值
|
前3个物品放入1的背包时最大价值
前3个物品放入2的背包时最大价值
前3个物品放入V的背包时最大价值
|
前N个物品放入1的背包时最大价值
前N个物品放入2的背包时最大价值
前N个物品放入V的背包时最大价值
- 优化后的代码
//初始化
//全局变量定义在堆里面,堆里面的值会被初始化为0
int dp[1000][1000]={0};
for(int i=1;i<=N;i++)
{
for(int j=V;j>=0;j--)
{
f[j] = max(f[j],f[j-W[i]]+C[i]);
}
}
1.3 初始化问题
最优解背包问题中,存在两种问法
- 有的题目要求"恰好装满背包"时的最优解
- 有的题目则并没有要求必须把背包装满
这两种问法的区别是在初始化的时候有所不同。
- 如果是第一种问法,要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1…V]均设为−∞,这样就可以保证最终得到的f[N]f[N]是一种恰好装满背包的最优解。
- 如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0…V]全部设为0。
可以这样理解:初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0的nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是−∞了。
如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。
这个小技巧完全可以推广到其它类型的背包问题,后面也就不再对进行状态转移之前的初始化进行讲解。
1.4 模板代码
#include<iostream>
using namespace std;
const int N = 1010;
int v[N], w[N], dp[N];//dp[N][N]
int main(){
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> v[i] >> w[i];
}
for(int i = 1; i <= n; i++){
// for(int j = 0; j <= m; j++){
// if(j < v[i])
// dp[i][j] = dp[i-1][j];
// else
// dp[i][j]=max(dp[i-1][j], dp[i-1][j-v[i]]+w[i]);
// }
for(int j = m; j >=v[i]; j--){
dp[j] = max(dp[j], dp[j-v[i]]+w[i]);
}
}
cout<<dp[m]<<endl;;
return 0;
}
2 完全背包问题
2.1 题目描述
题目
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。每件物品数量不限,求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
2.2 基本思路
该方法就是将01背包的j、循环正序写就行;
#include<iostream>
using namespace std;
const int N = 1010;
int v[N], w[N], dp[N];//dp[N][N]
int main(){
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> v[i] >> w[i];
}
for(int i = 1; i <= n; i++){
// for(int j = 0; j <= m; j++){
// if(j < v[i])
// dp[i][j] = dp[i-1][j];
// else
// dp[i][j]=max(dp[i-1][j], dp[i-1][j-v[i]]+w[i]);
// }
for(int j = m; j >=v[i]; j--){
dp[j] = max(dp[j], dp[j-v[i]]+w[i]);
}
}
cout<<dp[m]<<endl;;
return 0;
}
类比01背包问题;状态转移方程:
- 简单小优化
但是上面需要循环三类;
for(int i=1;i<=N;i++)
for(int j=1;j<=M;j++)
for(int ii = 1;ii<=k;ii++)
//write code here
复杂度就非常高了;
将完全背包问题转换为01背包问题求解
如上图所示,但是并没有改变时间复杂度
二进制转化方法:
3 多重背包问题
3.1 问题描述
题目
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。第i件物品共有n[i]件可用,求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
3.2 基本思路
3.3 拆分代码
3.4 模板代码
普通代码:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int n,m;
cin>>n>>m;
vector<int> f(m+1,0);
int v,w,s;
for(int i=1;i<=n;i++)
{
cin>>v>>w>>s;
for(int j=m;j>=0;j--)
{
for(int ii = 0;ii<=s;ii++)
{
if(j>=ii*v)
f[j] = max(f[j],f[j-ii*v]+ii*w);
}
}
}
cout<<f[m]<<endl;
return 0;
}
二进制优化代码:
#include<iostream>
#include <vector>
using namespace std;
int main()
{
int n,m;
cin>>n>>m;
vector<int> datav,dataw;
int vv,ww,ss;
for(int i=0;i<n;i++)
{
cin>>vv>>ww>>ss;
for(int ii = 1;ii<ss;ii*=2)
{
datav.push_back(ii*vv);
dataw.push_back(ii*ww);
ss-=ii;
}
if(ss>0)
{
datav.push_back(ss*vv);
dataw.push_back(ss*ww);
}
}
vector<int> f(m+1,0);
for(int i=0;i<datav.size();i++) // the num of goods should be updated
{
for(int j=m;j>=datav[i];j--)
{
f[j] = max(f[j],f[j-datav[i]]+dataw[i]);
}
}
cout<<f[m]<<endl;
return 0;
}
4. 混合背包问题
分组就行;
4.1 模板代码
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int n,m;
cin>>n>>m;
vector<int> f(m+1,0);
vector<int> v,w,s;
int tempv,tempw,temps;
for(int i=0;i<n;i++)
{
cin>>tempv>>tempw>>temps;
if(temps == -1)
{
s.push_back(1);
v.push_back(tempv);
w.push_back(tempw);
}
else if(temps>0)
{
for(int ii = 1;ii<=temps;ii*=2)
{
s.push_back(ii);
v.push_back(tempv*ii);
w.push_back(tempw*ii);
temps-=ii;
}
if(temps>0)
{
s.push_back(temps);
v.push_back(tempv*temps);
w.push_back(tempw*temps);
}
}else
s.push_back(temps),v.push_back(tempv),
w.push_back(tempw);
}
for(int i=1;i<=s.size();i++)
{
if(s[i-1] ==0) //完全背包
{
for(int j=1;j<=m;j++)
{
if(j>=v[i-1])
f[j]=max(f[j],f[j-v[i-1]]+w[i-1]);
}
}else //01
{
for(int j=m;j>=0;j--)
{
if(j>=v[i-1])
f[j] = max(f[j],f[j-v[i-1]]+w[i-1]);
}
}
}
cout<<f[m]<<endl;
return 0;
}
5. 二维背包问题
多一层循环就行
5.1 模板代码
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int n,m,M;
cin>>n>>m>>M;
int v,w,c;
vector<vector<int> > f(m+1,vector<int>(M+1,0));
for(int i=1;i<=n;i++)
{
cin>>v>>w>>c;
for(int j=m;j>=v;j--)
{
for(int k = M;k>=w;k--)
{
f[j][k] = max(f[j][k],f[j-v][k-w]+c);
}
}
}
cout<<f[m][M]<<endl;
return 0;
}
10. 求方案总数的问题
dd大佬的讲解中最后也有介绍;下面是个截图
for i=1..N
for v=0..V
f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
g[i][v]=0
if(f[i][v]==f[i-1][v])
inc(g[i][v],g[i-1][v]
if(f[i][v]==f[i-1][v-c[i]]+w[i])
inc(g[i][v],g[i-1][v-c[i]])
伪代码:
F[0][] ← 0
F[][0] ← 0
G[][ ] ← 1
for i ← 1 to N
do for j ← 1 to V
F[i][j] ← F[i-1][j]
G[i][j] ← G[i-1][j]
if (j >= C[i])
if (F[i][j] < F[i][j-C[i]]+W[i])
then F[i][j] ← F[i][j-C[i]]+W[i]
G[i][j] ← G[i][j-C[i]]
else if (F[i][j] = F[i][j-C[i]]+W[i])
then G[i][j] ← G[i-1][j]+G[i][j-C[i]]
return F[N][V] and G[N][V]
F[0][0] ← 1
for i ← 1 to N
do for j ← 0 to V
if (j < C[i])
then F[i][j] ← F[i-1][j]
else
F[i][j] ← F[i-1][j]+F[i][j-C[i]]
return F[N][V]
模板代码
dp[0]=1;
for(int i=1;i<=N;i++)
{
for(int j=v[i];j<=V;j++)
dp[j]=dp[j]+dp[j-v[i]];
}
1000. 另外穿插一个找零钱问题
1000.1 题目简介
一般找零钱和背包问题很像;
输入:总钱数,和拥有的金币种类(1元,2元,5元,10元,20元等);
输出:需要的最少硬币数
扩展:
每种硬币只能取一类;
每种硬币可以无限取;
每种硬币有上限(s[i])
有的硬币可以无限取,有个硬币只有一个和多个;
1000.2 基本思路
乍一看和背包问题是一模一样。零钱的背包容量V 就是 金币总数
每种物品的体积v 就是他的面额;需要注意的就是:只能是恰好装满
所需要统计的就是硬币数量因此 f[i] = max(f[i],f[i-v]+1);
具体分析:
定义f[ i ] [ j ] 为第i个硬币恰好装满amount数量的总额
则分为 选 和 不选
选 : f[i][j] = f[i-1][j-v[i]]+1;
不选: f[i][j] = f[i-1][j];
所以讲道理来说;代码应该和背包问题差不多;
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
if(coins.size()==0||amount ==0)
return 0;
vector<int> f(amount+1,99999);
f[0] = 0;
for(int i=0;i<coins.size();i++)
{
for(int j=1;j<=amount;j++)
{
if(j>=coins[i])
f[j] = min(f[j],f[j-coins[i]]+1);
}
}
if(f[amount]==99999)
return -1;
return f[amount];
}
};
1000.3 华为多维找零钱问题0715
//华为笔试题0715
int main()
{
vector<int> coins = {1, 3, 7, 11, 13};
vector<int> nums = {1,2,3,4,5};
int m = 30;
//多维背包问题 --- 二进制优化
vector<int> v, w;
for (int i = 0; i <= coins.size(); i++)
{
for (int k = 1; k <= nums[i];k*=2)
{
v.push_back(k * coins[i]);
w.push_back(k); //个数也要统计
nums[i] -= k;
}
if(nums[i]>0)
{
v.push_back(nums[i] * coins[i]);
w.push_back(nums[i]);
}
}
vector<int> f(m + 1, 9999);
f[0] = 0;
for (int i = 0; i < v.size();i++)
{
for (int j = m; j >= v[i];j--)
{
f[j] = min(f[j], f[j - v[i]] + w[i]);
}
}
if(f[m] == 9999)
cout << -1 << endl;
cout << f[m] << endl;
return 0;
}
本文参考资料:
1.单调队列优化多重背包问题
2.各种背包问题
3.背包九江
4.背包玖讲
5.背包九江
6.单队列优化多重背包
7. 背包9讲
8. 9讲
9. dd大佬九江
10.哔哩哔哩教学视频