01背包
概念:01背包是最基础的背包问题,大概意思就是从n件物品,m的背包容量下选出总价值最大的一些物品装入背包,每件物品只有一件并且每件物品只有选和不选两个状态,因此得名01背包。
状态转移方程:dp[i][j]的含义是前i件物品容量为j的情况下最大价值。
状态转移分析
1.dp[i][j] = dp[i-1][j],第i件物品的体积w[i] > j,也就是装不下,所以前i件物品j容量下所获得的最大价值就是前i-1件物品容量为j下所获得的最大价值。
2.dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]),如果当前容量j >= w[i],那么当前物品我可以选也可以不选。不选就是前i-1件物品的价值(dp[i-1][j],选的话就是dp[i-1][j-w[i]]+v[i]。两者中取一个最大值。
3.最后直接dp[n][m]就可以得到n件物品m容量下装入的最大价值是多少,如果我们需要知道选了那些物品怎么看呢?因为我们每件物品的状态只有选和不选两种状态,也就是对应的dp转移方程dp[i-1][j]和dp[i-1][j-w[i]]+v[i]。
黑色是当前的状态dp[i][j],如果选了那么必定是从蓝色dp[i-1][j-w[i]]转移过来,如果没选必定是从红色dp[i-1][j]转移过来,所以我们可以回溯去找。如果dp[i][j] = dp[i-1][j],那么第i件物品必定没有选进来,否则第i件物品一定放入了背包中。因此可以得到下面的时间复杂度为O(nm)复杂度,空间复杂度是O(nm)的代码。
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 1005;
const int maxm = 100005;
struct info{
int w; //重量
int v; //价值
}s[maxn];
int n,m;
int dp[maxn][maxn];
void dfs(int x,int y)
{
if(x == 0 || y == 0){ //递归出口
return ;
}
if(dp[x][y] == dp[x-1][y]){ //从正上方转移过来说明没选
dfs(x-1,y);
}
else{ //当前这个物品选了,从左上角转移过来
dfs(x-1,y-s[x].w);
printf("i = %d, w = %d, v = %d\n",x,s[x].w,s[x].v);
}
}
void solve()
{
memset(dp,0,sizeof(dp));
for(int i = 1;i <= n;i++){ //枚举每一个商品
for(int j = 0;j <= m;j++){ //从左往右算
if(j < s[i].w){ //当前容量放不下i号物品
dp[i][j] = dp[i-1][j]; //那么当前最大价值就是前i-1件物品的最大价值
}
else{ //当前容量可以放下,那么就考虑第i件物品放和不放产生的最大价值
dp[i][j] = max(dp[i-1][j],dp[i-1][j-s[i].w]+s[i].v);
}
}
}
cout << dp[n][m] << endl;
dfs(n,m);
}
int main()
{
while(cin>>n>>m){
for(int i = 1;i <= n;i++){
cin >> s[i].w >> s[i].v;
}
solve();
}
return 0;
}
01背包滚动数组优化
关于优化问题,01背包在时间复杂度上已经无法在继续优化了,但是可以优化空间复杂度。根据上面的分析与代码可以发现,黑色当前状态只由蓝色区域或者红色区域转移过来,并不会由绿色部分转移过来。所以我们可不可以算到dp[i][j]的值覆盖写在dp[i-1][j]的位置上呢?
如果直接将得到的值覆盖发现更新的值会不对,比如我们更新dp[i][4]的时候应该是dp[i-1][4-2] + 6 = 12,很明显不对,正确答案应该是3 + 6 = 9。为什么会不对?我们将dp[i][j]的值写入了dp[i-1][j]的位置,而dp[i][j]的值只可能大于等于dp[i-1][j],所以我们在更新dp[i][4]的时候实际用的是dp[i][2] + 6 = 12,而不是dp[i-1][2]的值去更新换句话说我们用了第i行j前面的值更新了dp[i][j],所以这个覆盖是失败的。为了保证每次更新都是正确的如果我们从m开始往前更新就不会出现这种错误。但是这样优化空间也有一个缺点就是不知道选了哪些物品,丢失了中间过程,无法回溯找到装入背包的商品编号
int dp[maxm];
void solve_extend()
{
//dp数组可以优化空间把空间复杂度从O(n*m)降低到O(m);
//计算dp[i][j]的值时候只用到了dp[i-1][j]的值和dp[i-1][j-s[i].w]的值
//也就是(i,j)正上方和左上方的值。这是从左往右算的时候的情况
//如果想进行滚动数组的优化那么必须从右往左算,因为从左往右算的时候
//dp[j]的值可能是当前第i行的值不是第i-1行的值
memset(dp,0,sizeof(dp));
for(int i = 1;i <= n;i++){
for(int j = m;j >= s[i].w;j--){ //从后往前更新保证每次使用的值都是覆盖之前的值
dp[j] = max(dp[j],dp[j-s[i].w]+s[i].v);
}
}
cout << dp[m] << endl;
}
完全背包
概念:完全背包大概意思就是从n件物品,m的背包容量下选出总价值最大的一些物品装入背包,每件物品只有无数件,可以任意选择0件,1件,2件,… k件。当然由于有背包的限制所以不可能选出无数件,每种物品最多是m/w[i]件。
状态转移方程:dp[i][j]的含义是前i件物品容量为j的情况下最大价值。
状态转移分析
1.dp[i][j] = dp[i-1][j],第i件物品的体积w[i] > j,也就是装不下,所以前i件物品j容量下所获得的最大价值就是前i-1件物品容量为j下所获得的最大价值。
2.dp[i][j] = max(dp[i-1][j],dp[i-1][j-kw[i]]+kv[i]),如果当前容量j >= kw[i],如果不选第i件物品也就是前i-1件物品的价值(dp[i-1][j],选的话就是dp[i-1][j-kw[i]]+kv[i]。两者中取一个最大值。很容易得到下面的代码,时间复杂度O(nm∑m/w[i]),空间复杂度是O(nm);
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
const int maxn = 1001;
using namespace std;
struct info{
int w;
int v;
}s[maxn];
int n,m;
int dp[maxn][maxn];
void solve()
{
memset(dp,0,sizeof(dp));
for(int i = 1;i <= n;i++){
for(int j = 0;j <= m;j++){
for(int k = 0;k*s[i].w <= j;k++){
dp[i][j] = max(dp[i][j],dp[i-1][j-k*s[i].w] + k*s[i].v);
}
}
}
cout<<dp[n][m]<<endl;
}
int main()
{
while(cin>>n>>m){
for(int i = 1;i <= n;i++){
cin>> s[i].w >> s[i].v;
}
solve();
}
return 0;
}
完全背包与优化
完全背包优化可以优化时间复杂度到O(n*m),空间复杂度可以优化到O(m)。还是刚才那个问题,01背包的优化代码为什么要从后往前更新,是不是因为01背包从前往后更新会导致复用更新后的值再去更新,也就是说当前物品重复选了放入了背包,那我们是不是可以利用这一点来做完全背包呢?01背包滚动数组优化最怕的是不是刚好就是完全背包想要的?
更新dp[i][4]的时候如果复用dp[i][2]的值会怎么样,是不是相当于dp[i][2]这个状态放入了一个第i件物品,dp[i][4]又放入了一个第i件物品,也就是从dp[i][j] = dp[i-1][j-2w[i]] + 2v[i]。是不是很奇妙?这样就可以把复杂度降低到O(n*m),如果想优化空间也可以把空间降低到O(m)。
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 1005;
const int maxm = 100005;
struct info{
int w; //重量
int v; //价值
}s[maxn];
int n,m;
int dp[maxn][maxn];
void solve()
{
memset(dp,0,sizeof(dp));
for(int i = 1;i <= n;i++){
for(int j = 0;j <= m;j++){
if(j < s[i].w){ //如果当前容量放不下第i件物品,那么当前的最大价值就是前i-1件的价值
dp[i][j] = dp[i-1][j];
}
else{ //如果当前容量够得话,那么考虑第i件物品 不放 和 放多少件的最大价值
dp[i][j] = max(dp[i-1][j],dp[i][j-s[i].w]+s[i].v);
//该转移方程神奇就神奇在从左向右更新过程中dp[i][j]的值是可能被不同数量的第i件物品更新过的
//这是01背包最忌惮的事情,但是确实完全背包想要达到的目的。
//dp[i][j] = max(dp[i-1][j],dp[i][j-k*s[i].w] + k*s[i].v); k = 0恰好是不选的情况
}
}
}
cout << dp[n][m] << endl;
}
/*
int dp[maxm];
void solve_extend() //时间优化 + 空间优化
{
//dp数组的复杂度从O(nm)降到O(m);
//从左往右更新恰好能在更新dp[j-w[i]]的情况从已经被第i件物品更新过dp[j]引用过来
//01背包滚动数组做法最怕的就是更新dp[j-w[i]]的时候前面的dp[j]以前被第i件商品更新过的值
memset(dp,0,sizeof(dp));
for(int i = 1;i <= n;i++){
for(int j = s[i].w;j <= m;j++){ //从左往右更新恰好和01背包相反
dp[j] = max(dp[j],dp[j-s[i].w]+s[i].v);
}
}
cout << dp[m] << endl;
}
*/
int main()
{
while(cin>>n>>m){
for(int i = 1;i <= n;i++){
cin>> s[i].w >> s[i].v;
}
solve();
// solve_extend();
}
return 0;
}
多重背包
概念:完全背包大概意思就是从n件物品,m的背包容量下选出总价值最大的一些物品装入背包,每件物品Ci件。当然由于有背包的限制所以也不可能选出无数件。多重背包和完全背包很像,但是又不像,因为完全背包每件商品是无数件,多重背包每件商品都有固定的件数。
状态转移方程:dp[i][j]的含义是前i件物品容量为j的情况下最大价值。
状态转移分析
1.dp[i][j] = dp[i-1][j],第i件物品的体积w[i] > j,也就是装不下,所以前i件物品j容量下所获得的最大价值就是前i-1件物品容量为j下所获得的最大价值。
2.dp[i][j] = max(dp[i-1][j],dp[i-1][j-kw[i]]+kv[i]),如果当前容量j >= kw[i],如果不选第i件物品也就是前i-1件物品的价值(dp[i-1][j],选的话就是dp[i-1][j-kw[i]]+k*v[i]。两者中取一个最大值。很容易得到下面的代码,时间复杂度O(nm∑Ci),空间复杂度是O(nm);
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 105;
struct info{
int w,v,n;
}s[maxn];
int dp[maxn][maxn];
int n,m;
void solve()
{
memset(dp,0,sizeof(dp));
for(int i = 1;i <= n;i++){
for(int j = 0;j <= m;j++){
for(int k = 0;k <= s[i].n;k++){
if(k*s[i].w <= j){
dp[i][j] = max(dp[i][j],dp[i-1][j-k*s[i].w] + k*s[i].v);
}
}
}
}
cout << dp[n][m] << endl;
}
int main()
{
ios::sync_with_stdio(false);cin.tie(0);
while(cin >>n >> m){
for(int i = 1;i <= n;i++){
cin >> s[i].w >> s[i].v >> s[i].n;
}
solve();
}
return 0;
}
多重背包与优化
多重背包的优化大致有三类:
-
将多重背包拆成最裸的01背包来做,也就是∑Ci件物品,m的容量下的一个朴素01背包。
-
采用二进制分解优化。
3.单调队列优化,目前还不会。
二进制分解优化
从2 ^ 0, 2 ^ 1,2 ^ 2,2 ^ 3,2 ^ (k-1)这k个2的整数次幂中选出若干个相加,可以表示出0~2 ^ k - 1之间的任何整数。进一步的,我们求出满足
2 ^ 0 + 2 ^ 1 + 2 ^ 2 + … + 2 ^ p <= Ci的最大整数p,设R = Ci - 2 ^ 0 - 2 ^ 1 - 2 ^ 2 - … - 2 ^ p。
-
根据最大性,有2 ^ 0 + 2 ^ 1 + 2 ^ 2 + … + 2 ^ p + 2 ^ (p+1) > Ci;因此从2 ^ 0, 2 ^ 1,2 ^ 2,2 ^ 3,2 ^ p,中选出若干个相加可以表示出0~R之间的任何整数。
-
从2 ^ 0, 2 ^ 1,2 ^ 2,2 ^ 3,2 ^ p,以及R中选出若干个相加可以表示出R~R+2^(p+1) - 1之间的任何整数。而根据R的定义,R + R+2 ^ (p+1) - 1 = Ci;因此从2 ^ 0, 2 ^ 1,2 ^ 2,2 ^ 3,2 ^ p,R中选出若干个相加可以表示出R ~ Ci之间的任何整数。
-
综上所述,我们可以将数量Ci拆成 p + 2个物品,体积分别是 2 ^ 0×W[i], 2 ^ 1×W[i],2 ^ 2×W[i],2 ^ 3×W[i],2 ^ p×W[i],R×W[i]。这样物品总数就变成了O(∑logCi)。转化完就可以继续套用01背包解题了。总体时间复杂度还是相当棒的O(m*∑logCi)。
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
struct info{
int h;
int p;
}s[2005];
int dp[105];
inline void solve(int n,int m)
{
memset(dp,0,sizeof(dp));
for(int i = 0;i < n;i++){
for(int j = m;j >= s[i].p;j--){
dp[j] = max(dp[j],dp[j-s[i].p]+s[i].h);
}
}
cout << dp[m] << endl;
}
int main()
{
ios::sync_with_stdio(false);cin.tie(0);
int t,n,m;;
cin >> t;
while(t--){
cin >> m >> n;
int cnt = 0;
for(int i = 0;i < n;i++){
int x,y,z;
cin >> x >> y >> z;
int t = 1;
while(z - t > 0){
s[cnt].p = t*x;
s[cnt++].h = t*y;
z -= t;
t <<= 1;
}
s[cnt].p = z*x;s[cnt++].h = z*y;
}
solve(cnt,m);
}
return 0;
}
愿你走出半生,归来仍是少年~