动态规划-背包问题

动态规划-背包问题

1. 背包问题的分类

    市面上关于动态规划的大多数问题都是背包问题。背包问题主要分为五种:
        1.  0 1背包问题
        2.  完全背包问题
        3.  多重背包问题
        4.  多重背包问题的优化
        5.  分组背包问题

2. 0 1背包问题概述

    给定n个物品,和一个容量是v的背包。每一个物品有两个属性:vi表示第i个物品的体积,wi表示第i个物品的价值(权值)。每一件物品最多使用一次。现在我们需要挑选一些物品装入背包,问:在物品总体积不超过v的情况下,物品的总价值最大是多少?

3. 0 1背包问题的思想

img

    Dp问题主要分为两个方面来进行考虑:
    1.  状态表示
        状态表示指:对于某一个问题,我们需要用几维的状态来进行表示,每一种状态的含义是什么。所谓状态就是一个未知数。例如,背包问题只需要用二维的状态f(i,j)来表示即可。
        状态表示还可以从两个角度来进行考虑:
            1.1 f(i,j)所表示的集合是什么?
                f(i,j)虽然从实际上看是一个未知数,但是从抽象意义上看f(i,j)表示的是集合。至于f(i,j)具体上表示的是哪一个集合,要根据具体问题而定。例如,在背包问题中,f(i,j)表示的是满足指定条件的所有选法上的集合。具体条件是什么,要根据具体问题而定。
            1.2 f(i,j)所表示的集合属性是什么?
                f(i,j)从实际上看是一个未知数。从抽象层面上看表示的是集合。f(i,j)所存储的未知数实际上就是这个集合的属性。一般来说,集合的属性有三种:
                1.2.1   这个集合的最大值
                1.2.2   这个集合的最小值
                1.2.3   这个集合中的元素数量
    2.  状态计算
        状态计算指:我们如何一步一步的将每一个状态f(i,j)计算出来。实际上就是在求状态转移方程。
    Dp问题的优化主要是指对某一个Dp问题的代码和方程做一个等价变形。所以,在考虑一个Dp问题的时候,一定要先考虑它的基本形式,然后在做相关的优化。

