背包问题-背包九讲阅读笔记

背包问题

0-1背包

这是最简单的背包问题,简而言之就是每件物品只有一样,可以取或者不取,对于容量为V的背包,N件物品,每一件都可以尝试放入背包中,那么顺序就是遍历这N件物品,每遍历到一个新的物品,都尝试将当前物品放入背包中,看看是放入后得到的价值高还是不放的价值高,取最高者即可。

所以需要一个二维数组w[N][V],这个二维数组中的每一个位置w[i][j]代表的意思是前i个物品在容量为j的时候可以获取的最大价值是多少。那么选当前物品(重量W,价值Va)得到的价值就是w[i - 1][j - W] + Va,不选当前物品的价值为w[i - 1][j],选最大值就行。

状态转移方程为:

w [ i ] [ j ] = m a x ( w [ i 1 ] [ j W ] + V a , w [ i 1 ] [ j ] )

题目:编号a,b,c,d,e的5件物品,重量分别是2,2,6,5,4,价值分别是6,3,5,4,6,现在背包承重为10,如何使装入背包的物品具有最大值?

如果采用状态转移方程,我们可以知道二维数组的值为:

0 1 2 3 4 5 6 7 8 9 10
a 0 0 6 6 6 6 6 6 6 6
b 0 0 6 6 9 9 9 9 9 9
c 0 0 6 6 9 9 9 9 10 13
d 0 0 6 6 9 9 9 10 11 13
e 0 0 6 6 9 9 12 12 15 15

代码如下:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
//二维数组求0-1背包
int main()
{
    int n, m;   //n:物品个数,m:背包容量
    while (cin >> n >> m) {
        vector<pair<int, int>> vw(n + 1);   //价值+重量
        for (int i = 1; i <= n; ++i)
            cin >> vw[i].first >> vw[i].second;
        vector<vector<int>> ret(n + 1, vector<int>(m + 1, 0));  //计算数组,选择或者不选择
        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= m; ++j) {
                if (vw[i].second > j) { //背包容量还不够装当前的东西
                    ret[i][j] = ret[i - 1][j];  //那就只能放可以放的(就是上一行,不包括当前物品的情况)
                }
                else {  //可以装当前物品,就看看在没有当前物品的前提下(就是参考上一行),装和不装哪个更好
                    ret[i][j] = max(ret[i - 1][j], ret[i - 1][j - vw[i].second] + vw[i].first);
                }
            }
        }
        cout << ret[n][m] << endl;
    }

    return 0;
}

这里可以优化到一维数组,这样可以减少空间复杂度,具体方法是将第二层循环颠倒,从容量为满到空进行反向计算,这样每次循环的时候,面对的背包都是不包含当前物品的背包,使用数值进行计算即可。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
    int n, m;   //n:物品个数,m:背包容量
    while (cin >> n >> m) {
        vector<pair<int, int>> vw(n + 1);   //价值+重量
        for (int i = 1; i <= n; ++i)
            cin >> vw[i].first >> vw[i].second;
        vector<int> ret(m + 1, 0);  //一维数组,反向求0-1背包
        for (int i = 1; i <= n; ++i) {
            for (int j = m; j >= 1; --j) {
                if(vw[i].second <= j)
                ret[j] = max(ret[j], ret[j - vw[i].second] + vw[i].first);  //去掉当前物品的重量+当前物品的价值和原价值相比
            }
        }
        cout << ret[m] << endl;
    }
    return 0;
}

完全背包

完全背包问题和0-1背包问题不同的地方在于,完全背包的每件物品都是无穷多件,这样我们就不用像0-1背包一样,每次都选择不包括当前物品的背包来尝试添加当前物品(因为每件物品只能添加一次),而是可以在包括当前物品的背包中,选择继续添加当前物品。

所以需要一个一维数组w[N],这个数组中的每一个位置w[i]代表的意思是在容量为j的时候可以获取的最大价值是多少。那么添加当前物品(第一次或是多次)(重量W,价值Va)得到的价值就是w[i - W] + Va,不添加当前物品的价值为w[i],选最大值就行。

状态转移方程为:

扫描二维码关注公众号,回复: 2862685 查看本文章
w [ i ] [ j ] = m a x ( w [ i ] [ j W ] + V a , w [ i ] [ j ] )

