目录
背包问题是很经典得动态规划问题,背包问题有很多种,下面一一介绍
一,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
对于这样得问题我们该怎么取分析呢
基本上所有得dp问题都可以这么去分析,先找到状态表示,再找到状态如何转移的,也就是状态计算,状态表示我们一般根据题意用一个一维或者二维甚至更多维的一个数组去表示,每一个状态代表一个集合,我们要弄清楚这个集合是表示了什么,然后找到这个集合的属性,集合的属性一般为最大值,最小值,和数量这三种,再对于状态计算,我们要将集合划分,看看这个集合是由什么状态转移过来的,再进行集合划分的时候我们要做到不重不漏,这样一个集合就能包含所有的情况。
1,二维数组解法
接着我们用这题结合上面的这张图来进行分析,首先我们要找到状态表示
以dp[i][j]表示只考虑前 i 件物品,容量不大于 j 的所有集合,这个集合的属性就是求最大值,所以我们dp[i][j]表示的是在容量为 j 时选择前 i 件物品的价值的最大值
然后我们看如何进行集合划分,假设有最后一件物品 i ,我们可以有两种选择,选择这件物品,和不选择这件物品,会出现以下两种情况
不选这件物品,那么dp[i][j]=dp[i-1][j],即表示在容量为j时选择前i-1件物品的最大值
选择这件物品的话只能在此时的容量比最后一件物品的体积大的种情况下,因为这样才能装下这件物品,那么dp[i][j]=dp[i-1][j-v[i]]+w[i],即表示在容量为 j-v[i] 的情况下选择前 i-1 件物品的最大价值加上第 i 件物品的价值
到这所有的集合我们就不重不漏的划分完了,如何用代码实现呢
代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e3+10;
int v[N],w[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++)
{
dp[i][j]=dp[i-1][j];
if(j>=v[i])
dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]);
}
}
cout<<dp[n][m];
return 0;
}
2,一维数组解法
通过观察上面的状态转移方程,我们可以发现每个dp[i][j]只与前 i-1 项有关,那么我们可以用滚动数组的方式求出来每个状态,每次for循环前 dp[j] 都表示前一层的状态,想要得到第 i 层的状态可以用第 i-1 层的状态 。因为dp[j-v[i]]一定比dp[j]小,所以我们要倒叙遍历第二层,防止我们更新第 i 层的数据时用到的第 i-1 层数据被更新了
用一个一维数组 dp[j] 表示容量不大于 j 时的物品最大值
代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e3+10;
int v[N],w[N],dp[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--)
{
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
}
}
cout<<dp[m];
return 0;
}
二,完全背包
题目描述
有 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
完全背包与01背包的区别就是01背包每个物品只能最多选择一次,完全背包中的每个物品可以选择无限次
1,二维数组解法
我们继续用上面的方法进行分析,同样先以一个二维数组 dp[i][j] 表示仅考虑前 i 项物品,容量不大于 j 的价值的集合,集合的属性同样是最大值,接下来是集合划分,同样是有最后一件物品 i ,我们可以将他划分成不选择最后一件物品,选择最后一件物品 1,2,3……k次的集合
不选的情况就是dp[i][j]=d[i-1][j]
选的情况就是dp[i][j]=dp[i-1][j-k*v[i]]+k*w[i]
我们发现当k=0时,就是不选的情况所以将上述两个转移方程合并,就是
dp[i][j]=dp[i-1][j-k*v[i]]+k*w[i]
接下来时代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e3 + 10;
int dp[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++)
dp[i][j] = max(dp[i][j], dp[i - 1][j - k * v[i]] + k * w[i]);
cout << dp[n][m];
return 0;
}
但是这个做法的时间复杂度差不多是O(N^3),比较慢,我们看看如何优化
2,简单优化的解法
根据上面的转移方程列出这两个等式,可以发现dp[i][j]=max(dp[i-1][j],dp[i][j-v]+w),那么就跟k没有关系了,我们就可以少一层循环了,时间复杂度就变成了O(N^2)
代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e3 + 10;
int dp[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++)
{
dp[i][j]=dp[i-1][j];
if(j>=v[i])
dp[i][j] = max(dp[i][j], dp[i][j -v[i]] + w[i]);
}
cout << dp[n][m];
return 0;
}
3,一维数组解法
同样的我们也可以用一维数组进行优化,用滚动数组进行优化,第 i 层的状态首先时由 i-1 层状态转移过来的,然后我们第二层循环要正序遍历,因为第 i 层的状态是由第 i 层的状态不断更新的,正序遍历的时候就是在不断更新第 i 层的状态
代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e3 + 10;
int dp[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 = v[i]; j <= m; j++)
dp[j] = max(dp[j], dp[j -v[i]] + w[i]);
cout << dp[m];
return 0;
}
三,多重背包
题目描述
有 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
多重背包的不同就是每个物品限定了件数s,不是无限件
1,二维数组解法 时间复杂度O(NVS)
多重背包的二维解法与完全背包的思路是一样的,只是完全背包是一件物品最多k次,这里是s次,多加一个判断条件就可以了
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 110;
int n, m;
int dp[N][N];
int v[N], w[N], s[N];
int main()
{
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++)
dp[i][j] = max(dp[i][j], dp[i - 1][j - k * v[i]] + k * w[i]);
cout << dp[n][m];
return 0;
}
2,二进制优化解法 时间复杂度O(NV logS)
当数据范围比较大时
数据范围
0<N≤≤1000
0<V≤2000
0<vi,wi,si≤2000
上面的做法就会超时,这时我们用二进制优化,可以将时间复杂度变成O(NVlogS)
二进制优化就是将s分组打包,分成几组,例如第 i 件物品的件数s=200时,我们将s分成1,2,4,8,16,32,64,73,这几组,从这几组里面去选,一定能凑出来0~200中的任何一个数,将全部s分组那么问题就转换成了01背包了,对于每一组我们可以选或不选,例如如果选择了第一组和第三组,就表示我们选择了1+4=5件 i 商品
代码如下:
这里要注意一下数组开多大的问题,N最大为1000,每件物品最大为2000,那么最多就是分成11组,保险起见我们认为最多分成12组,避免数组开小了,所以总共就是可以分成1000*12=12000组,因此数组最小要开到12000,否则会越界
#include<iostream>
#include<algorithm>
using namespace std;
const int N=12000,M=2010;
int v[N],w[N];
int dp[M];
int main()
{
int n,m,cnt=0;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
int a,b,c;
cin>>a>>b>>c;
int k=1;
while(k<=c)
{
cnt++;
v[cnt]=a*k;
w[cnt]=b*k;
c-=k;
k*=2;
}
if(c>0)
{
cnt++;
v[cnt]=c*a;
w[cnt]=c*b;
}
}
n=cnt;
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];
return 0;
}
3,单调队列优化解法
在多重背包中,我们每个物品最多选择s个,我们以f[i][j]表示在容量为j时在前i个物品中选择的最大价值,那么
f[i][j]=max(f[i-1][j], f[i-1][j-v]+w, f[i-1][j-2v]+2w, f[i-1][j-3v]+3w …… f[i-1][j-sv]+sw)
f[i][j-v]=max( f[i-1][j-v], f[i-1][j-2v]+w, f[i-1][3v]+2w …… f[i-1][j-sv]+(s-1)w,f[i-1][j-(s+1)w)
我们发现对于每个j,他只会由跟他余数相同的数转移过来,并且是前s个(这里先假设背包空间足够大),假设 j mod v= r
那么 j 就是由r,r+v,r+2*v, r+3v, ……r+kv转移过来的,因此对于每个j,我们只用在他前面的数并且余数相同的数找最大值就可以了
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=20010;
//q存储的是空间容量v,backup用来拷贝数组,dp表示容量为j是的最大价值
int dp[N],backup[N],q[N];
int n,m;
int main()
{
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++)
{
int v,w,s;
scanf("%d%d%d",&v,&w,&s);
memcpy(backup,dp,sizeof dp);//因为每一层用到的是上一层的数据,所以需要进入第i层时要拷贝第i-1层的数据
for(int j=0;j<v;j++)
{
int hh=0,tt=-1;
for(int k=j;k<=m;k+=v)
{
if(hh<=tt&&k-s*v>q[hh])hh++;//如果对头元素存储的空间已经滑出窗口了就要出队
while(hh<=tt&&backup[q[tt]]-(q[tt]-j)/v*w<=backup[k]-(k-j)/v*w)tt--;//对于每一个要减去偏移量的w
if(hh<=tt)
dp[k]=max(dp[k],backup[q[hh]]+(k-q[hh])/v*w);
q[++tt]=k;
}
}
}
printf("%d",dp[m]);
return 0;
}
四,分组背包
题目描述
有 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
分组背包就是在每个组中最多选择一件商品,本质上跟01背包差不多,我们只要遍历每个组的物品,进行选或者是不选就行了
#include<iostream>
#include<algorithm>
using namespace std;
const int N=110;
int n,m;
int v[N][N],w[N][N],s[N];
int dp[N];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>s[i];
for(int j=1;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=1;k<=s[i];k++)
if(v[i][k]<=j)//max是在不断更新,但是只保留最大值,所以我们可以保证每个组最多只选了一个
dp[j]=max(dp[j],dp[j-v[i][k]]+w[i][k]);
cout<<dp[m];
return 0;
}