img
img

    接下来,我们来讲述一下关于0 1背包问题的求解思路:
        1.  首先,我们从状态表示和状态计算两个方面来进行考虑。
        2.  在状态表示方面:我们采用二维状态来表示我们的问题。即,f(i,j)。f(i,j)表示的集合为满足指定条件的所有选法的集合。在选择集合的时候,我们需要遵从两个条件:
            2.1 只从前i个物品中选
            2.2 总体积<=j
        f(i,j)所存储的数,表示的就是满足指定条件的集合中所有选法的价值的最大价值。(集合的最大值)
        3.  在状态计算方面:我们需要考虑f(i,j)是怎么计算出来的?假设,我们把所有的状态f(i,j)都计算出来的话,那么0 1 背包问题的答案就是:f(n,v)。即,只从前n个物品中选,总体积<=v的所有选法所构成集合,这些选法中所产生价值的最大值。
            如果说,状态表示的是集合。那么状态的计算对应的就是集合的划分。假设说,f(i,j)表示的是某一种集合。那么对于状态f(i,j)的计算就应该是:将f(i,j)所表示的集合划分为若干个子集,这些子集都能够去计算出来。使得f(i,j)所表示的集合可以用更小的集合(子集)来表示出来。集合的划分主要有以下原则:
                3.1 不重复。某一个集合的元素(选法),不能同时属于两个集合。(有些时候,可以不满足)
                3.2 不漏。所划分的子集取并集之后,完全等于原集合。不能漏掉某一个元素或子集。(一定满足)
                3.3 至于以上原则是否满足的情况,需要具体问题具体分析。
            在背包问题中,f(i,j)所表示的集合,通常被划分为两大部分:
                3.1 选择物品时,不含第i个物品,总体积不超过j的所有选法构成的集合。实际上就是:从1~i-1当中选,总体积不超过j的所有选法构成的集合。我们可以用f(i-1,j)来表示。f(i-1,j)表示的就是这个集合中所有选法的最大价值。
                3.2 选择物品时,必须含第i个物品,总体积不超过j。我们发现,这样的情况不好用状态来进行表示。那么我们可以采用如下步骤进行表示:
                首先,我们需要明确的一点就是:求出这个集合中所有选法的最大价值。我们先将所有选法中的第i件物品去掉,这样做并不会影响这个集合中所有选法的最大价值。将所有选法中的第i件物品去掉的话,情况就变成了:从1~i-1当中选,总体积不超过j-vi的所有选法所构成的集合。其中,vi表示第i个物品的体积。这时,我们可以用状态f(i-1,j-vi)表示。f(i-1,j-vi)表示的就是这个集合中所有选法的最大价值。由于我们将第i个物品去掉了,因此最后我们需要在原来的基础上加上第i个物品的价值即可。即,f(i-1,j-vi)+wi。
                需要注意的就是:第二个子集可能为空集。什么情况下是空集呢?由于第二个子集中的选法必须包含第i件物品,那么如果第i件物品的体积大于j,那么这个子集就为空。编写代码的时候,需要考虑到这一点。
            我们可以发现,f(i,j)的状态完全可以由以上两个子状态进行表示。
            综上所述,如果我们要求f(i,j)的最大值的话,实际上就是以上两个子集所表示状态的最大值。
    我们可以对以上0 1背包问题做一个优化,使其从二维状态转换成一维状态。具体思想如下:
        首先,我们知道二维状态的状态转移方程:
        f(i,j) = Max(f(i-1,j),f(i-1,j-vi)+wi);
        我们观察上述的方程,我们可以发现:
        1.  f(i,j)的计算只用到了i-1这一层,没有用到0~i-2这一层。
        2.  以及,无论是j还是j-vi都是小于等于j的。
        因此,我们可以从如上角度来进行优化。
    首先,根据上述的第一个角度,我们可以将二维状态缩小到一维状态。只需要用一维数组原地计算即可。
    因此,在二维0 1背包问题的代码中:我们需要修改:
        1.  //代表状态
            int f[N][N];        =>  int f[N]
        2.  //所以我们从1开始遍历
            for(int i=1;i<=n;i++){  
                for(int j=1;j<=m;j++){
                    f[i][j] = f[i-1][j];        =>f[j] = f[j]   //注意:这里是一个恒等式,所以可以忽略不计
                    //如果第二个子集不是空集
                    if(v[i] <= j){              =>由于j小于v[i]的时候,无意义。因此,我们可以直接让j从v[i]开始,把这个判断去掉。
                        f[i][j] = max(f[i][j],(f[i-1][j-v[i]]+w[i]));   
                        =>修改为:f[j] = max(f[j],f[j-v[i]]+w[i]);
                    }
                }
            }
            printf("%d",f[m]);  =>f[n][m] =>f[m]        //此时f[m]代表从1~N物品选,总体积不超过m的最大价值。
    综上所述,上述内容可以修改为:
    int f[N];
    for(int i=1;i<=n;i++){
        //j从小到大遍历
        for(int j=v[i];j<=m;j++){
                f[j] = max(f[j],f[j-v[i]]+w[i]);
        }
    }
    printf("%d",f[m]);
    但是,这么修改真的对吗?我们发现,由于从二维=>一维,因此就会出现数据"覆盖"的问题。
    上述的代码应该让j从m到v[i]遍历,这样的话就正确了。
    为什么?为什么从二维修改到一维之后,j从原先的小->大遍历到现在的大->小遍历?
    这就是一维数组会出现的数据"覆盖"问题。我们通过一个案例来进行讲解。
    假设,我们有两件物品:
        1.  体积为1,权重为1。
        2.  体积为1,权重为1。
    现在,我们要求:从前两个物品选,每种物品只能选一次,总体积不超过2的最大价值。
    我们很容易使用二维来进行计算,直接套用上述的状态转移方程即可:
    //这里需要提一下:由于选择0件物品时或体积为0时,最大价值都是0。因此i、j从1开始
    for(int i=1;i<=2;i++){
        for(int j=1;j<=2;j++){
            f[i][j] = f[i-1][j];
            if(j >= v[i]){
                f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);
            }
        }
    }
    //从二维中,我们知道第i层的计算,需要依赖于前一层。而我们采用的是二维数组,因此我们可以很好的将前一层的值存储下来,不会有数据覆盖的问题。换句话说,f[i][*]和f[i-1][*]之间是独立的。(很重要)
    我们现在转到一维,j从小到大遍历,看看有什么问题。
    我们现在将i=0的情况看做是[上一层]的情况。
    f[0] = 0,f[1] = 0,f[2] = 0;(这个含义:我选择0种物品,总体积不超过0和1和2的最大价值)
    我们现在开始遍历:
        for(int i=1;i<=2;i++){
            for(int j=v[i];j<=2;j++){
                f[j] = max(f[j],f[j-v[i]]+w[i]);
            }
        }
    首先,当i为1时,在内层循环中我们要计算f[1]和f[2]。此时,f[1]和f[2]的含义:从前1个物品中选,总体积不超过12的最大价值。
        当j为1时:
            j = v[1] = 1,f[1] = max(f[1],f[0]+w[1]) = max(0,1) = 1;
            注意:此时的f[1]是第i(i=1)层的f[1](=1),它把i-1(i=0)层的f[1](=0)覆盖了。
        接下来,当j为2的时候,
            f[2] = max(f[2],f[2-v[1]]+w[1]) = max(0,f[1]+w[1]) = 2;
            注意:这里就出现了问题,由于我们在二维的时候,第i层的数据用的都是i-1层的数据。而如果让j从小到大遍历的话,f[2]使用的f[1]并不是上一层的f[1]而是这一层的f[1]。这样的话,数据就出现了问题。f[2] = 2意味着:我从前1个物品选,总体积不超过2的最大价值为2。但是第一个物品体积为1,由于第一个物品只能选择1次。因此,f[2] = 2相当于把第一个物品选了两次,这是一个完全背包问题,就不是0 1 背包问题了。
    当我们让j从大到小遍历的时候:
        当i等于1,j为2时:
            f[j] = max(f[j],f[j-v[i]]+w[i]) = max(f[2],f[2-v[1]]+w[1]) = f[2]
            f[2] = max(f[2],f[1]+w[1]) = max(0,0+1) = 1;
            注意:这样的话,由于f[1]=0,它仍然是第i-1(0)层的f[1],没有被覆盖,因此这样计算是正确的。f[2] = 1;
        当i等于1j为1时:
            f[1] = max(f[1],f[0]+w[1]) = max(0,0+1) = 1;
            注意:这样的话,由于f[0] = 0,它仍然是第i-1(0)层的f[0],没有被覆盖,因此这样计算是正确的。
        当i等于2时,跟上述情况类似,这里就不再赘述。
    因此,当缩小到一维时,j从小到大遍历的话,就会导致:第i层的计算会用到第i层的内容,而不是上一层或i-1层的内容。根本原因在于:j从小到大遍历的话,由于j会越来越大,j-v[i]不可避免的会访问到j遍历过的值。由于j每一次遍历的时候,都会将第i层的内容计算出来,因此就导致了第i层的计算会用到i层的内容而不是i-1层的内容。(i层的内容将i-1层的内容覆盖了)。
    所以,当j从大到小遍历的时候,由于j会越来越小,j-v[i]一定比当前的j和上一个循环的j(j遍历过的值)更小(j-v[i]不会访问到j遍历过的值)。因此,使用的都是i-1层的值,而不会被第i层的值覆盖。因此,这就是为什么一维时,j从大到小遍历的原因。
    这样的话,就完成了对二维状态转移公式的等价变形。

