01背包问题
分析
对于每一个物品,在背包容量足够的情况下,我们有两种状态,一种是把该物品放在背包,一种是不把该物品放在背包。
我们建立一个二维的数组, d p [ i ] [ j ] dp[ i ][ j ] dp[i][j]表示前 i i i 个物品,在容量为 j j j 的时候的最大价值。
V [ i ] V[ i ] V[i] 表示物品所占的体积 , W [ i ] W[ i ] W[i] 表示物品的价值
- 不把该物品放在背包: d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[ i ][ j ] = dp[ i - 1][ j ] dp[i][j]=dp[i−1][j],我们直接顺延上一个物品选择的结果
- 把该物品放在背包: d p [ i ] [ j ] = m a x ( d p [ i ] [ j ] , d p [ i − 1 ] [ j − V [ i ] ] + W [ i ] ) dp[ i ][ j ]=max(dp[ i ][ j ],dp[i-1] [ j-V[ i ] ] + W[ i ]) dp[i][j]=max(dp[i][j],dp[i−1][j−V[i]]+W[i])
因为要把这个物品放在背包,所以背包中首先得有该物品的容量大小。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int dp[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++)
{
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]<<endl;
return 0;
}
在动态规划中,对于这种当前状态只与前一层的状态有关的情况,我们就可以对数组进行压缩,可以把二维变成一维。
改进后
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int dp[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 = m; j >= V[i]; j--)
dp[j] = max(dp[j], dp[j - V[i]] + W[i]);
cout<<dp[m]<<endl;
return 0;
}
拓展问题
上面的问题求的是,背包中物品的总价值最大,但是此时背包确是不一定满的。
如果问题改为,求背包中物品的体积之和恰好为背包的容量的时候,怎么做呢?我们可以在初始化的时候做点手脚。
通过改进后,我们就可以确保 d p [ n ] dp[ n ] dp[n]的值是从初始状态转移过来的。也就是背包中物品体积恰好为背包体积时,背包中物品的最大价值,
完全背包
分析
完全背包问题与01背包的一个不同点就是,完全背包问题中一个物品在不超过背包体积的情况下,可以无限用。
与01背包相比,只有一个小小的变化,我们也可以引用01背包问题的思路
- dp[ i ] :表示当前背包体积时,物品的最大价值
- 状态转移方程:max(dp[ j ], dp[ j - V[ i ]] + W[ i ])
与01背包相比,改进的地方
因为需要考虑无限放的问题,就可以从前向后遍历。这样我们在每一个体积的时候,看上去值减去了一个物品的重量,但是我们是从前向后的,我们在后面的每一次状态,其实就可以看成是一个迭代的的过程。他是在前面已经选择该物品的前提下,再选一次这个物品。
#include <iostream>
using namespace std;
const int N = 1010;
int dp[N];
int n,m;
int main()
{
cin>>n>>m;
for(int i = 1; i <= n; i++)
{
int v,w;
cin>>v>>w;//v 物品体积 w 物品重量
for(int j = v; j <= m; j++)
dp[j] = max(dp[j], dp[j - v] + w);
}
cout<<dp[m]<<endl;
return 0;
}
多重背包 I
分析
与01背包不同的是,多重背包的每一个物品有 S S S件,而01背包每个物品有 1 1 1件。
我们是不是可以进行一个拆分,把这 S S S件物品拆成01背包的模型来计算。
我们再看物品的数量,假设最多的 100 100 100件物品,每个物品可以取 100 100 100次,我们使用三重循环的话,时间复杂度是 100 ∗ 100 ∗ 100 100 * 100 * 100 100∗100∗100,也就是 1 0 6 10^6 106。而C++程序一秒大概可以执行 1 0 7 ∼ 1 0 8 左 右 10^7 \sim10^8左右 107∼108左右的次数。
也就是说我们简单使用三重循环的暴力方法是可以过的。
#include <iostream>
using namespace std;
const int N = 110;
int dp[N];
int n,m;
int main()
{
cin>>n>>m;
for(int i = 1; i <= n; i++)
{
int v,w,s;//v 物品所占的体积 w 物品的重量 s 物品的数量
cin>>v>>w>>s;
for(int j = m; j >= v; j--)
for(int k = 1; k <= s && (k * v <= j); k++)
dp[j] = max(dp[j], dp[j - k * v] + k * w);
}
cout<<dp[m]<<endl;
return 0;
}
多重背包 II
分析
看了一眼数据范围, 1000 × 2000 × 2000 1000 \times 2000 \times 2000 1000×2000×2000,直接暴力三重循环肯定超时了。
提示中显示,可以使用二进制优化的方法,也就是使用二进制对背包进行压缩。
我们直接把所有物品都变成一个一个的肯定不行,我们可以利用二进制独特的压缩方式,可以把多个物品压缩成一个物品,这样物品的数量压缩的范围简化到 1000 × l o g ( 2000 ) 1000 \times log(2000) 1000×log(2000),时间复杂度近似于 1000 × 11 × 2000 1000 \times 11 \times 2000 1000×11×2000,我们看这是可以在 1 S 1S 1S中跑完的,时间复杂度可以通过了。
#include <iostream>
#include <vector>
using namespace std;
const int N = 2010;
int n,m;
int dp[N];
//存放压缩后物品的仓库
typedef struct Good
{
int v,w;
}Good;
int main()
{
cin>>n>>m;
vector<Good> goods;
for(int i = 1; i <= n; i++)
{
int v,w,s;
cin>>v>>w>>s;
//状态压缩
//10 -> 1 + 2 + 4 + 3
for(int k = 1; k <= s; k *= 2)