一篇解决背包问题


0-1背包问题

image-20220401135811676

思路1:二维动态规划法

对于状态f[i] [j]定义为前i给物品下,背包重量为j的最优解:

  • 当前的状态依赖于之前的状态,那么初始的状态f[0] [0]==0开始决策。有n个物品,则需要N种决策,每一次对第i件物品的决策,状态f[i] [j]不断由之前的状态跟新而来。

  • 对于第i件物品时,如果空间的容量不够, 那么f[i] [j]==f[i-1] [j]

  • 如果背包的容量够用,那么就有两种情况:

    • 如果选择了第i件物品,那么f[i] [j]==f[i-1] [j-v[i]]]+w[i]。

    • 如果没有选择第i件物品,那么 f[i] [j]==f[i-1] [j]

    • 那么最后的情况是两者的max()

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1010;
int n,m;
int f[N][N];
int v[N],w[N];
int main()
{
    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=1;j<=m;j++)
        {
            //如果体积不够用
            if(j<v[i])
                f[i][j]=f[i-1][j];
            //如果体积够用
            else
            {
                f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
            }
        }
    }
    int res=0;
    for(int j=0;j<=m;j++) res=max(res,f[n][j]);
    cout<<res<<endl;
    return 0;
}

优化:一维将状态f[i] [j]优化为一维f[j],定义为:N件物品,背包容量为j的最优解。

  • 枚举的顺序必须是从最大容量m开始

  • 在二维的情况下,状态f[i] [j]是由上一轮的i-1的状态得来的

  • 为什么一维情况下枚举背包容量需要逆序?在二维情况下,状态f[i] [j]是由上一轮i - 1的状态得来的,f[i] [j]与f[i - 1] [j]是独立的。而优化到一维后,如果我们还是正序,则有f[较小体积]更新到f[较大体积],则有可能本应该用第i-1轮的状态却用的是第i轮的状态。

  • 例如,一维状态第i轮对体积为 33 的物品进行决策,则f[7]由f[4]更新而来,这里的f[4]正确应该是f [i - 1] [4],但从小到大枚举j这里的f[4]在第i轮计算却变成了f[i] [4]。当逆序枚举背包容量j时,我们求f[7]同样由f[4]更新,但由于是逆序,这里的f[4]还没有在第i轮计算,所以此时实际计算的f[4]仍然是f[i - 1] [4]。

  • 简单来说,一维情况正序更新状态f[j]需要用到前面计算的状态已经被「污染」,逆序则不会有这样的问题。

    状态转移方程为:f[j] = max(f[j], f[j - v[i]] + w[i] 。

for(int i = 1; i <= n; i++) 
    for(int j = m; j >= 0; j--)
    {
        if(j < v[i]) 
            f[i][j] = f[i - 1][j];  // 优化前
            f[j] = f[j];            // 优化后,该行自动成立,可省略。
        else    
            f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);  //优化前
            f[j] = max(f[j], f[j - 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]);
    }
}

完整代码

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1010;
int n,m;
int f[N];
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        int v,w;
        cin>>v>>w;
        for(int j=m;j>=v;j--)
        {
            f[j]=max(f[j],f[j-v]+w);
        }
    }
    cout<<f[m]<<endl;
    return 0;
}

完全背包问题

image-20220401151347187

思路:把完全背吧问题转化为多个0-1背包问题

#include<iostream>
#include<cstring>
using namespace std;
const int N=1010;
int n,m;
int f[N][N];
int v[N],w[N];
int main()
{
    //传入物品的种类和和背包的容量
    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;j>=k*v[i];k++)
            {
                f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
            }
        }
    }
    cout<<f[n][m]<<endl;
    return 0;
}

不出意外,该算法Time Limit Exceeded ,现在有的办法是对该算法进行将维处理。降低算法的循环次数。

  • 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 , j-v]= max( f[i-1,j-v] , f[i-1,j-2v] + w , f[i-1,j-3v]+2*w , …)
    由上两式,可得出如下递推关系:
    f[i] [j]=max(f[i,j-v]+w , f[i-1] [j])
  • 对上面的代码进行改造
for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=m;j++)
        {
            if(j>=v[i])
            {
                f[i][j]=max(f[i][j-v]+w,f[i-1][j]);
            }
        }
	}

完整代码

include<iostream>
#include<cstring>
using namespace std;
const int N=1010;
int n,m;
int f[N][N];
int v[N],w[N];
int main()
{
    //传入物品的种类和和背包的容量
    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];
            if(j-v[i]>=0)
                f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
        }
    }
    cout<<f[n][m]<<endl;
    return 0;
}

进一步优化

逆序是为了保证更新当前状态时,用到的状态是上一轮的状态,保证每个物品只有一次或零次;在这里,因为每个物品可以取任意多次,所以不再强求用上一轮的状态,即本轮放过的物品,在后面还可以再放;

思路模拟:

首先dp数组初始化全为0:给定物品种类有4种,包最大体积为5,数据来源于题目的输入
v[1] = 1, w[1] = 2
v[2] = 2, w[2] = 4
v[3] = 3, w[3] = 4
v[4] = 4, w[4] = 5

i = 1 时: j从v[1]到5
dp[1] = max(dp[1],dp[0]+w[1]) = w[1] = 2 (用了一件物品1)
dp[2] = max(dp[2],dp[1]+w[1]) = w[1] + w[1] = 4(用了两件物品1)
dp[3] = max(dp[3],dp[2]+w[1]) = w[1] + w[1] + w[1] = 6(用了三件物品1)
dp[4] = max(dp[4],dp[3]+w[1]) = w[1] + w[1] + w[1] + w[1] = 8(用了四件物品1)
dp[5] = max(dp[3],dp[2]+w[1]) = w[1] + w[1] + w[1] + w[1] + w[1] = 10(用了五件物品)

