背包九讲系列2——混合背包、二维费用背包、分组背包

4 混合三种背包问题

4.1 问题

如果将前面1、2、3中的三种背包问题混合起来。也就是说,有的物品只可以取一次(01 背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。应该怎么求解呢?

4.2 01 背包与完全背包的混合
考虑到01 背包和完全背包中给出的伪代码只有一处不同,故如果只有两类物品:一类物品只能取一次,另一类物品可以取无限次,那么只需在对每个物品应用转移方程时,根据物品的类别选用顺序或逆序的循环即可,复杂度是O(V N)。伪代码如下:

混合01背包和完全背包的伪代码

4.3 再加上多重背包
如果再加上最多可以取有限次的多重背包式的物品,那么利用单调队列,也可以给出均摊O(V N) 的解法。
但如果不考虑单调队列算法的话,用将每个这类物品分成O(logMi) 个01 背包的物品的方法也已经很优了。
最清晰的写法是调用我们前面给出的三个过程。

三种背包混合的伪代码

在下一章的末尾我会用一到例题来实现这个三种背包混合的伪代码

在最初写出这三个过程的时候,可能完全没有想到它们会在这里混合应用。我想这体现了编程中抽象的威力。如果你一直就是以这种“抽象出过程”的方式写每一类背包问题的,也非常清楚它们的实现中细微的不同,那么在遇到混合三种背包问题的题目时,一定能很快想到上面简洁的解法,对吗?

4.4 小结

有人说,困难的题目都是由简单的题目叠加而来的。这句话是否公理暂且存之不论,但它在本讲中已经得到了充分的体现。本来01 背包、完全背包、多重背包都不是什么难题,但将它们简单地组合起来以后就得到了这样一道一定能吓倒不少人的题目。但只要基础扎实,领会三种基本背包问题的思想,就可以做到把困难的题目拆分成简单的题目来解决。

5 二维费用的背包问题

5.1 问题

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

5.2 算法

费用加了一维,只需状态也加一维即可。设F[i, v, u] 表示前i 件物品付出两种费用分别为v 和u 时可获得的最大价值。状态转移方程就是:

伪代码

如前述优化空间复杂度的方法,可以只使用二维的数组:当每件物品只可以取一次时变量v 和u 采用逆序的循环,当物品有如完全背包问题时采用顺序的循环,当物品有如多重背包问题时拆分物品。这里就不再给出伪代码了,相信有了前面的基础,读者应该能够自己实现出这个问题的程序。

本章末尾会有一道例题实现二维费用背包的这段伪代码

5.3 物品总个数的限制

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

5.4 二维整数域N2 上的背包问题

另一种看待二维背包问题的思路是:将它看待成N2 域上的背包问题。也就是说,背包的容量以及每件物品的费用都是一个二维向量。而常见的一维背包问题则是自然数域上的背包问题。所以说,一维背包的种种思想方法,往往可以应用于二位背包问题的求解中,因为只是数域扩大了而已。作为这种思想的练习,你可以尝试将后文中提到的“子集和问题”扩展到二维,并试图用同样的复杂度解决。

5.5 小结

当发现由熟悉的动态规划题目变形得来的题目时,在原来的状态中加一维以满足新的限制是一种比较通用的方法。希望你能从本讲中初步体会到这种方法。

例题来了,这道例题结合了混合背包和二维费用背包。所以用这一道题把之前说的两部分伪代码做一下实现,那么我们接下来看一看题目以及题目的代码实现部分

题目:

Problem Description
暗黑游戏中,装备直接决定玩家人物的能力。可以使用Pg和Rune购买需要的物品。暗黑市场中的装备,每件有不同的价格(Pg和Rune)、能力值、最大可购买件数。Kid作为暗黑战网的一个玩家,当然希望使用尽可能少的Pg和Rune购买更优的装备,以获得最高的能力值。请你帮忙计算出现有支付能力下的最大可以获得的能力值。
Input
输入有多组数据,每组数据的首行三个整数N(0<N<=150),P(0<P<=100),R(0<R<=100),分别代表市场中物品的种类,Pg的支付能力和Rune的支付能力。第2至N+1行,每行四个整数,前两个整数分别为购买此物品需要花费的Pg和Rune,第三个整数若为0,则说明此物品能购买无数件,若为其他数字,则此物品可购买的最多件数(0<=S<=32),第四个整数为该装备的能力值。
Output
对于每组数据输出一个整数,最大可获得的能力值。
Sample Input
3 10 105 3 0 1104 3 4 1202 3 1 130

Sample Output
370

思路分析:我们可以看到题目中可以使用pg和rune两个支付手段去支付装备,那么这里级可以抽象成二维费用,能力值就是背包的价值。最大可购买件数就是物品的数量。题目中的描述当物品数为0时代表无限件,这里可以抽象成完全背包问题,当物品件数为1时则可以抽象成01背包问题。当物品件数为除了0和1之外的值时变可以抽象成多重背包问题。所以这道题是典型的二维费用背包+混合背包的问题。那么接下来我们看看代码实现

#include <iostream>
using namespace std;

const int N=150;
const int P=5000;

int pg[N+1]; //pg支付费用
int rune[N+1]; //rune支付费用
int dp[P+1][P+1];  //dp[i][j]=k 表示花费pg为i rune为j时的最大价值为k
int s[N+1]; // 件数
int v[N+1]; //能力值


int max(int a,int b)
{
    return a>b?a:b;
}

void ZeroOnePack(int dp[][P+1],int weight_1,int weight_2,int total_1,int total_2,int value)
{
    int j,i;
    for(i=total_1;i>=weight_1;i--)
    {
        for(j=total_2;j>=weight_2;j--)
        {
            dp[i][j]=max(dp[i][j],dp[i-weight_1][j-weight_2]+value);
        }
    }
}


void completePack(int dp[][P+1],int weight_1,int weight_2,int total_1,int total_2,int value)
{
    int j,i;
    for(i=weight_1;i<=total_1;i++)
    {
        for(j=weight_2;j<=total_2;j++)
        {
            dp[i][j]=max(dp[i][j],dp[i-weight_1][j-weight_2]+value);
        }
    }
}


void mutiPack(int dp[][P+1],int weight_1,int weight_2,int total_1,int total_2,int amount,int value)
{
    if(amount*weight_1>total_1&&amount*weight_2>total_2)
    {
        completePack(dp,weight_1,weight_2,total_1,total_2,value);
    }
    else
    {
        int k=1;
        while(amount-k>=0)
        {
            ZeroOnePack(dp,k*weight_1,k*weight_2,total_1,total_2,k*value);
            amount-=k;
            k*=2;
        }
        ZeroOnePack(dp,amount*weight_1,amount*weight_2,total_1,total_2,amount*value);
    }
}


int main()
{
    int n,P,R;
    cin>>n>>P>>R;

    int i;
    
    int p,r,num,val;
    for(i=0;i<n;i++)
    {
        cin>>p>>r>>num>>val;
        pg[i]=p;
        rune[i]=r;
        s[i]=num;
        v[i]=val;
    }
    
    
    for(i=0;i<n;i++)
    {
        if(s[i]==1)//01 背包
        {
            ZeroOnePack(dp,pg[i],rune[i],P,R,v[i]);
        }
        else if(s[i]==0)// 完全背包
        {
            completePack(dp,pg[i],rune[i],P,R,v[i]);
        }
        else //多重背包
        {
            mutiPack(dp,pg[i],rune[i],P,R,s[i],v[i]);
        }
    }

    cout<<dp[P][R]<<endl;

    return 0;
}

6 分组的背包问题

6.1 问题

有N 件物品和一个容量为V 的背包。第i 件物品的费用是Ci,价值是Wi。这些物品被划分为K 组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

6.2 算法

这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设F[k, v] 表示前k 组物品花费费用v 能取得的最大权值,则有:

伪代码

使用一维数组的伪代码如下:

一维数组伪代码

这里三层循环的顺序保证了每一组内的物品最多只有一个会被添加到背包中。
另外,显然可以对每组内的物品应用2.3中的优化。

6.3 小结

分组的背包问题将彼此互斥的若干物品称为一个组,这建立了一个很好的模型。不少背包问题的变形都可以转化为分组的背包问题(例如7),由分组的背包问题进一步可定义“泛化物品”的概念,十分有利于解题。

下面拿了2道和分组背包有关的题目来当例子实践伪代码

第一道: HDU 1712

ACboy needs your help

Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 7535    Accepted Submission(s): 4164


Problem Description
ACboy has N courses this term, and he plans to spend at most M days on study.Of course,the profit he will gain from different course depending on the days he spend on it.How to arrange the M days for the N courses to maximize the profit?


Input
The input consists of multiple data sets. A data set starts with a line containing two positive integers N and M, N is the number of courses, M is the days ACboy has.
Next follow a matrix A[i][j], (1<=i<=N<=100,1<=j<=M<=100).A[i][j] indicates if ACboy spend j days on ith course he will get profit of value A[i][j].
N = 0 and M = 0 ends the input.


Output
For each data set, your program should output a line which contains the number of the max profit ACboy will gain.


Sample Input
2 2
1 2
1 3
2 2
2 1
2 1
2 3
3 2 1
3 2 1
0 0


Sample Output
3
4
6

这道题就是很典型的可以抽象成分组背包的问题,每门课代表一种物品,所有的天数代表背包容量。然后每门课准备上的天数代表在这门课中所有可能的值。一门课就是一个组,并且每组内的物品互相冲突,比如你选择了A课上2天就不能再选择A课上1天。组内互斥,每组最多选一个。下面看看代码实现;

#include<iostream>
using namespace std;

const int N=100;

int a[N+1][N+1]; //a[i][j]=k 代表第i门课上j天所获得的价值为k

int dp[N+1];


//分组背包问题 每组最多拿一个 最典型分组背包
int main()
{
    int n,m;

    cin>>n>>m;

    while(n!=0&&m!=0)
    {
        int i,j;
        for(i=1;i<=n;i++)
        {
            for(j=1;j<=m;j++)
            {
                cin>>a[i][j];
            }
        }

        for(i=1;i<=n;i++)//n门课
        {
            for(j=m;j>=1;j--)//所拥有的天数
            {
                for(int k=1;k<=m;k++)//第i门课 的课程时间 每门课有多个课程时间 存储在二维数组a中
                {
                    if(j-k>=0&&dp[j]<dp[j-k]+a[i][k])
                    {
                        dp[j]=dp[j-k]+a[i][k];
                    }
                }
            }
        }

        cout<<dp[m]<<endl;
        memset(dp,0,sizeof(dp));
        cin>>n>>m;
    }

    return 0;
}

第二道: HDU 3033

I love sneakers!
**Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 5718    Accepted Submission(s): 2344**
Problem Description
After months of hard working, Iserlohn finally wins awesome amount of scholarship. As a great zealot of sneakers, he decides to spend all his money on them in a sneaker store.
There are several brands of sneakers that Iserlohn wants to collect, such as Air Jordan and Nike Pro. And each brand has released various products. For the reason that Iserlohn is definitely a sneaker-mania, he desires to buy at least one product for each brand.
Although the fixed price of each product has been labeled, Iserlohn sets values for each of them based on his own tendency. With handsome but limited money, he wants to maximize the total value of the shoes he is going to buy. Obviously, as a collector, he won’t buy the same product twice.Now, Iserlohn needs you to help him find the best solution of his problem, which means to maximize the total value of the products he can buy.
 
Input
Input contains multiple test cases. Each test case begins with three integers 1<=N<=100 representing the total number of products, 1 <= M<= 10000 the money Iserlohn gets, and 1<=K<=10 representing the sneaker brands. The following N lines each represents a product with three positive integers 1<=a<=k, b and c, 0<=b,c<100000, meaning the brand’s number it belongs, the labeled price, and the value of this product. Process to End Of File.
 
Output
For each test case, print an integer which is the maximum total value of the sneakers that Iserlohn purchases. Print "Impossible" if Iserlohn's demands can’t be satisfied.
 
Sample Input
5 10000 31 4 62 5 73 4 991 55 772 44 66

 
Sample Output
255

这道题也是分组背包问题的代表,只不过和上一道有一点不一样的是,每组内至少拿一件物品。我们来看,一个品牌代表一个组,所拥有的钱代表背包容量,每个品牌里面的鞋子产品代表这个组内的物品成员。

dp[i][j] 代表前i个品牌鞋子花费j元 所获得的最大价值

这道题的初始化可能稍微有点变化,因为如果初始化dp为0则无法区分是买不全那几款鞋子还是能买全但最大价值是0,因为我们在买不全的时候需要输出特殊字符。而在之前的01背包问题等其他问题时,只需要计算最大价值即可,所以为0并不影响结果。

这道题的状态转移方程:

  • dp[i][k]是不选择当前鞋子;
  • dp[i-1][k-v[j]]+w[j]是选择当前鞋子,但是是第一次在本组中选,由于开始将该组dp赋为了-1,所以第一次取时,必须由上一组的结果推知,这样才能保证得到全局最优解;
  • dp[i][k-v[j]]+w[j]表示选择当前鞋子,并且不是第一次在本组中取。

代码实现:

#include <iostream>
using namespace std;

const int N=100;

const int Max_brand=10;
const int Max_money=10000;

int s[N+1];// 鞋子的品牌数组
int v[N+1];// 鞋子的价值数组
int c[N+1];// 鞋子的费用数组

int dp[Max_brand+1][Max_money+1]; //dp[i][j]  代表购买前i组品牌鞋子花费j元所得到的最大家价值 一个品牌的鞋子算一个分组



int max(int a,int b,int c)
{
    return a>b?(a>c?a:c):(b>c?b:c);
}

//分组背包问题  每组至少取一个

/*
dp[i][k]是不选择当前鞋子;
dp[i-1][k-v[j]]+w[j]是选择当前鞋子,但是是第一次在本组中选,由于开始将该组dp赋为了-1,所以第一次取时,必须由上一组的结果推知,这样才能保证得到全局最优解;
dp[i][k-v[j]]+w[j]表示选择当前鞋子,并且不是第一次在本组中取。
*/


/*

这道题dp初始化为-1 因为如果初始化为0 
则无法区分是买不全那几款鞋子还是能买全但最大价值是0
因为我们在买不全的时候需要输出特殊字符。
*/

int main()
{
    int n,m,S;

    while(scanf("%d%d%d",&n,&m,&S)!=EOF)
    {

        int i,j,si,vi,ci;

        for(i=0;i<n;i++)
        {
            cin>>si>>ci>>vi;
            s[i]=si;
            c[i]=ci;
            v[i]=vi;
        }

        for(i=0;i<=S;i++)
        {
            for(j=0;j<=m;j++)
            {
                if(i==0)
                {
                    dp[i][j]=0;
                }
                else
                {
                    dp[i][j]=-1;
                }
            }
        }

        for(i=1;i<=S;i++)
        {
            for(j=0;j<=n;j++)//遍历所有鞋子 
            {
                if(s[j]==i)//找到品牌为i的鞋子
                {
                    for(int k=m;k>=c[j];k--)//第i组内选择
                    {
                        dp[i][k]=max(dp[i][k],dp[i][k-c[j]]+v[j],dp[i-1][k-c[j]]+v[j]);
                    }
                }
            }
        }


        if(dp[S][m]<0)
        {
            cout<<"Impossible"<<endl;
        }
        else
        {
            cout<<dp[S][m]<<endl;
        }
    }

    return 0;
}

推荐阅读更多精彩内容