【背包问题】 01背包+完全背包+多重背包

版权声明:本文为博主原创文章,转载请注明出处( • ̀ω•́ )✧ https://blog.csdn.net/wangws_sb/article/details/83932332

前言

背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中。相似问题经常出现在商业、组合数学,计算复杂性理论、密码学和应用数学等领域中。也可以将背包问题描述为决定性问题,即在总重量不超过W的前提下,总价值是否能达到V?它是在1978年由Merkel和Hellman提出的。

一、01背包

问题描述:有N件物品和一个容量为V的背包。第i件物品的重量是w[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。

基本思路:这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。用子问题定义状态:即f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:f[i][v]=max( f[i-1][v], f[i-1][v-w[i]]+v[i] )。可以压缩空间:f[v]=max( f[v],f[v-w[i]]+v[i] )这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下:"将前i件物品放入容量为v的背包中"这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。

1. 如果不放第i件物品,那么问题就转化为"前i-1件物品放入容量为v的背包中",价值为f[i-1][v];

2. 如果放第i件物品,那么问题就转化为"前i-1件物品放入剩下的容量为v-w[i]的背包中",此时能获得的最大价值就是f [i-1][v-w[i]]再加上通过放入第i件物品获得的价值v[i]。

注意:对 f[ ][ ]初始化时,若题目要求背包恰好装满,那么f[0][0]初始化为0,其他初始化为-INF。若题目没有要求背包恰好装满,那么对f[ ][ ]数组全部初始化为0。

//01背包核心代码
//初始化f[][]数组
f[0][0]=0;
for(int i=1;i<=n;i++)//要求背包恰好装满
    for(int j=0;j<=V;j++)
        f[i][j]=-INF;
for(int i=1;i<=n;i++)//不要求背包是否装满
    for(int j=0;j<=V;j++)
        f[i][j]=0;
for(int i=1;i<=n;i++)
{
    for(int j=0;j<=V;j++)
    {
        f[i][j]=f[i-1][j];
        if(j>=w[i])
            f[i][j]=max(f[i][j],f[i-1][j-w[i]]+v[i]);
    }
}

优化:以上方法的时间和空间复杂度均为O(N*V),其中时间复杂度基本已经不能再优化了,但空间复杂度却可以优化到O(V)。

先考虑上面讲的基本思路如何实现,肯定是有一个主循环i=1..N,每次算出来二维数组f[i][0..V]的所有值。那么,如果只用一个数组f [0..V],能不能保证第i次循环结束后f[v]中表示的就是我们定义的状态f[i][v]呢?

f[i][v]是由f[i-1][v]和f [i-1][v-w[i]]两个子问题递推而来,能否保证在推f[v]时(也即在第i次主循环中推f[v]时)能够得到f[v]和f[v -w[i]]的值呢?事实上,这要求在每次主循环中我们以v=V..0的顺序推f[v],这样才能保证推f[v]时f[v-w[i]]保存的是状态f[i-1][v-c[i]]的值。伪代码如下:

for i=1..N

for v=V..0

f[v]=max{f[v],f[v-w[i]]+v[i]};

其中的f[v]=max{f[v],f[v-w[i]]}一句恰就相当于我们的转移方程f[i][v]=max{f[i-1][v],f[i-1][v-w[i]]},因为的

f[v-w[i]]就相当于原来的f[i-1][v-w[i]]。如果将v的循环顺序从上面的逆序改成顺序的话,那么则成了f[i][v]由f[i][v-w[i]]推知,与本题意不符,但它却是另一个重要的背包问题完全背包最简捷的解决方案,故学习只用一维数组解01背包问题是十分必要的。

//优化后的代码,降低了空间复杂度
for(int i=1;i<=n;i++)
{
    for(int j=V;j>=w[i];j--)//注意一定要逆序循环
    {
        f[j]=max(f[j],f[j-w[i]]+v[i]);
    }
}

二、完全背包

问题描述:有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的体积是c,价值是w。将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。

基本思路:这个问题非常类似于01背包问题,所不同的是每种物品有无限件,也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……取[V/c]件等很多种。如果仍然按照解01背包时的思路,令f[v]表示前i种物品恰放入一个容量为v的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:f[i][j]=max{f[i-1][j],f[i-1][j-k*c]+k*w}(0<=k*c<=v)这跟01背包问题一样有O(N*V)个状态需要求解,但求解每个状态f[v]的时间是O(V/c),总的复杂度是超过O(VN)的。

注意:对 f[ ][ ]初始化时与01背包相同,若题目要求背包恰好装满,那么f[0][0]初始化为0,其他初始化为-INF。若题目没有要求背包恰好装满,那么对f[ ][ ]数组全部初始化为0。

//完全背包核心代码
for(int i=1;i<=n;i++)
{
    for(int j=V;j>=c[i];j--)
    {
        for(int k=0;k*c[i]<=j;k++)
            f[i][j]=max(f[i-1][j],f[i-1][j-k*c[i]]+k*w[i]);
    }
}

优化:完全背包问题有一个很简单有效的优化,是这样的:若两件物品i、j满足c[i]<=c[j]且w[i]>=w[j],则将物品j去掉,不用考虑。这个优化的正确性显然:任何情况下都可将价值小费用高得j换成物美价廉的i,得到至少不会更差的方案。首先将费用大于V的物品去掉,然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可以O(V+N)地完成这个优化。

既然01背包问题是最基本的背包问题,那么我们可以考虑把完全背包问题转化为01背包问题来解。最简单的想法是,考虑到第i种物品最多选V/c件,于是可以把第i种物品转化为V/c件费用为c[I]及价值w[I]的物品,然后求解这个01背包问题。这样完全没有改进基本思路的时间复杂度,但这毕竟给了我们将完全背包问题转化为01背包问题的思路:将一种物品拆成多件物品。

//完全背包优化后代码
for(int i=1;i<=n;i++)
{
    for(int j=c[i];j<=V;j--)//顺序循环
    {
        f[j]=max(f[j],f[j-c[i]]+w[i]);
    }
}

循环顺序及原因:为什么01背包中要按照v=V...0逆序来循环。这是因为要保证第i次循环中的状态f[v]是由状态f[v-c]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个没有已经选入第i件物品的子结果f[v-c]。而当前完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[v-c],所以就可以并且必须采用v=0..V的顺序循环。

三、多重背包

问题描述:有N种物品和一个容量为V的背包。第i种物品最多有n件可用,每件体积是c,价值是w。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。

基本思路:这题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略微一改即可,因为对于第i种物品有n+1种策略:取0件,取1件……取 n件。令f[v]表示前i种物品恰放入一个容量为v的背包的最大权值,则:f[v]=max{f[v-k*c]+ k*w|0<=k<=n}。复杂度是O(V*∑n)。另一种好想好写的基该方法是转化为01背包求解:把第i种物品换成n件01背包中的物品,则得到了物品数为∑n的01背包问题,直接求解,复杂度仍然是O(V*∑n)。

//多重背包代码
for(int i=1;i<=m;i++)
{
    cin>>v[i]>>w[i]>>c[i];
    for(int j=0;j<c[i];j++)//枚举第i种物品的数目
    {
        for(int k=n;k>=v[i];k--)
            dp[k]=max(dp[k],dp[k-v[i]]+w[i]);
    }
}

总结

01背包问题是最基本的背包问题,它包含了背包问题中设计状态、方程的最基本思想,另外,别的类型的背包问题往往也可以转换成01背包问题求解。故一定要仔细体会上面基本思路的得出方法,状态转移方程的意义,以及最后怎样优化的空间复杂度。完全背包问题和多重背包问题都是基础的背包问题,有两个状态转移方程,都可以由01背包扩展而来。

推荐文章:dd大牛的《背包九讲》

推荐题目:HDU-2546(01背包)HDU-1114(完全背包)HDU-2191(多重背包)

AC代码:

//HDU-2546(01背包)
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <queue>
#include <stack>
#include <vector>
#include <map>
#include <set>
using namespace std;
#define io ios::sync_with_stdio(0),cin.tie(0)
#define ms(arr) memset(arr,0,sizeof(arr))
#define inf 0x3f3f3f
typedef long long ll;
const int mod=1e9+7;
const int maxn=1e3+7;
int n,v;
int w[maxn];
int dp[maxn];
int main()
{
    io;
    while(cin>>n&&n)
    {
        memset(dp,0,sizeof(dp));
        for(int i=1;i<=n;i++)
            cin>>w[i];
        cin>>v;
        sort(w+1,w+1+n);
        if(v<5)
            cout<<v<<endl;
        else
        {
            for(int i=1;i<n;i++)
                for(int j=v-5;j>=w[i];j--)
                    dp[j]=max(dp[j],dp[j-w[i]]+w[i]);
            cout<<v-dp[v-5]-w[n]<<endl;
        }
    }
    return 0;
}
//HDU-1114(完全背包)
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <queue>
#include <stack>
#include <vector>
#include <map>
#include <set>
using namespace std;
#define io ios::sync_with_stdio(0),cin.tie(0)
#define ms(arr) memset(arr,0,sizeof(arr))
#define inf 0x3f3f3f
typedef long long ll;
const int mod=1e9+7;
const int maxn=1e5+7;
int t;
int e,f,n,p,w;
int dp[maxn];
int main()
{
    io;
    cin>>t;
    while(t--)
    {
        cin>>e>>f;
        dp[0]=0;
        for(int i=1;i<=f;i++)
            dp[i]=inf;
        cin>>n;
        for(int i=1;i<=n;i++)
        {
            cin>>p>>w;
            for(int j=w;j<=f-e;j++)
                dp[j]=min(dp[j],dp[j-w]+p);
        }
        if(dp[f-e]==inf)
            cout<<"This is impossible."<<endl;
        else
            cout<<"The minimum amount of money in the piggy-bank is "<<dp[f-e]<<"."<<endl;
    }
    return 0;
}
//HDU-2191(多重背包)
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <queue>
#include <stack>
#include <vector>
#include <map>
#include <set>
using namespace std;
#define io ios::sync_with_stdio(0),cin.tie(0)
#define ms(arr) memset(arr,0,sizeof(arr))
#define inf 0x3f3f3f
typedef long long ll;
const int mod=1e9+7;
const int maxn=110;
int t,n,m;
int v[110],w[110],c[110];
int dp[110];
int main()
{
    cin>>t;
    while(t--)
    {
        cin>>n>>m;
        ms(dp);
        for(int i=1;i<=m;i++)
        {
            cin>>v[i]>>w[i]>>c[i];
            for(int j=0;j<c[i];j++)
            {
                for(int k=n;k>=v[i];k--)
                    dp[k]=max(dp[k],dp[k-v[i]]+w[i]);
            }
        }
        cout<<dp[n]<<endl;
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/wangws_sb/article/details/83932332