4. 0 1背包例题

https://www.acwing.com/activity/content/problem/content/997/

img

    上图是0 1背包问题的案例。
//这种解法为未优化(二维)解法
#include <iostream>
#include <cstdio>

using namespace std;

const int N = 1010;
//代表体积和价值
int v[N],w[N];
//代表状态
int f[N][N];

int main(){
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        int a,b;
        scanf("%d%d",&a,&b);
        v[i] = a;
        w[i] = b;
    }
    //我们不考虑:选择0种物品,总体积小于等于1~m的情况。因为这种状态的最大值一定为0。
    //所以我们从1开始遍历
    for(int i=1;i<=n;i++){  
        for(int j=1;j<=m;j++){
            f[i][j] = f[i-1][j];
            //如果第二个子集不是空集
            if(v[i] <= j){
                f[i][j] = max(f[i][j],(f[i-1][j-v[i]]+w[i]));
            }
        }
    }
    printf("%d",f[n][m]);
    return 0;
}
//这种解法为已优化(一维)解法
#include <iostream>
#include <cstdio>

using namespace std;

const int N = 1010;

int v[N],w[N];

int n,m;

int f[N];

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        int a,b;
        scanf("%d%d",&a,&b);
        v[i] = a;
        w[i] = b;
    }
    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]);
        }
    }
    printf("%d",f[m]);
    return 0;
}