那么可以用一维数组来存储背包的容量产生的最大价值,设数组为f[m],那么f[i]代表i容量的背包可以产生的最大价值。

牛客网-牛牛炒股票

题目:牛牛得知了一些股票今天买入的价格和明天卖出的价格,他找犇犇老师借了一笔钱,现在他想知道他最多能赚多少钱。

输入描述:每个输入包含一个测试用例。 输入的第一行包括两个正整数,表示股票的种数N (1<=N<=1000)(1<=N<=1000)和牛牛借的钱数M (1<=M<=1000)(1<=M<=1000)。 接下来N行,每行包含两个正整数,表示这只股票每一股的买入价X (1<=X<=1000)(1<=X<=1000) 和卖出价Y (1<=Y<=2000)(1<=Y<=2000)。 每只股票可以买入多股,但必须是整数。

输出描述:输出一个整数表示牛牛最多能赚的钱数。

样例:

in:
3 5 
3 6 
2 3 
1 1

out:
4

这是一个很明显的完全背包问题,从股票的描述来看,买入就是重量,卖出就是价值,每一个股票都是无限量,可以一直买。

先不考虑借的钱的本钱,最后算出来再减去就可以,所以价值最小值就是没有买股票(此初始值可以筛选掉赔钱的股票,求max值会自动筛选赔钱股票),整个数组的初始化值为借来的钱数(即第i列的值就为i),而数组f[i]的值代表的意思就是借来i元可以产生的最大收益。

递推数组如下:

当只有股票(3,6)的时候:

1 2 3 4 5
(3,5) 1 2 6 7

增加股票(2,3)时:

1 2 3 4 5
(3,5) 1 3 6 7

增加股票(1,1)时:

1 2 3 4 5
(3,5) 1 3 6 7

所以最后的价值就是9。代码如下(已AC):

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
    int n, m;
    cin >> n >> m;
    vector<pair<int, int>> bag(n + 1, make_pair(0, 0));
    for(int i = 1; i <= n; ++i){
        int in, out;
        cin >> in >> out;
        bag[i].first = in;
        bag[i].second = out;
    }
    vector<int> money(m + 1, 0);
    for(int i = 1; i <= m; ++i)
        money[i] = i;
    for(int i = 1; i <= n; ++i){
        for(int j = bag[i].first; j <= m; ++j){
            money[j] = max(money[j], money[j - bag[i].first] + bag[i].second);
        }
    }
    cout << money[m] - m << endl;

    return 0;
}

多重背包

多重背包就是每种物品的量都是固定的,取完即止,求解将哪些物品放入背包可以使得物品的费用总和超过背包,且总价值最大。可以使用0-1背包的思路来解,就是把每类物品(假设n个)看作不同的n个物品,逐个取用即可,这里的时间复杂度为(m种物品,每种物品都有n[i]个)

O ( n n [ i ] )

题目来源:HDU2192

题目:为了挽救灾区同胞的生命,心系灾区同胞的你准备自己采购一些粮食支援灾区,现在假设你一共有资金n元,而市场有m种大米,每种大米都是袋装产品,其价格不等,并且只能整袋购买。 请问:你用有限的资金最多能采购多少公斤粮食呢?

输入:输入数据首先包含一个正整数C,表示有C组测试用例,每组测试用例的第一行是两个整数n和m(1<=n<=100, 1<=m<=100),分别表示经费的金额和大米的种类,然后是m行数据,每行包含3个数p,h和c(1<=p<=20,1<=h<=200,1<=c<=20),分别表示每袋的价格、每袋的重量以及对应种类大米的袋数。

输出:对于每组测试数据,请输出能够购买大米的最多重量,你可以假设经费买不光所有的大米,并且经费你可以不用完。每个实例的输出占一行。

输入样例:

1 8 2

2 100 4

4 100 2

输出样例:

400

使用一维数组来解答的话的话,设数组为w[n],这样每次都是选择加入当前物品和不加入当前物品,和0-1背包相同的是,使用一维数组需要反向遍历,使得每次计算的数组都是不包含当前物品的数组。状态转移方程为:

w [ i ] = m a x ( w [ i W ] + V a , w [ i ] )

对于这道题,既可以把样例分为6(=4+2)个物品,采用0-1背包的方法来解。

