背包问题————AcWing
以下所有题的时/空限制:1s / 64MB
1. 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
二维做法:
i——前 i 个物品
j——体积不超过 j
f[i][j]———从前 i 个物品选择且大小不超过 j 的所有选法的最大价值
f[i][j]可以划分成两个集合:在体积不超过 j 时 ①包含第 i 个物品的 ②不包含第 i 个物品的
②很好理解,就是f[i-1][j],难点在① 的表示上,如何让 f[i][j] 中一定存在第 i 个物品呢? 答案是 f[i-1][j-v[i]] +w[i] 我们将一定存在第i个物品的 f[i][j] 表示成只存在 i - 1 个物品的且 j >= v[i] (代表能装下第 i 个物品) 的背包加上第 i 个物品的价值,这样 f[i][j]就一定有第 i 个物品
代码:
#include<iostream>
using namespace std;
const int N=1010;
int f[N][N];
int w[N],v[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++)
{
f[i][j]=f[i-1][j];//先都赋值,只有当j>=v[i]时才考虑是否加上第i个物品
if(j>=v[i]) f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
}
}
cout<<f[n][m];
}
一维优化:
类似于滚动数组,但只需要一个一维数组即可,每次拿上一次不含 i 的和当前含 i 的比较, 也就是说如果 j 的大小不足以将 i 装下就可以不用考虑,所以 j 完全可以从v[i]开始,如果将for循环写成for(int j=v[i];j<=m;j++)
会导致一个问题,我们一个更新过的 f[j] 在后面用来更新其他的 f[j] ,f[j] 的更新应该用到上一次遗留下的 f 数组而不是已经更新过的 f[j] ,相当于拿了两次第 i 个物品,所以采用倒序写法for(int j=m;j>=v[i];j--)
让 f[j] 从后往前更新,这样就可以保证不用到已经更新的 f[j] 。
代码:
#include<iostream>
using namespace std;
const int N=1010;
int f[N];
int w[N],v[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=m;j>=v[i];j--)//倒序
{
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
cout<<f[m];
}
本题总结:01背包时基础,只要这个彻底理解了其他的进阶版理解起来更容易
2. 完全背包
题目描述
有 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
二维朴素做法:
三重循环,很简单,直接上代码:
#include<iostream>
using namespace std;
const int N=1010;
int f[N][N];
int w[N],v[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++)
{
for(int k=0;k*v[i]<=j;k++)//枚举k的每一种情况
{//k是f[i][j]中含i个物品的个数
f[i][j]=max(f[i][j],f[i-1][j-v[i]*k]+w[i]*k);
}
}
}
cout<<f[n][m];
}
这是最朴素暴力的做法,成功TLE了,要将三重循环优化成二维循环才能A
将这第三层循环等价成公式得:
f[i][j] = max( f[i][j] , f[i-1][j-v[i]]+w[i] , f[i-1][j-v[i]*2]+w[i]*2…)
优化公式推导(错位相减法):
f[i][j] = max( f[i][j] , f[i-1][j-v[i]]+w[i] , f[i-1][j-v[i]*2]+w[i]*2…)
f[i-1][j]=max( f[i-1][j] , f[i-1][j-v[i]]+w[i] , f[i-1][j-v[i]*2]+w[i]*2…)
从这两个式子中我i们可以看出,式子1除去 f[i][j] 的部分(下面用A1表示)和式子2(下面用A2表示)很相似,由此得知 max(A1) = max(A2)+w[i]
所以我们可以将循环部分替换为:f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
代码:
#include<iostream>
using namespace std;
const int N=1010;
int f[N][N];
int v[N],w[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++)//当j<v[i]时直接继承f[i-1][j]
{
f[i][j]=f[i-1][j];
if(j>=v[i]) f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i]);
}
}
cout<<f[n][m];
}
一维优化:
此代码和01背包很像
其不同点如下:
if(j>=v[i]) f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i]);//完全背包
if(j>=v[i]) f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);//01背包
01背包只能拿一次,当拿了第 i 个物品时想要再变得更优只能从f[i-1][j-v[i]]上寻找突破口
完全背包可以拿无限次,所以他 f[i][j] 要和 f[i][j-v[i]] 和f[i-1][j] 比较来使自身更优
同01背包一样可以优化成一维
代码:
#include<iostream>
using namespace std;
const int N=1010;
int f[N];
int v[N],w[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=v[i];j<=m;j++)//注意,这里和01背包不同,使正序
{
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
cout<<f[m];
}
完全背包的一维优化里第二层循环是正序 ,原因:
在同一次循环中一个数利用一个刚更新完的数来更新自己就相当于又拿了一次 i ,01背包最多拿一次,所以采用倒序避免重拿,但是完全背包物品无限,所以能拿就拿,用正序即可
本题总结:完全背包和01背包都是基础,之所以从朴素版本一步步优化是为了理解其思想,有助于接下来几个题的理解
3. 多重背包
题目描述
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。 输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤100
0<vi,wi,si≤100
输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例
10
可以看出多重背包的数据量很小,这题也很简单,就是朴素版的完全背包
代码:
#include<iostream>
using namespace std;
const int N=1010;
int v[N],s[N],w[N];
int f[N][N];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>v[i]>>w[i]>>s[i];
}
for(int i=1;i<=n;i++)
{
for(int j=0;j<=m;j++)
{
for(int k=0;k<=s[i]&&k*v[i]<=j;k++)//只比完全背包多了个条件 k<=s[i]
{
f[i][j]=max(f[i][j],f[i-1][j-v[i]*k]+w[i]*k);
}
}
}
cout<<f[n][m];
}
本题总结: 数据量小的时候很简单,大的时候就该用到二进制优化了
4. 多重背包的二进制优化
题目描述:
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。 输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N≤1000
0<V≤2000
0<vi,wi,si≤2000
输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例
10
数据范围变大,计算一下上一题代码在这里的最坏情况的时间复杂度:
NxVxS[i]=1000x2000x2000=4000000000=40亿
铁超时,所以就要对 s 进行预处理,用二进制对 s 进行优化,减少第三重循环的循环次数(优化过程很像快速幂)
代码:
#include<iostream>
using namespace std;
const int N=25000;//log2000约等于12,12*2000=2400,防止越界开2500
int v[N],w[N];
int f[N];
int main()
{
int n,m;
cin>>n>>m;
int cnt=0;//所有(包括原来的和新加的)物品的数量
for(int i=0;i<n;i++)
{
int a,b,s;
cin>>a>>b>>s;
int k=1;
while(k<=s)
{
cnt++;
v[cnt]=a*k;
w[cnt]=b*k;
s-=k;
k*=2;
//这里很妙,每次对s递减,又让k*2,实现了k*2的递加效果
}
if(s>0)//当s大于时直接对加入s-k的v和w
{
cnt++;
v[cnt]=a*s;
w[cnt]=b*s;
}
}
for(int i=1;i<=cnt;i++)//对所有物品进行01背包的操作
{
for(int j=m;j>=v[i];j--)
{
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
cout<<f[m];
}
思路:
在第三层循环for(int k=0;k<=s;k++) 中,只当k=2^(0,1,…)求出 k 对应的 v 和 w 的值,然后用这些值就可求得所有 0~s 的 v 和 w 的值,举个例子:
s=576
只需要求s=1,2,4,8,16,…,256即可
1~3可以用1和2来表示
同理4~7也可以用1,2,4来表示,以此类推,最多只能表示到511,那么剩下的[512 , 576]只要直接将576 - 512的 v 于 w 值算出就可以借之前的二进制数来代表这个区间的所有数
据上述得: 2^(k+1)<=s
然后将这些二进制的数直接当作一种物品来看待,对所有的 s 处理过后,所有的物品=原物品+二进制处理后的物品,并且每个物品都只有一件,这样就成功转化成01背包问题了
本题视频讲解链接
本题总结:二进制优化很不好想,因此留下视频讲解
5. 分组背包
题目描述
有 N 组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。 每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行有两个整数 N,V,用空格隔开,分别表示物品组数和背包容量。
接下来有 N 组数据:
每组数据第一行有一个整数 Si,表示第 i 个物品组的物品数量; 每组数据接下来有 Si 行,每行有两个整数
vij,wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤100
0<Si≤100
0<vij,wij≤100
输入样例
3 5
2
1 2
2 4
1
3 4
1
4 5
输出样例
8
分组背包每组只能选一个,代码和思路很好理解
代码:
#include<iostream>
using namespace std;
const int N=110;
int v[N][N],w[N][N],s[N];
int f[N];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>s[i];
for(int j=0;j<s[i];j++)
{
cin>>v[i][j]>>w[i][j];
}
}
for(int i=1;i<=n;i++)
{
for(int j=m;j>=0;j--)//倒序
{
for(int k=0;k<s[i];k++)//是否选择同一组内第k个物品
{
if(v[i][k]<=j)
f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
}
}
}
cout<<f[m]<<endl;
}
思路:
和多重背包很像,多重背包里是选择k个同样物品,分组背包是选择同一组内的第k个物品。第二层循环时倒序的原因很简单,和01背包一样,都是为了防止重拿,01背包时防止重拿相同物品,分组背包时防止重拿同一组的物品
总结
这些背包理解起来很简单,但是难就难在如何将题目抽象成背包问题,看完y总的视频我觉得不写笔记很快会忘掉,第一次写博客,就当复习复习学到的知识,有问题希望指出。