5. 完全背包问题概述

    给定n个物品,和一个容量是v的背包。每一个物品有两个属性:vi表示第i个物品的体积,wi表示第i个物品的价值(权值)。每一件物品可以使用无限次。现在我们需要挑选一些物品装入背包,问:在物品总体积不超过v的情况下,物品的总价值最大是多少?

6. 完全背包问题的思想

img
img

    完全背包问题,我们也可以由dp问题的两种方面来进行考虑:
        1.  状态表示
            完全背包问题的状态表示和0 1背包问题的状态表示相同,这里就不再赘述。
        2.  状态计算
            由于第i个物品我们可以选择无数次,因此状态f(i,j)我们就可以划分为如下子集:
                2.1 从前i个物品中选,不包括第i个物品,总体积不超过j的所有选法的集合。属性:所有选法的总价值的最大值。
                2.2 从前i个物品中选,选择第i个物品1次,总体积不超过j的所有选法的集合。属性:同上。
                2.3 从前i个物品中选,选择第i个物品2次,之后同上。
                ...
                2.k 从前i个物品中选,选择第i个物品k次,之后同上。(这里为什么是k?因为虽然第i个物品理论上可以选择无数次,但是背包的体积是有限的,因此物品的个数也有了相应的限制。)
            那么,对于上述的子集,我们如何进行计算呢?具体如下:
                2.1 f(i-1,j)(第i件物品选择0次)
                2.2~2.k 对于之后的集合,直接算不好求,我们可以直接分三步去算:
                    (1) 所有选法,去掉k个i物品。(会保证集合的最大值)
                    (2) 那么剩下的选法可以由这样的状态表示:f(i-1,j-k*v[i])
                    (3) 再把k个i物品加回去。这样的状态就变为:f(i-1,j-k*v[i])+k*w[i](第i件物品选择1~k次)
            我们只需要对这些子集的总价值求最大就是f(i,j)。
            同时,我们发现,上面的集合可以合起来,因为当k=0时:
                f(i-1,j-k*v[i])+k*w[i] = f(i-1,j);
            综上所述:二维状态下的状态转移方程:
                f(i,j) = max(f(i-1)(j-k*v[i])+k*w[i]);(k=0,1,2...不能无限大)
            
    我们发现,上述做法的时间复杂度很高,为O(n三次方)。因此,我们需要对完全背包问题进行优化,使其优化到二维。

img

    接下来,我们将完全背包问题优化到二维。具体思想如下:
        我们可以查看上述的状态转移方程,将其展开,我们可以得到:
            f(i,j) = max(f(i-1,j),f(i-1,j-v[i])+w[i],f(i-1,j-2*v[i])+2*w[i],...);
        我们再次查看一下f(i,j-v[i]),将其展开,我们可以得到:
            f(i,j-v[i]) = max(f(i-1,j-v[i]),f(i-1,j-2*v[i])+w[i],....);
        我们可以将这两个展开式对齐,发现图中画橙色框框的部分与其下面相对应的部分类似,只不过比下面多了w[i]。
        因此,对于橙色框框部分的最大值,实际上就是f(i,j-v)+w。
        所以,f(i,j) = max(f(i-1,j),f(i,j-v[i])+w[i]);
        之后,我们可以惊奇的发现,f(i,j)的状态计算跟k无关了,因此就由三重循环降为了二重。

img

    我们可以将0 1背包问题的状态转移方程和完全背包问题的状态转移方程进行比较,发现非常相似,具体见上图红色框框。
        1.  0 1 背包问题的状态计算由上一层得到。
        2.  完全背包问题的状态计算由本层得到。(可以对照一下0 1 背包问题的数据覆盖问题)。
    我们可以在上述完全背包问题的状态转移方程的基础上,优化为一维数组。优化的思想跟0 1背包问题类似。
        1.  将二维转换成一维,至于为什么不解释(请见上述内容)。
        2.  j的循环从小到大,而不是从大到小。因为从前面0 1背包的部分解释过这种现象:j从小到大的话,f[j-v[i]]实际上取的是本层的f[j-v[i]]。换句话说,f[j-v[i]] <=> f[i][j-v[i]]。然而,从大到小的话,取的就是上一层的f[j-v[i]]。换句话说,f[j-v[i]] <=> f[i-1][j-v[i]]。所以,对于完全背包问题,j应该从小到大。对于0 1背包问题,j应该从大到小。

7. 完全背包例题

https://www.acwing.com/activity/content/problem/content/998/
//这是二维状态的完全背包问题
//时间复杂度为O(n三次方)
//现在,会超时。
//因此,我们需要对其进行优化。
#include <iostream>
#include <cstdio>

