背包DP问题(01背包+完全背包+分组背包+多重背包+混合背包+二维费用的背包)

背包DP问题

01背包问题

问题:
有N件物品和一个容量为V的背包。第i件物品的费用(即体积,下同)是w[i],价值是c[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。 基本思路:
  这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
  用子问题定义状态:即f[i][v]表示前i件物品(部分或全部)恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:f[i][v]=max{f[i-1][v],f[i-1][v-w[i]]+c[i]}。
  这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下:“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”;如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-w[i]的背包中”,此时能获得的最大价值就是f [i-1][v-w[i]]再加上通过放入第i件物品获得的价值c[i]。
  
问题的具体描述上面已经说的很清楚了,不难发现在是否拿第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[i][v]时(也即在第i次主循环中推f[v]时)能够得到f[i-1][v]和f[i-1][v-w[i]]的值呢?事实上,这要求在每次主循环中我们以v=V…0的逆序推f[v],这样才能保证推f[v]时f[v-w[i]]保存的是状态f[i-1][v-w[i]]的值。
伪代码如下:
  for i=1…N
   for v=V…0
     f[v]=max{f[v],f[v-w[i]]+c[i]};

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

代码:

#include 

using namespace std;

const int maxn = 1e5+5;
int w[maxn],c[maxn];	//w,c分别表示物品的质量和价值
int dp[maxn];			//动态方程

int main()
{
    int m,n;
    cin >> m >> n;				  //m为背包的最大容量,n为
    for(int i = 0 ; i < n ; i++) //读入数据不多赘述
        cin >> w[i] >> c[i];
    for(int i = 0 ; i < n ; i++)
    {
        for(int j = m ; j >= w[i] ; j--)//内层循环从m到w[i],要保证背包可以放的下第i件物品
            dp[j] = max(dp[j],dp[j - w[i]] + c[i]); //动态方程
    }
    cout << dp[m];
    return 0;
}

完全背包问题

问题:
  有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是w[i],价值是c[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
基本思路:
  这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很多种。如果仍然按照解01背包时的思路,令f[i][v]表示前i种物品恰放入一个容量为v的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:f[i][v]=max{f[i-1][v-kw[i]]+kc[i]|0<=k*w[i]<= v}。
  将01背包问题的基本思路加以改进,得到了这样一个清晰的方法。这说明01背包问题的方程的确是很重要,可以推及其它类型的背包问题。
  这个算法使用一维数组,先看伪代码:
  for i=1…N
   for v=0…V
     f[v]=max{f[v],f[v-w[i]]+c[i]};
你会发现,这个伪代码与01背包问题的伪代码只有v的循环次序不同而已。为什么这样一改就可行呢?首先想想为什么01背包问题中要按照v=V…0的逆序来循环。这是因为要保证第i次循环中的状态f[i][v]是由状态f[i-1][v-w[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果f[i-1][v-w[i]]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[i][v-w[i]],所以就可以并且必须采用v= 0…V的顺序循环。这就是这个简单的程序为何成立的道理。
   这个算法也可以以另外的思路得出。例如,基本思路中的状态转移方程可以等价地变形成这种形式:f[i][v]=max{f[i-1][v],f[i][v-w[i]]+c[i]},将这个方程用一维数组实现,便得到了上面的伪代码。

代码:

#include 

using namespace std;

const int maxn = 1e5+5;
int w[maxn],c[maxn]; //w,c分别表示质量和价值
int dp[maxn];		//动态方程

int main()
{
    int m,n;
    cin >> m >> n; // m表示背包总质量,n表示有n件物品
    for(int i = 0 ; i < n ; i++)
        cin >> w[i] >> c[i];
    for(int i = 0 ; i < n ; i++)
    {
        for(int j = w[i] ; j <= m ; j++)		//这里注意是从w[i]到m,具体原因上面已经讲过
            dp[j] = max(dp[j],dp[j - w[i]] + c[i]); //动态方程
    }
    cout << dp[m];
    return 0;
}

多重背包问题

问题:
  有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是w[i],价值是c[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
这其实是一个简单的01背包拓展问题,01背包是每种物品最多只能取一次,而多重背包是每种物品可以最多取k次,原理和01背包一样,只需要多加一层for循环用来循环每件物品的个数即可

代码:

#include 

using namespace std;

const int maxn = 1e5+5;
int w[maxn],c[maxn],p[maxn]; //w,c,p分别表示每件物品的质量,价值,数量
int dp[maxn];//动态方程

int main()
{
    int m,n;
    cin >> n >> m;
    for(int i = 0 ; i < n ; i++)
        cin >> w[i] >> c[i] >> p[i];
    for(int i = 0 ; i < n ; i++)
    {
        for(int j = m ; j >= 0 ; j--)
        {
            for(int k = 0 ; k <= p[i] ; k++) //循环每件物品的件数
            {
                if(k*w[i] > j)	//如果放入k件第i件物品的质量超过了容量为j的背包,说明无法放入,直接跳出循环
                    break;
                dp[j] = max(dp[j],dp[j - k*w[i]] + k*c[i]); //动态方程
            }
        }
    }
    cout << dp[m];
    return 0;
}

混合背包问题

问题:
  如果将01背包、完全背包、多重背包混合起来。也就是说,有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。应该怎么求解呢?
01背包与完全背包的混合
  考虑到在01背包和完全背包中最后给出的伪代码只有一处不同,故如果只有两类物品:一类物品只能取一次,另一类物品可以取无限次,那么只需在对每个物品应用转移方程时,根据物品的类别选用顺序或逆序的循环即可,复杂度是O(VN)。
  
伪代码如下:
for i=1…N
 if 第i件物品是01背包
  for v=V…0
   f[v]=max{f[v],f[v-w[i]]+c[i]};
 else if 第i件物品是完全背包
  for v=0…V
   f[v]=max{f[v],f[v-w[i]]+c[i]};

代码:

#include 

using namespace std;

const int maxn = 1e5+5;
int w[maxn],c[maxn],p[maxn]; //w,c,p表示每件物品的质量,价值以及数量
int dp[maxn];

int main()
{
    int m,n;
    cin >> m >> n;
    for(int i = 0 ; i < n ; i++)
        cin >> w[i] >> c[i] >> p[i];
    for(int i = 0 ; i < n ; i++)
    {
        if(p[i]) //数量不为0是为01背包
        {
            for(int j = m ; j >= 0 ; j--)
            {
                for(int k = 0 ; k <= p[i] ; k++)
                {
                    if(k*w[i] > j)
                        break;
                    dp[j] = max(dp[j],dp[j - k*w[i]] + k*c[i]);
                }
            }
        }
        else //数量为0时则为完全背包
        {
            for(int j = w[i] ; j <= m ; j++)
                dp[j] = max(dp[j],dp[j - w[i]] + c[i]);
        }
    }
    cout << dp[m];
    return 0;
}

二维费用的背包问题

问题:
  二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为a[i]和b[i]。两种代价可付出的最大值(两种背包容量)分别为V和U。物品的价值为c[i]。

算法:
  费用加了一维,只需状态也加一维即可。设f[i][v][u]表示前i件物品付出两种代价分别为v和u时可获得的最大价值。
  状态转移方程就是:f [i][v][u]=max{f[i-1][v][u],f[i-1][v-a[i]][u-b[i]]+c[i]}。如前述方法,可以只使用二维的数组:当每件物品只可以取一次时变量v和u采用逆序的循环,当物品有如完全背包问题时采用顺序的循环。当物品有如多重背包问题时拆分物品。

物品总个数的限制 :
  有时,“二维费用”的条件是以这样一种隐含的方式给出的:最多只能取M件物品。这事实上相当于每件物品多了一种“件数”的费用,每个物品的件数费用均为1,可以付出的最大件数费用为M。换句话说,设f[v][m]表示付出费用v、最多选m件时可得到的最大价值,则根据物品的类型(01、完全、多重)用不同的方法循环更新,最后在f[0…V][0…M]范围内寻找答案。
另外,如果要求“恰取M件物品”,则在f[0…V][M]范围内寻找答案。

例题:

潜水员(diver)

Description

潜水员为了潜水要使用特殊的装备。他有一个带2种气体的气缸:一个为氧气,一个为氮气。让潜水员下潜的深度需要各种的数量的氧和氮。潜水员有一定数量的气缸。每个气缸都有重量和气体容量。潜水员为了完成他的工作需要特定数量的氧和氮。他完成工作所需气缸的总重的最低限度的是多少?

例如:潜水员有5个气缸。每行三个数字为:氧,氮的(升)量和气缸的重量:

3 36 120

10 25 129

5 50 250

1 45 130

4 20 119

如果潜水员需要5升的氧和60升的氮则总重最小为249(1,2或者4,5号气缸)。

你的任务就是计算潜水员为了完成他的工作需要的气缸的重量的最低值。

Input

第一行有2整数m,n(1<=m<=21,1<=n<=79)。它们表示氧,氮各自需要的量。

第二行为整数k(1<=n<=1000)表示气缸的个数。

此后的k行,每行包括ai,bi,ci(1<=ai<=21,1<=bi<=79,1<=ci<=800)3整数。这些各自是:第i个气缸里的氧和氮的容量及汽缸重量。

Output

仅一行包含一个整数,为潜水员完成工作所需的气缸的重量总和的最低值。

Sample Input 1

5 5
9
1 1 12
3 1 52
1 3 71
2 1 33
3 2 86
2 3 91
2 2 43
3 3 113
1 2 28

Sample Output 1

104

代码:

#include 

using namespace std;

const int maxn = 1e3+5;
int w1[maxn],w2[maxn],c[maxn]; //w1,w2表示两个费用,c表示价值
int dp[maxn][maxn];

int main()
{
    memset(dp,127,sizeof(dp)); // 初始化数据
    dp[0][0] = 0;			//为了后面求最小花费,把dp[0][0]初始化为0
    int m,n,k;
    cin >> n >> m >> k;
    for(int i = 0 ; i < k ; i++)
        cin >> w1[i] >> w2[i] >> c[i];
    for(int i = 0 ; i < k ; i++)
    {
        for(int j = n ; j >= 0 ; j--)
        {
            for(int v = m ; v >= 0 ; v--)
            {
                int t1 = j + w1[i];
                int t2 = v + w2[i];
                if(t1 > n)	//判断是否超过总质量
                    t1 = n;
                if(t2 > m)	//判断是否超过总质量
                    t2 = m;
                dp[t1][t2] = min(dp[t1][t2],dp[j][v] + c[i]);
            }
        }
    }
    cout << dp[n][m];
    return 0;
}

分组背包问题:

问题:
  有N件物品和一个容量为V的背包。第i件物品的费用是w[i],价值是c[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
算法:
  这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设f[k][v]表示前k组物品花费费用v能取得的最大权值,则有f[k][v]=max{f[k-1][v],f[k-1][v-w[i]]+c[i]|物品i属于第k组}。
使用一维数组的伪代码如下:
for 所有的组k
  for v=V…0
    for 所有的i属于组k
      f[v]=max{f[v],f[v-w[i]]+c[i]}
  注意这里的三层循环的顺序,“for v=V…0”这一层循环必须在“for 所有的i属于组k”之外。这样才能保证每一组内的物品最多只有一个会被添加到背包中。

例题:

分组背包(group)

Description

一个旅行者有一个最多能用V公斤的背包,现在有n件物品,它们的重量分别是W1,W2,…,Wn,它们的价值分别为C1,C2,…,Cn。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

Input

第一行:三个整数,V(背包容量,V<=200),N(物品数量,N<=30)和T(最大组号,T<=10);

第2…N+1行:每行三个整数Wi,Ci,P,表示每个物品的重量,价值,所属组号。

Output

仅一行,一个数,表示最大总价值。

Sample Input 1

10 6 3
2 1 1
3 3 1
4 8 2
6 9 2
2 8 3
3 9 3

Sample Output 1

20

代码:

#include 

using namespace std;

const int maxn = 1e3+5;
int w[maxn],c[maxn],a[maxn][maxn]; //w表示每件物品的质量,c表示每件物品的价格
//a用来存储每件物品所属组数,a[k][0]表示第k组中物品的总数,a[k][j]表示第a[k][j]件物品
int dp[maxn];

int main()
{
    int v,n,t;
    cin >> v >> n >> t;
    for(int i = 1 ; i <= n ; i++)
    {
        int p;
        cin >> w[i] >> c[i] >> p;
        a[p][++a[p][0]] = i; 	//存储每组的物品,这里上面已经介绍过了
    }
    for(int i = 1 ; i <= t ; i++)
    {
        for(int j = v ; j >= 0 ; j--)
        {
            for(int k = 1 ; k <= a[i][0] ; k++)
            {
                int tmp = a[i][k];
                if(j < w[tmp])
                    break;
                dp[j] = max(dp[j],dp[j-w[tmp]] + c[tmp]);
            }
        }
    }
    cout << dp[v];
    return 0;
}

总结:
背包问题实际上就是一个动态规划的过程,了解每种背包的动态方程,理解过程,根据题目的要求做出变化,动态规划的思想十分重要!

你可能感兴趣的:(ACM,算法详解,ACM,算法,背包问题)