题记:在日本人的书上为动态规划问题下了一个简短而明确的定义——记录结果再用
内容大多来自背包九讲(如有侵权,立即删除)
所涉及背包内容:
1.01背包
2.完全背包
3.多重背包(朴素版本的首先讲解,主要是多重背包的二进制优化版本,优先队列优化版本等作者学会了再进行补充)
4.混合背包
5.二维费用的背包问题
6.分组背包
7.背包问题的方案数
8.输出背包的具体方案
9.有依赖的背包问题
一、01背包问题
作为背包问题的基础,必须要花费一些精力去弄懂01背包问题,否则剩下的8种背包都是基于01背包问题,将会很难理解。
首先我们来了解什么是01背包问题
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例:
4 5
1 2
2 4
3 4
4 5
输出样例:
8
动态规划的精髓在于缩小问题规模,并记录下小规模问题的答案,用此来求解大规模问题——即最优子问题结构。
在01背包中,所需要记录的状态并不难想,因为大规模的问题就是n(物品的个数),m(背包的体积)非常大的时候,所以以此我们便可以确定状态为物品个数和背包体积。
那么,我们该怎么记录小规模问题的结果呢?
没错,用数组,dp[i][j]数组的值代表在只能取前(i-1)个物品且背包体积为j的情况下,所能得到的最大价值。
如何记录结果已经明白了,那么该如何解决结果的正确性呢?
到这我们就不得不提一个东西——状态转移方程。
像01背包这样的问题,无非两种选择,拿或者不拿当前第i个物品,那么状态转移方程就非常好写了:
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i])(当前背包容量足够放第i个物品)
(当然了,如果不够放那么很显然就应该是dp[i][j]=dp[i-1][j],因为你没有取的可能性)
dp[i-1][j]代表不取当前第i个物品,直接用先前已经计算好的前i-1个物品且背包容量同为j时的最优解来代表当前的最优解
dp[i-1][j-v[i]]+w[i]代表取当前第i个物品,并且用先前已经计算好的前i-1个物品且背包容量为j-v[i]时的最优解来代表当前的最优解。
将两种状态取最大值,便可以代表当前取前i个物品且背包容量为j时的最优解了
对于样例,有这样一张表格:
而这张表格如何得到的呢,我先做一个示范,如dp[4][5]这个位置,也就是我们最终答案的位置,他应该是由 max(dp[3][5],dp[3][5-4]+5)所转移过来的,很显然前者为8,而后者为7,所以最终答案为8,具体每个格子该如何转移,我认为应该自己动手去理解一遍这个过程。
放上一个大佬的博客,里面详细的写了每一步如何转移:0—1背包
而代码也十分简洁明了:
#include<iostream>
#include<iomanip>
using namespace std;
const int MAXN=1010;
int dp[MAXN][MAXN],n,m,v[MAXN],w[MAXN];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v[i]>>w[i];
}
for(int i=1;i<=n;i++){//当前有i个物品可供选择
for(int j=1;j<=m;j++){//当前的背包容量为j
if(j>=v[i]) dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);//如果足够放,使用状态转移方程求解
else dp[i][j]=dp[i-1][j];//不够放就直接将上一行的值拿下来
}
}
cout<<dp[n][m]<<endl;//输出有n个物品且背包容量为m时的最优结果,即为答案
return 0;
}
这就是最基础的01背包做法了,时间复杂度为O(n*m)。
想必看到这,聪明的你们就发现了:好像每次状态转移只用到了当前行和上一行的值诶,那我可不可以只开一个只有两行的数组来求解呢?
答案是可以的,并且这样做可以大大节省空间。
代码如下:
#include<iostream>
#include<iomanip>
using namespace std;
const int MAXN=1010;
int dp[2][MAXN],n,m,v[MAXN],w[MAXN];
int main(){
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=1;j<=m;j++){
if(j>=v[i]) dp[1][j]=max(dp[0][j],dp[0][j-v[i]]+w[i]);
else dp[1][j]=dp[0][j];
}
for(int j=1;j<=m;j++) dp[0][j]=dp[1][j];//当前行在下次运算中就变成了上一行,所以将当前行赋值给上一行
}
cout<<dp[1][m]<<endl;
return 0;
}
好了现在问题又来了!既然可以把他放在两行,那么我可不可以再减去一行!只用一个一位数组,就顺利求出问题的解呢?
答案又是可以的,很神奇吧?
只不过我们需要更改内重循环j的枚举顺序,把 for(int j=1;j<=m;j++) 变成 for(int j=m;j>=v[i];j- - )
至于为何,请听我解释:在内重循环枚举背包容量结束后,你所需要的只有前i个物品的最优解已经出来了,并且存在dp数组中(注意当前的dp数组是一位数组),下一次外重循环便会加1,表示当前有i+1个物品供你选择,你需要得出最优解,显而易见,数组元素在没有被赋值前,会保持原来的那个值不变,而原来的那个值是什么呢?就是在只有前i个物品且质量为j的最优解,而你在用完这个值后,他就不会再出现在以后的计算中了,我们便可以用dp[j]=max(dp[[j],dp[j-v[i]]+w[i])的这个值来替换掉他。
代码如下:
#include<iostream>
#include<iomanip>
using namespace std;
const int MAXN=1010;
int dp[MAXN],n,m,v[MAXN],w[MAXN];
int main(){
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=m;j>=v[i];j--){
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
}
}
cout<<dp[m]<<endl;
return 0;
}
这便是01背包问题的滚动数组优化。
到目前为止,01背包问题就讲解完毕了。
二、完全背包
直接上问题描述:
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
10