代码如下(已AC):

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
//多重背包问题
struct rice {
    int w, v, n;
};
int main()
{
    int c;
    cin >> c;
    while (c--) {
        int n, m;
        cin >> n >> m;
        vector<rice> v(m + 1);  //大米数组
        for (int i = 1; i <= m; ++i)
            cin >> v[i].w >> v[i].v >> v[i].n;
        vector<int> ans(n + 1, 0);
        for (int i = 1; i <= m; ++i) {  //遍历大米种类
            for (int j = 1; j <= v[i].n; ++j) { //遍历每种个数(当作0-1来做)
                for (int k = n; k >= v[i].w; --k) { //往背包里面添加
                    ans[k] = max(ans[k], ans[k - v[i].w] + v[i].v);
                }
            }
        }
        cout << ans[n] << endl;
    }

    return 0;
}

对于上面的方法,时间复杂度较高,有一种减少时间复杂度的方法,就是采用二进制的思想。当输入一个物品的重量,价值,个数的时候,假设个数是n,上述的解决方法会产生n个物品,而采用二进制思想的时候,把物品分为2^0,2^1,……,2^k,n-2^(k+1)-1。假设说有物品20件,那么传入参数若是w,v,n,那么将会分为w,v,1;2w,2v,1,……,(2^k)w,(n-2^(k+1)-1)v,1;(n-2^(k+1)-1)w,(n-2^(k+1)-1)v,1。很明显这样的二进制可以组成任意数量,和一个一个组合是相同的。这种方法的时间复杂度是

O ( n l o g ( n [ i ] ) )

具体代码如下(已AC):

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
//多重背包问题
struct rice {
    int w, v;
    rice(int _w, int _v) : w(_w), v(_v){ }
};
int main()
{
    int c;
    cin >> c;
    while (c--) {
        int n, m;
        cin >> n >> m;
        vector<rice> vec;   //大米数组
        for (int i = 1; i <= m; ++i) {
            int w, v, n;
            cin >> w >> v >> n;
            for (int i = 1; ; i *= 2) { //加入背包
                if (n >= i) {   //现在数量还多于要装入背包的
                    vec.push_back(rice(i * w, i * v));
                    n -= i; //去掉已经加入背包的数量
                }
                else {  //数量不够了,就把剩下的放进去然后退出
                    vec.push_back(rice(n * w, n * v));
                    break;
                }
            }
        }
        vector<int> ans(n + 1, 0);
        for (int i = 0; i < vec.size(); ++i) {  //遍历大米种类
            for (int k = n; k >= vec[i].w; --k) {   //往背包里面添加
                ans[k] = max(ans[k], ans[k - vec[i].w] + vec[i].v);
            }
        }
        cout << ans[n] << endl;
    }

    return 0;
}

混合背包问题

如果将01背包、完全背包、多重背包混合起来。也就是说,有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。 就是混合背包问题。

这个问题其实跟01背包、完全背包、多重背包同源,可以使用同一种解决办法,只要在解决问题的时候牢牢记住,无限量的物品实际上等同于背包总容量与单间物品的重量相处所得的件数(最大使用量),因为即使物品数量再多也不可能使用到超过最大使用量的部分,又考虑到件数为1的情况是有限量中的特殊情况,所以,这个问题可以转换为一个多重背包问题,进而又可以通过多重背包问题转换成01背包问题求解。

题目来源:POJ1742-coins(楼教主男人八题之一,难度有点大)

题目:People in Silverland use coins.They have coins of value A1,A2,A3…An Silverland dollar.One day Tony opened his money-box and found there were some coins.He decided to buy a very nice watch in a nearby shop. He wanted to pay the exact price(without change) and he known the price would not more than m.But he didn’t know the exact price of the watch. You are to write a program which reads n,m,A1,A2,A3…An and C1,C2,C3…Cn corresponding to the number of Tony’s coins of value A1,A2,A3…An then calculate how many prices(form 1 to m) Tony can pay use these coins.

输入:The input contains several test cases. The first line of each test case contains two integers n(1<=n<=100),m(m<=100000).The second line contains 2n integers, denoting A1,A2,A3…An,C1,C2,C3…Cn (1<=Ai<=100000,1<=Ci<=1000). The last test case is followed by two zeros.

输出:For each test case output the answer on a single line.