using namespace std;

const int N = 1010;

int n,m;

int v[N],w[N];

int f[N][N];



int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        int a,b;
        scanf("%d%d",&a,&b);
        v[i] = a;
        w[i] = b;
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            //由于每一种选法必带k件的i物品,所以一定要避免空集的情况。k*v[i] > j
            for(int k=0;k*v[i] <= j;k++){
                f[i][j] = max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
            }
        }
    }
    printf("%d",f[n][m]);
    return 0;
}
//完全背包问题的优化由O(n的三次方)=>O(n²)

#include <iostream>
#include <cstdio>

using namespace std;

const int N = 1010;

int n,m;

int v[N],w[N];

int f[N][N];



int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        int a,b;
        scanf("%d%d",&a,&b);
        v[i] = a;
        w[i] = b;
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            f[i][j] = f[i-1][j];
            //对空集的处理跟0 1 背包问题一样
            if(j >= v[i]){
                f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]);
            }
        }
    }
    printf("%d",f[n][m]);
    return 0;
}
//将完全背包问题,优化为一维数组
#include <iostream>
#include <cstdio>

using namespace std;

const int N = 1010;

int n,m;

int v[N],w[N];

int f[N];



int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        int a,b;
        scanf("%d%d",&a,&b);
        v[i] = a;
        w[i] = b;
    }
    for(int i=1;i<=n;i++){
        for(int j=v[i];j<=m;j++){
            f[j] = max(f[j],f[j-v[i]]+w[i]);
        }
    }
    printf("%d",f[m]);
    return 0;
}

8. 多重背包问题概述

    给定n个物品,和一个容量是v的背包。每一个物品有两个属性:vi表示第i个物品的体积,wi表示第i个物品的价值(权值)。每一件物品可以使用xi次(注:每件物品的xi可能不同)。现在我们需要挑选一些物品装入背包,问:在物品总体积不超过v的情况下,物品的总价值最大是多少?

9. 多重背包问题的思想

    多重背包问题的思想跟完全背包问题的思想类似。只不过从无限个变成了有限个。k的范围从0~s[i]。其中,s[i]代表第i件物品的个数。详细过程这里就不再讲解,请看上述的完全背包问题。
    这里直接给出二维状态下的状态转移方程:(跟完全背包一样,只是范围不同)
    f[i][j] = max(f[i-1][j-v[i]*k]+k*w[i]);
    其中,k = 0,1,2,...,s[i]。

10. 多重背包例题

https://www.acwing.com/activity/content/problem/content/999/
//此代码维多重背包问题的朴素解法
//三重循环,时间复杂度为:O(n*m*s)
#include <iostream>
#include <cstdio>

using namespace std;

const int N = 110;

int n,m;

int v[N],w[N],s[N];

int f[N][N];

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        int a,b,k;
        scanf("%d%d%d",&a,&b,&k);
        v[i] = a;
        w[i] = b;
        s[i] = k;
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            for(int k=0;k<=s[i] && k*v[i] <= j;k++){
                f[i][j] = max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
            }
        }
    }
    printf("%d",f[n][m]);
    return 0;
}

11. 多重背包问题的优化

img

    首先,我们可以提出一个问题:能否用完全背包问题的优化方法对多重背包问题进行优化呢?答案是不可以的。上图展示了多重背包问题子集的展开式。我们可以发现,f[i,j-v]比f[i,j]多出了一项。换句话说,我们已知f[i,j-v],已知f[i-1,j-(s+1)v]+sw。我们是不能够求出f[i,j-v]子集中除了f[i-1,j-(s+1)v]+sw之外子集的最大值。因此,使用完全背包问题的优化思路来优化多重背包问题是不可行的。