i = 2 时:j从v[2]到5
dp[2] = max(dp[2],dp[0]+w[2]) = w[1] + w[1] = w[2] = 4(用了两件物品1或者一件物品2)
dp[3] = max(dp[3],dp[1]+w[2]) = 3 * w[1] = w[1] + w[2] = 6(用了三件物品1,或者一件物品1和一件物品2)
dp[4] = max(dp[4],dp[2]+w[2]) = 4 * w[1] = dp[2] + w[2] = 8(用了四件物品1或者,两件物品1和一件物品2或两件物品2)
dp[5] = max(dp[5],dp[3]+w[2]) = 5 * w[1] = dp[3] + w[2] = 10(用了五件物品1或者,三件物品1和一件物品2或一件物品1和两件物品2)

i = 3时:j从v[3]到5
dp[3] = max(dp[3],dp[0]+w[3]) = dp[3] = 6 # 保持第二轮的状态
dp[4] = max(dp[4],dp[1]+w[3]) = dp[4] = 8 # 保持第二轮的状态
dp[5] = max(dp[5],dp[2]+w[3]) = dp[4] = 10 # 保持第二轮的状态

i = 4时:j从v[4]到5
dp[4] = max(dp[4],dp[0]+w[4]) = dp[4] = 10 # 保持第三轮的状态
dp[5] = max(dp[5],dp[1]+w[4]) = dp[5] = 10 # 保持第三轮的状态

#include<iostream>
#include<cstring>
using namespace std;
const int N=1010;
int n,m;
int f[N];
int v[N],w[N];
int main()
{
    //传入物品的种类和和背包的容量
    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[j]=f[j];
            if(j-v[i]>=0)
                f[j]=max(f[j],f[j-v[i]]+w[i]);
        }
    }
    cout<<f[m]<<endl;
    return 0;
}

多重背包问题一

image-20220401165420165

思路一:帮多重背包问题强行拆为0-1背包问题

#include<iostream>
#include<cstring>
using namespace std;
int n,m;
//分别保存体积和价值
int v[10010],w[10010],t=0;
int dp[10010];
int main()
{
    cin>>n>>m;
    while(n--)
    {
        int v,w,s;
        cin>>v>>w>>s;
        while(s--)
        {
            //拆开多重背包问题
            a[++t]=v;
            b[t]=w;
        }
    }
    //解决0-1背包问题
    for(int i=1;i<=t;i++)
    {
        for(int j=m;j>a[i];j--)
        {
            dp[j]=max(dp[j],dp[j-a[i]+b[i]]);
        }
    }
    cout<<dp[m]<<endl;
    return 0;
}

复杂度有点高,对代码进行优化

#include<iostream>
#include<cstring>
using namespace std;
int dp[1010],n,t,v,w,s;
int main()
{
    cin>>n>>t;
    while(n--)
    {
        cin>>v>>w>>s;
        for(int i=1;i<=s;i++)
        {
            for(int j=t;j>=w;j--)
            {
                dp[j]=max(dp[j],dp[j-w]+w);
            }
        }
    }
    cout<<dp[t]<<endl;
    return 0;
}

思路二

#include<iostream>
#include<cstring>
#include<algorithm>
const int N=110;
int n,m;
int f[N];
using namespace std;
int main()
{
   cin>>n>>m;
   for(int i=1;i<=n;i++)
   {
       int v,w,s;
       cin>>v>>w>>s;
       for(int j=m;j>=0;j--)
       {
           for(int k=1;k<=s&&k*v<=j;k++)
           {
                f[j]=max(f[j],f[j-k*v]+k*w);
           }
       }
   }
   cout <<f[m]<<endl;
   return 0;
}

多重背包问题二

image-20220401172222815

二进制优化:对于任何一个数s,最少有[logs]个数可以表示0~s的所有整数。

比如对于7,我们有3个数:1,2,4(2的0次方,2的1次方,2的平方)可以表示0~7所有的数。

0=0; 1=1;2=2;3=1+2;4=4;5=1+4;6=2+4;7=1+2+4;

再不如10,我们有[log2 10]=4个数能表示010所有的数;010=(0~7)+3;

所有数分别为:1,2,4,3;

0=0; 1=1;2=2;3=3;4=4;5=1+4;6=2+4;7=1+2+4;8=1+3+4;

9=2+3+4;10=1+2+4+3

对于这个题:

我们可以把多重背包问题按照二进制转换为N*[log2 k]个0-1背包问题

#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=2010;
int n,m;

int f[N];

struct good
{
    int v;
    int w;//分别表示体积和价格
};
int main()
{
    vector<good>goods;
    cin>>n>>m;
    for(int i=0;i<n;i++)
    {
        int v,w,s;
        cin>>v>>w>>s;
        //把多重背包问题按照二进制转换为多个0-1背包问题
        for(int k=1;k<=s;k*=2)
        {
            s-=k;
            goods.push_back({v*k,w*k});
        }
        if(s>0) goods.push_back({v*s,w*s});
    }
    for(auto good:goods)
        for(int j=m;j>=good.v;j--)
        {
            f[j]=max(f[j],f[j-good.v]+good.w);
        }
        
    cout<<f[m]<<endl;
    return 0;
    
}

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

影中人lx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值