输入样例:

3 10
1 2 4 2 1 1
2 5
1 4 2 1
0 0

输出样例:

8
4

题目的大意是一个人有不同面值的硬币。需要凑出从1到m价格中的面值,问最多能凑出多少种不同的类型。例如样例2 5 1 4 2 1 中,价值是10,有2种硬币,分别是1(2个),4(1个),5可以看作是背包容量,1,4就是重量了,这题简而言之就是在对应的背包容量下,现有的东西,能装多少个,可以组成什么容量。但是由于这题没有价值,所以对于数组a[i],表示a[i]可以被组成。也就是说,对于整个数组,只要能被组成的,最后予以计数就可以。

这题有点偏向多重背包,但是如果出现了钱币的价格*数量>要凑的价格上限的话,就相当于完全背包了(这也侧面提醒我们,当掺杂了完全背包的时候,直接用背包上限/重量+1来表示数量就行),0-1背包没什么好讲的,就是数量为1的多重背包。这样,对于不同的背包问题,使用不同的方法来解决就可以。

用表格表示就是:

0 1 2 3 4 5
1, 1 true true false false false
1, 1 true true true false false
4, 1 true true true false true

遍历钱币的时候,首先是1,很明显是0-1背包,对应0-1背包的处理策略,反向一维数组处理,由于这里不涉及到价值最大,只涉及到能不能组成,所以不需要表示数字,只表示能不能组成就可以(初始化的时候数组为0的置为true,因为肯定能组成0钱)。这样,1输入后,可以组成大小为1的钱,再输入1,下标为2的部分置为true,最后输入4,会将相应部分置为true,最后遍历数组为true的数量即可。

这题的难度在于,时间空间的限制比较死,所以按照一般的思路来,不是TLE就是MLE,所以要做两个优化,1是二维数组优化为一维数组减少内存,二是多重背包转0-1背包的时候要指数减少,不能线性减少。

最后吐槽一下,STL是过不了的,只能按照预先分配内存的方式来做,对我而言比较不爽。

代码如下(已AC):

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int n, m;
int coin[110];  //钱币+数量
int nums[110];  //数量
bool price[100010]; //一维数组,当成0-1背包来做

void bp01(int w)
{
    for (int i = m; i >= w; --i)    //0-1背包反向做
        price[i] |= price[i - w];   //如果之前钱k可以组成的话,那么加入w后,可以组成k+w钱
}
void bpcomplate(int w)
{
    for (int i = w; i <= m; ++i)
        price[i] |= price[i - w];
}
void bpmulti(int w, int c)
{
    /*将多重背包转化成0-1背包,这样三种背包混合问题就转换成0-1
    背包和完全背包的组合,再针对不同的背包问题,使用不同的策略就行*/
    if (w * c >= m) //完全背包
        bpcomplate(w);
    else{
        int i = 1;  //优化多重背包转0-1背包(若有n件物品,转化为log(n)件0-1物品)
        while (i < c) {
            c -= i;
            bp01(i * w);
            i <<= 1;
        }
        bp01(c * w);    //最后多出来的
    }
}
int main()
{
    price[0] = true;
    while (cin >> n >> m) {
        if (n == 0 && m == 0) break;
        for (int i = 1; i <= n; ++i)
            cin >> coin[i];
        for (int i = 1; i <= n; ++i)
            cin >> nums[i];
        for (int i = 1; i <= n; ++i)    //遍历背包
            if(nums[i]) bpmulti(coin[i], nums[i]);
        int cnt = 0;
        for (int i = 1; i <= m; ++i)
            if (price[i]) cnt++;
        memset(price + 1, 0, sizeof(bool) * m);
        cout << cnt << endl;
    }

    return 0;
}

二维费用背包问题

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

费用加了一维,只需状态也加一维即可。所以理论上用三维数组来解,当然肯定是可以优化的。设f[i][v][u]表示前i件物品付出两种代价分别为v和u时可获得的最大价值。状态转移方程就是:

f [ i ] [ v ] [ u ] = m a x f [ i 1 ] [ v ] [ u ] , f [ i 1 ] [ v a [ i ] ] [ u b [ i ] ] + w [ i ]