img
img

    因此,我们接下来介绍另外一种优化多重背包问题的方式:(二进制优化方式)
        假设,i物品有1023件,根据多重背包问题的朴素算法,我们需要从0遍历到1023。但是,这显然很低效。那么,我们可以采取如下算法:
        1.1023件物品分为10组,每一组中存放的i物品个数都是2的整数次幂。分别是:1 2 4 8 16 ... 5122.  选择物品时,我们只能选择每一组当中的1个物品。那么,我们如何利用这10组来表示0~1023任意一个数字呢?我们可以将每一组看做是2进制数中的一位。当选择这一组中的一件物品时,我们将这一组(这一位)置为1。那么,如果我们不选10组中的任何一个物品。那么,在二进制的角度对应的就是0000000000 => 0。如果我们在每一组中都选择一件物品。那么,在二进制的角度对应的就是1111111111 => 1023。因此,我们就可以利用这10组来表示0~1023的任何一个数字。
        3.  由于每一组只能选择一次,因此我们可以把这个问题看做是0 1 背包问题。每一组对应选/不选两种选择。(换句话说,可以把每一组看做是一个新物品)
        上图展示了s = 200的案例。经过证明,我们可以发现,给定一个一般的数s,按照上述分组的方法,我们一定可以表示0~s中的任何一个数。(证明略)
    因此,我们可以进行总结:
        1.  给定i物品的个数s,我们可以将其分为logs组。
        2.  每组都可以看成是一种新物品,之后我们再对新的物品做一遍0 1背包问题即可。
    这样的话,时间复杂度由原来的:O(n*m*s)=>O(n*m*logs)的0 1 背包问题。

12. 多重背包优化例题

https://www.acwing.com/problem/content/5/
#include <iostream>
#include <cstdio>

using namespace std;

//1000*log2000代表组个数
const int N = 15000;

int v[N],w[N];

int f[N];

int n,m;


int main(){
    scanf("%d%d",&n,&m);
    //cnt代表小组数量
    int cnt = 0;
    for(int i=1;i<=n;i++){
        int a,b,s;
        scanf("%d%d%d",&a,&b,&s);
        //k代表每个小组中的物品
        //划分小组
        int k = 1;
        while(k<=s){
            cnt++;
            v[cnt] = k*a;
            w[cnt] = k*b;
            s -= k;
            k*=2;
        }
        if(s > 0){
            cnt++;
            v[cnt] = s*a;
            w[cnt] = s*b;
        }
    }
    //小组数量即为新物品数量
    n = cnt;
    //做一遍0 1背包问题即可
    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]);
        }
    }
    printf("%d",f[m]);
    return 0;
}

13. 分组背包问题概述

    给定n个物品,和一个容量是v的背包。每一个物品有两个属性:vi表示第i个物品的体积,wi表示第i个物品的价值(权值)。物品被分为多组,每一组中会有若干个物品,一组中只能选择一个物品。现在我们需要挑选一些物品装入背包,问:在物品总体积不超过v的情况下,物品的总价值最大是多少?

14. 分组背包问题思想

img
img

    首先,我们从dp的两个角度来考虑分组背包问题。
        1.  状态表示:f[i,j]表示只从前i组物品中选,且总体积不大于j的所有选法。集合的属性就是最大值。
        2.  状态计算。对于f[i,j]我们可以按照选择第i组的某个物品来划分子集。
            2.1 选择第i组的第0个物品。这个可以用f[i-1,j]
            2.2 选择第i组的第1~k个物品。(其中,k代表第i组中的物品数量)这个可以用f[i-1,j-v[i,k]] + w[i,k] (其中,v[i,k]代表第i组中第k个物品的体积,w[i,k]代表第i组中第k个物品的价值)。
    综上所述,状态转移方程:
        f[i,j] = Max(f[i-1,j],f[i-1,j-v[i,k]]+w[i,k]);
    同理,分组背包问题也可以转换成一维。
        状态转移方程:f[j] = Max(f[j],f[j-v[i,k]]+w[i,k]);
    转换成一维的规律:如果方程中用的是本层,那么从小到大遍历体积。如果方程中用的是上一层那么就从大到小遍历体积。

15. 分组背包问题例题

https://www.acwing.com/problem/content/9/
#include <iostream>
#include <cstdio>

using namespace std;

const int N = 110;

//代表某件物品的体积,价值,s[i]代表第i组的元素数量
int v[N][N],w[N][N],s[N];

int f[N];

int n,m;

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%d",&s[i]);
        for(int j=1;j<=s[i];j++){
            int a,b;
            scanf("%d%d",&a,&b);
            v[i][j] = a;
            w[i][j] = b;
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=m;j>=1;j--){
            for(int k=1;k<=s[i];k++){
                //避免空集的情况
                if(v[i][k] <= j){
                    f[j] = max(f[j],f[j-v[i][k]]+w[i][k]);
                }
            }
        }
    }
    printf("%d",f[m]);
    
    return 0;
}
    作者:gao79138
    链接:https://www.acwing.com/
    来源:本博客中的截图、代码模板及题目地址均来自于Acwing。其余内容均为作者原创。
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
posted @   夏目^_^  阅读(76)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示