第五章 动态规划:背包问题
背包问题:(ps:我这里的小写v一般指的是value,w一般指weight)
【0-1背包,】
N个物品,v[i],w[i],每个物品最多用几次,容量为V的背包,价值max【容量限制,价值max】
0-1背包【每件物品最多用1次】
f(i,j) 表示总重量<=j,只从前i个物品选的价值max
f(i,j) = max{含i,不含i} = max( f(i-1,j-w[i]) + v[i] , f(i-1,j) )
代码:
错因:注意!!!明确i和j从多少开始遍历,对于f[i][j]要表示前i个物品,体积小于j的max价值,读取物品要从i=1开始,而不是0-->vector<int>(N+1)
如果需要初始化0,可以vector<vector<int>> f(N+1,vector<int>(V+1,0));
#include<iostream>
#include<algorithm>
using namespace std;
// 件物品的体积和价值。
int main(){
int N,V;
cin>>N;
cin>>V;
vector<int> w(N+1);//体积
vector<int> v(N+1);//价值
for(int i = 1;i<N+1;i++){
cin>>w[i];
cin>>v[i];
}
//f[i][j] = max(f[i-1][j],f[i-1][j-w[i]]+v[i]]前i个物品,不超过j
vector<vector<int>> f(N+1,vector<int>(V+1,0));//愣住了,f(n)初始化是什么
//f(几行,vector<int>(几列))
for(int i = 1;i<=N;i++){
for(int j =0;j<=V;j++){
// if(i== 0 || j==0){
// f[i][j] = 0;
// continue;//忘记continue了
// }
f[i][j] = f[i-1][j];
if(j>=w[i]){
f[i][j] = max(f[i][j],f[i-1][j-w[i]]+v[i]);
}
}
}
cout<<f[N][V];
}
进阶:一维dp
因为f[i]仅仅与f[i-1] 有关,所以可以变成一维数组f[j] 重量<=j的max价值,
就是相当于也是遍历i,遍历j,但是不需要每一个i对应的f都存下来,因为遍历的时候前一个还没有被后一个覆盖
其中,如果直接:f[j] = max(f[j],f[j-w[i]]+v[i]);其实比较的是f[i][j]和f[i][j-w[i]],和预期不符
因此j倒过去遍历,这个好妙!!!!!
犯了什么错:初始化f(N+1),应该是V+1!!【要对每一个变量负责】
#include<iostream>
#include<algorithm>
using namespace std;
int main(){
int N;//物品
int V;//背包重量
cin>>N>>V;
vector<int>w(N);//体积
vector<int>v(N);//价值
for(int i = 0;i<N;i++){
cin>>w[i];
cin>>v[i];
}
vector<int> f(V+1);
// 初始化第 0 行
for (int j = 0; j <= V; j++) {
if (j >= w[0]) {
f[j] = v[0];
}
}
for(int i = 1;i<N;i++){//物品
for(int j =V;j>=w[i];j--){
//f[i][j] = f[i-1]
f[j] = max(f[j],f[j-w[i]]+v[i]);
//这里如果直接:f[j] = max(f[j],f[j-w[i]]+v[i]);
//其实比较的是f[i][j]和f[i][j-w[i]],和预期不符
}
}
cout<<f[V];
}
完全背包【每件物品无限个】
f[i][j] 同样表示从前i个物品取,总重量<=j
递推公式【就像集合划分一样,不重不漏】:f[i][j] = max( f[i-1][j] , f[i-1][j - k*w[i]]+k*v[i])【k>=0 && k<=V/w[i]】
即max(红框) = f[i,j-v] +w
递推公式升级版:f[i,j] = max(f[i-1,j] , f[i,j-w[i]]+v[i])
对比两个背包:
优化成一维:
f[j] = max( f[j] , f[j-v]+w](这次不需要倒着遍历j
小插曲:这个代码写得,都无语,对i=0的处理,要不然是没加continue,要不然是if的对 i==0的条件限制不全,一部分还是会有f[-1](见注释)
还是初始化vector<vector<int>>f(N+1,vector<int>(V+1,0))好一点
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main() {
int N; // 物品数量
int V; // 背包容量
cin >> N >> V;
vector<int> w(N); // 物品体积
vector<int> v(N); // 物品价值
for (int i = 0; i < N; i++) {
cin >> w[i]>> v[i];
}
vector<vector<int>> f(N, vector<int>(V + 1, 0));
// 动态规划
//多重f[i,j] = max( f[i-1,j],f[i,j-w]+v)
for (int i = 0; i < N; i++) {
for (int j = 0; j <= V; j++) {
// if(i == 0 && j>=w[0]){
// int num = j/w[0];
// f[0][j] = num*v[0];
// continue;
// }else{
// f[0][j] = 0;
// continue;
// }
if(i == 0){
if(j>=w[0]){
int num = j/w[0];
f[0][j] = num*v[0];
}
continue;
}
f[i][j] = f[i-1][j];//为啥在里面初始化,因为j<w[i]也要有值
if(j>=w[i])
f[i][j] = max(f[i][j], f[i][j -w[i]]+v[i]); // 选择 k 个当前物品
}
}
cout << f[N - 1][V] << endl;
return 0;
}
优化版:就挺无语的,遍历改成了i= 1开始,但是前面读取w和v还是从0 开始,但是输出f[v],输出的是131985?,最后一个是8emmmmm
#include<iostream>
#include<algorithm>
using namespace std;
int main(){
int N,V;
cin>>N>>V;//空格?
vector<int>w(N+1);
vector<int> v(N+1);
for(int i = 1;i<=N;i++){
cin>>w[i]>>v[i];//每种物品都有无限件可用。
}
//f[i][j] = max(f[i-1][j],f[i-1][j-k*w[i]]+k*v[i]) (k:0-j/w[i])
//f[i][j-w[i]] =max(f[i-1][j-k*w[i]]+k*v[i]) (k:0-j/w[i])
//f[i][j] = max(f[i-1][j],f[i][j-w[i]])
vector<int>f(V+1);
for(int i = 1;i<=N;i++){//前i个物品
for(int j = 0;j<=V;j++){
if(j>=w[i]){
f[j] = max(f[j],f[j-w[i]]+v[i]);
}
}
}
cout<<f[V];
}
多重背包【每个物品个数不同个】
每个物品最多拿s[i]
f[i,j] = max(f[i-1,j-k*w[i]]+k*v[i])(0<=k<=s[i])【类似完全背包】【O(NSV)】
参考完全背包:原来:O(nks)
f[i,j] = max( f[i-1,j] , f[i-1,j-w[i]] +v[i] , f[i-1,j-2*w[i]]+2*v[i] , ...f[i-1,j-s[i]*w[i]]+s[i]*v[i] )
f[i,j-w[i]] = max (f[i-1,j-w[i]] , f[i-1,j-2*w[i]]+1*v[i] , ...,f[i-1,j-s[i]*w[i]]+(s[i]-1)*v[i],f[i-1,j-(s[i]+1)*w[i]]+(s[i])*v[i]
多了一项,不能直接用f[i,j] = max{ f[i,j-w[i]]+v[i] , f[i-1,j] }
优化:
1,2,4,8,16,32,...
可以表示0-2^n-1的所有数
即可以把s[i]拆分为logs[i]个组,进而转换为0-1背包问题
O(NVS)-->O(NlogS*V)【小小疑惑:原来是s,现在是logs个数去组合,那不是2^logs??】
最不缺的就是离谱的错误啊啊啊啊啊
1.count和cnt弄反,count计划是是s[i]-->0-1背包后的总物品数量,应该提前输出化,以及最好在代码块前自增,因为如果是代码块后,while循环可能结束,后面没东东了。比如最后一次while循环,这样count实际多了一个
2.判断s应该是>0而不是>=0
想法:1.while/for中的自增操作尽量放在前面【如果统计的是个数】
2.及时记录变量的一般形式,比如cnt:1,2,4,8
// 第一行两个整数,N,V
// ,用空格隔开,分别表示物品种数和背包容积。
// 接下来有 N
// 行,每行三个整数 vi,wi,si
// ,用空格隔开,分别表示第 i
// 种物品的体积、价值和数量。
#include<iostream>
#include<algorithm>
using namespace std;
int main(){
int N,V;
cin>>N>>V;
// vector<int>w(N+1);//可以加吗
// vector<int>v(N+1);
// vector<int>s(N+1);
int w[22000],v[22000];
int a,b,s;//计算logs
int count = 0;//count有问题
for(int i = 1;i<=N;i++){
cin>>a>>b>>s;
//count有问题
int cnt = 1;
while(cnt<=s){
count++;
w[count] = a*cnt;//注意*count
v[count] = b*cnt;
s-=cnt;//cnt vs count
cnt = cnt*2;
}
if(s>0){//先判断s>=0,是s>0
count++;
w[count] = a*s;//余下的怎么办
v[count] = b*s;
}
}
// cout<<count<<endl;
vector<int>f(V+1,0);
for(int i = 1;i<=count;i++){
for(int j = V;j>0;j--){//
if(j>=w[i]){
f[j] = max(f[j],f[j-w[i]]+v[i]);
}
}
}
cout<<f[V];//cout写成count了
}
分组背包【n组,每组只能选一个】
f(i,j)从前i组物品里面选,总体积不超过j的max利益
f[i][j] = max (f[i-1,j] , f[i-1][j-w[i][k]]+v[i][k])遍历i,j,k
记录想法:原来有想过开int V[101]那没有用到的数组元素怎么办,其实在遍历时,范围已经卡好了,就是for中i和j的区间
注意:N<100开的是101的大小,因为自己规定的是v[1,2,3,...]
// 第一行有两个整数 N,V
// ,用空格隔开,分别表示物品组数和背包容量。
// 接下来有 N
// 组数据:
// 每组数据第一行有一个整数 Si
// ,表示第 i
// 个物品组的物品数量;
// 每组数据接下来有 Si
// 行,每行有两个整数 vij,wij
// ,用空格隔开,分别表示第 i
// 个物品组的第 j
// 个物品的体积和价值;
#include<iostream>
#include<algorithm>
using namespace std;
int main(){
int N,V;//N组
cin>>N>>V;
vector<int>s(N+1);
int v[101][101];
int w[101][101];
for(int i = 1;i<=N;i++){//N组
cin>>s[i];
for(int j =1;j<=s[i];j++){//每组s[i]个
cin>>w[i][j]>>v[i][j];//每个w[1,2,3,组]【1,2,3个】
}
}
vector<int>f(V+1);
// f[i][j] = max f[i-1][j],f[i-1][j-w[i][k]]+v[k]]
for(int i = 1;i<=N;i++){
for(int j = V;j>=0;j--){
for(int k = 1;k<=s[i];k++){
if(j>=w[i][k])
f[j] = max (f[j],f[j-w[i][k]]+v[i][k]);
}
}
}
cout<< f[V];
}