如前述方法,优化后可以只使用二维的数组: 当每件物品只可以取一次时变量v和u采用逆序的循环,当物品有如完全背包问题时采用顺序的循环。当物品有如多重背包问题时拆分物品。

题目来源:HDU2159

题目:最近xhd正在玩一款叫做FATE的游戏,为了得到极品装备,xhd在不停的杀怪做任务。久而久之xhd开始对杀怪产生的厌恶感,但又不得不通过杀怪来升完这最后一级。现在的问题是,xhd升掉最后一级还需n的经验值,xhd还留有m的忍耐度,每杀一个怪xhd会得到相应的经验,并减掉相应的忍耐度。当忍耐度降到0或者0以下时,xhd就不会玩这游戏。xhd还说了他最多只杀s只怪。请问他能升掉这最后一级吗?

输入:输入数据有多组,对于每组数据第一行输入n,m,k,s(0 < n,m,k,s < 100)四个正整数。分别表示还需的经验值,保留的忍耐度,怪的种数和最多的杀怪数。接下来输入k行数据。每行数据输入两个正整数a,b(0 < a,b < 20);分别表示杀掉一只这种怪xhd会得到的经验值和会减掉的忍耐度。(每种怪都有无数个)

输出:输出升完这级还能保留的最大忍耐度,如果无法升完这级输出-1。

输入样例:

10 10 1 10
1 1
10 10 1 9
1 1
9 10 2 10
1 1
2 2

输出样例:

0
-1
1

有时候,二维背包的第二维限制是对总物品的数量有限制,也就是说,在之前0-1背包或者完全背包上加一个条件:“只允许总共拿X件物品”,那么那些题目就变成了二维背包。

本题就是这样,阅读题目可以发现,这里要求在不超过忍耐度的情况下可以获取超过某个数量的经验,如果无法获取超过这个数量的经验,就返回-1,这里的背包容量就是忍耐度,重量(代价)就是每个怪消耗的忍耐度,价值就是每个怪得到的经验。同时要求杀怪数不超过一个限制,所以杀怪数就是一个背包,怪的数量则是重量(代价),得到的经验就是价值。

P.s. 这里有一个隐含的条件,条件就是杀怪数和忍耐度之间是有联系的,在杀怪数量为i的时候,消耗的忍耐度是i*每只怪的代价。

说到这里,形式已经很明朗了,只需要按照背包的思想,将不选择本物品和选择本物品的价值取最大值即可。设二维数组l[i][j]代表杀i只怪并消耗j点忍耐度的情况下获得的经验值。状态转移公式为:

l [ i ] [ j ] = m a x ( l [ i ] [ j ] , l [ i 1 ] [ j m o n s t e r . h a t e ] + m o n s t e r . e x p )

代码如下(已AC):

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
struct mon
{
    int exp, hate;  //经验+忍耐度
};
int main()
{
    int n, m, k, s;
    while (cin >> n >> m >> k >> s) {
        vector<mon> monster(k); //经验+忍耐度
        for (int i = 0; i < k; ++i)
            cin >> monster[i].exp >> monster[i].hate;
        //二维数组,levelup[i][j]表示杀掉i只怪物,使用了j点忍耐度并可以得到的经验值
        vector<vector<int>> levelup(s + 1, vector<int>(m + 1, 0));  //行:怪物数量,列:忍耐度
        int left = m + 1;   //初始要用掉的忍耐度(没变的话m-left得出的结果肯定是-1)
        for (int idx = 0; idx < monster.size(); ++idx) {    //遍历背包
            for (int i = 1; i <= s; ++i) {  //遍历二维数组(完全背包顺序遍历)
                for (int j = monster[idx].hate; j <= i * monster[idx].hate && j <= m; ++j) {    //每杀1只怪物要使用x点忍耐度
                    levelup[i][j] = max(levelup[i][j], levelup[i - 1][j - monster[idx].hate] + monster[idx].exp);
                    if (levelup[i][j] >= n) {   //得到了需要的经验值
                        left = min(left, j);
                    }
                }
            }
        }
        cout << m - left << endl;
    }
    return 0;
}

分组背包问题

分组背包较普通的背包问题,多加了一个不同组的条件,也就是说,同一组只能有一个物品被选中。具体来说就是一个容量为V的背包,还有若干组物品,每组包含若干物品,这些物品各不相同,而且体积w和价值p各不相同。组内的物品相冲突。求出能在不超过V的情况下尽可能的使价值最大。

虽然分成很多组,但只能选一个,或者不选,这就是0-1背包,但是多了一个额外的限制。如何取解决这个限制呢,从遍历的顺序取思考即可,因为如果再按照之前遍历背包,然后遍历容量的话,无法保证每组只选一个。这里就要首先遍历背包的各组,在每组中选择可产生价值最大的物品,但是不能按照贪心来做,因为贪心要求自问题最优,而这里最终的最优解并不是子问题的最优解,我们需要得到并保存总的问题中每一个子问题的最优解,也就是说需要得到背包容量为k的时候,从容量为1到容量为k所有问题的最优解。所以这里按照0-1背包的思路,反向遍历一维数组,对于每组的物品来说,都只会遍历一次容量数组,也就是说,要在遍历容量数组的内部,遍历组内的物品,这样对于每一个可能的容量来说,都是遍历了整个物品数量的最优解了。

也就是说,对于每个组,遍历背包容量;对于每个子问题容量,每个物品面临的都是空包加上自己的价值,只是会选择组内价值最大的那一个。(三层for循环)

仔细分析一下,问题就很清晰了。

问题来源:HDU1712

问题: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?

输入:

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.

输出:

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

样例输入:

2 2
1 2
1 3
2 2
2 1
2 1
2 3
3 2 1
3 2 1
0 0

样例输出:

3
4
6

题目大意是一个人有n门课程,并且他有m天的时间,如何在这m天的时间内选择不同的课程以收益最大,对于输入来说,首先输入n(课程数),m(天数),然后是一个二维数组A[i][j],代表第i门课程在学了j天后的收益。

乍一看好像这和组没有什么关系啊?但其实这里算是简化的组,也就是说,第i门课程在第j天能得到的收益就是之前要求的组内所有子问题中天数为j的价值最大的收益。所以这里还是一个三维数组,结果用一维数组反向遍历保存。(0-1背包的思路,比较简单)

代码如下(已AC):

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
    int n, m;   //n门课,m天
    while (cin >> n >> m && n != 0 && m != 0) {
        vector<vector<int>> course(n + 1, vector<int>(m + 1));  //课程数组
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= m; ++j)
                cin >> course[i][j];
        vector<int> profit(m + 1, 0);
        for (int i = 1; i <= n; ++i) {  //遍历每组
            for (int j = m; j > 0; --j) {   //反向遍历容量(只能选一个)
                for (int k = 1; k <= m; ++k) {  //遍历
                    if(j >= k)
                        profit[j] = max(profit[j], profit[j - k] + course[i][k]);
                }
            }
        }
        cout << profit[m] << endl;
    }

    return 0;
}

有依赖的背包问题

有依赖的背包问题具体表现为一部分物品是需要以另一部分物品的存在为前提的,也就是说存在主件附件的物品区别。一般而言,只有将一个主件加到背包中,才能将依赖这个主件的附件加到背包中

所以这里需要先对主件的所有附件做一次0-1背包,找到主件+附件(1~最大数)的不同组合中的价值,也就是说如果一个主件最多可以搭配k个附件,那么主件和附件之间就能有主件+1(所有附件中选1),主件+2,主件+3,…,主件+k个可能性。这里不能将所有可能单独列举出来,时间复杂度较高,所以直接以0-1背包方式将可能性存在数组里面,最后再对整个容量做一次0-1背包就行。

故对于有依赖的背包问题,是要做两次0-1背包处理的,首先是根据主件在附件中寻找合适的,使用0-1背包。其次是在总共的背包容量中,对不同主件+附件的组合使用0-1背包,这个时候主件+附件的组合更像是一个组,组内选择最优的一个子问题和别的组进行组合。

题目来源:HDU3449

题目:FJ is going to do some shopping, and before that, he needs some boxes to carry the different kinds of stuff he is going to buy. Each box is assigned to carry some specific kinds of stuff (that is to say, if he is going to buy one of these stuff, he has to buy the box beforehand). Each kind of stuff has its own value. Now FJ only has an amount of W dollars for shopping, he intends to get the highest value with the money.

输入:The first line will contain two integers, n (the number of boxes 1 <= n <= 50), w (the amount of money FJ has, 1 <= w <= 100000) Then n lines follow. Each line contains the following number pi (the price of the ith box 1<=pi<=1000), mi (1<=mi<=10 the number goods ith box can carry), and mi pairs of numbers, the price cj (1<=cj<=100), the value vj(1<=vj<=1000000)

输出:For each test case, output the maximum value FJ can get

输入样例:

3 800
300 2 30 50 25 80
600 1 50 130
400 3 40 70 30 40 35 60

输出样例:

210

这道题的大意是买物品和盒子,如果想买物品,必须要有盒子去装,这样主件和附件的关系就出来了,但是一个盒子能装几个物品是不确定的,需要用0-1背包将最优的子问题全部保存在数组中。这样,首先是对主件的循环,在每次循环的时候,将附件添加到主件中,然后把主件+附件添加到背包数组中,这里也是做一个0-1背包,将此次主件+附件的子问题和之前主件+附件的子问题进行相加,取最大值。

代码如下(已AC):

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100005;
int dp[N], tmp[N];
struct box
{
    int price, value;   //代价,价值
};
int main()
{
    int n, w;   //盒子数,总钱数
    while (cin >> n >> w) {
        memset(tmp, 0, N);
        memset(dp, 0, N);
        for (int idx = 0; idx < n; ++idx) {
            int p, m;   //箱子价格,箱子可以装的物品数量
            cin >> p >> m;
            memcpy(tmp, dp, sizeof(dp));
            box bx;
            for (int i = 0; i < m; ++i) {   //对附件采用0-1背包(得到盒子装不同物品的最大价值)
                cin >> bx.price >> bx.value;
                for (int i = w; i >= bx.price; --i)
                    tmp[i] = max(tmp[i], tmp[i - bx.price] + bx.value);
            }
            //对总钱数进行0-1背包
            for (int i = p; i <= w; ++i) {
                dp[i] = max(dp[i], tmp[i - p]);
            }
        }
        cout << dp[w] << endl;
    }

    return 0;
}

泛化物品

考虑这样一种物品,它并没有固定的费用和价值,而是它的价值随着你分配给它的费用而变化。这就是泛化物品的概念。

在背包容量为V的背包问题中,泛化物品是一个定义域为0到V中的整数的函数h,当分配给它的费用为v时,能得到的价值就是h(v)。这个定义有一点点抽象,另一种理解是一个泛化物品就是一个数组h[0..V],给它费用v,可得到价值h[V]。

一个费用为c价值为w的物品,如果它是0-1背包中的物品,那么把它看成泛化物品,它就是除了

h ( c ) = w

其它函数值都为0的一个函数。

如果它是完全背包中的物品,那么它可以看成这样一个函数,仅当占用容量v被物品重量c整除时有

h ( v ) = w v / c

其它函数值均为0。

如果它是多重背包中重复次数最多为n的物品,那么它对应的泛化物品的函数有

h ( v ) = w v / c

仅当v被c整除且v/c<=n,其它情况函数值均为0。

一个物品组可以看作一个泛化物品h。对于一个0到V中的v,若物品组中不存在费用为v的的物品,则h(v)=0,否则h(v)为所有费用为v的物品的最大价值。有依赖的背包问题中中每个主件及其附件集合等价于一个物品组,自然也可看作一个泛化物品。

如果面对两个泛化物品h和l,要用给定的费用从这两个泛化物品中得到最大的价值,怎么求呢?事实上,对于一个给定的费用v,只需枚举将这个费用如何分配给两个泛化物品就可以了。同样的,对于0到V的每一个整数v,可以求得费用v分配到h和l中的最大价值f(v)。即

f ( v ) = m a x ( h ( k ) + l ( v k ) | 0 <= k <= v )

可以看到,f也是一个由泛化物品h和l决定的定义域为0..V的函数,也就是说,f是一个由泛化物品h和l决定的泛化物品。

当之前在做分组问题的时候,面临不同的课程,学习不同的时间有不同的收益,这其实就是一个泛化物品。这也v从侧面告诉我们,泛化物品可以看作分组的0-1背包来求解。也就是说,之前介绍的问题都属于泛化物品中的一种特例,这里不再给出样例,具体样例可以看分组背包问题。

猜你喜欢

转载自blog.csdn.net/u012630961/article/details/80911956