背包问题全解析

背包问题

阅读崔添翼老师《背包九讲》后,个人所作总结

目录

  1. 01背包
  2. 完全背包
  3. 多重背包
  4. 混合背包
  5. 二维费用背包
  6. 分组背包
  7. 有依赖的背包问题
  8. 泛化物品
  9. 背包问题目标的变化

01背包

  1. 题目描述:有N件物品和一个容量为V的背包。放入第i件物品耗费的费用是Ci,得到的价值是Wi。求解将哪些物品装入背包可使价值总和最大。

  2. 特点:每种物品只有一件,选择放或不放

  3. 状态:F[i,v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。

  4. 状态转移方程(在考虑第i件物品时,只有放和不放两种情况):F[i,v]=max{F[i−1,v],F[i−1,v−Ci]+Wi}。

    // 伪代码
    F[0,0..V]←0
    for i ← 1 to N
        for v ← Ci to V
            F[i,v] ← max{F[i−1,v],F[i−1,v−Ci]+Wi}
    
  5. 边界:相当于啥都不放,背包获得的最大价值(可推广到其它类型的背包问题)

    • 要求恰好装满背包:F[0]为0,其它F[1..V]均设为−∞
    • 背包可以不装满:F[0..V]均设为0
  6. 空间优化:用F[v]以及第i次循环表示前i件物品恰放入容量为v的背包获得的最大价值。

    // 伪代码
    F[0..V]←0
    for i ← 1 to N
        // 逆序保证F[v-Ci]是F[i-1,v-Ci],确定物品不会重复取,否则会出现重复取的
        for v ← V to Ci
            F[v] ← max{F[v],F[v−Ci]+Wi}
    // 综合一下
    def ZeroOnePack(F,Ci,Wi)
        for v ← V to C
            F[v] ← max(F[v],F[v−C]+W)
    
    for i ← 1 to N
        ZeroOnePack(F,Ci,Wi);
    

顶部

完全背包

  1. 题目描述:和01背包类似,只不过物品可以是无限件。

  2. 特点:物品可以是无限件,取0,1,2…⌊V/Ci⌋件

  3. 状态:F[i,v]表示前i种物品恰放入一个容量为v的背包可以获得的最大价值。

  4. 状态转移方程:以01背包的思路,在考虑第i件物品时,有取0,1,2…⌊V/Ci⌋件,从而得到F[i,v]=max{ F[i−1,v−kCi] + kWi | 0 ≤ kCi ≤ v}

  5. 时间优化

    • 对数据的初步筛选O(N2):若两件物品i、j满足Ci≤Cj且Wi≥Wj,则将可以将物品j直接去掉,不用考虑。
    • (未改进时间复杂度)将无法确定下来的第i种物品,分解成⌊V/Ci⌋件价值相同,花费相同的物品,转换成01背包。
    • 第二种的改进版(二进制思想)将第i种物品拆成费用为Ci2k、价值为Wi2k的物件,k取遍Ci2k≤V的所有整数。
  6. 最终O(VN)的算法:

    // 伪代码
    F[0..V]←0
    for i ← 1 to N
        // 和01背包相反,需要一个可能已选入第i种物品的子结果F[i,v−Ci]
        for v ← Ci to V
            F[v] ← max{F[v],F[v−Ci]+Wi}
    // 综合一下
    def CompletePack(F,Ci,Wi)
        for v ← C to V
            F[v] ← max(F[v],F[v−C]+W)
    
    for i ← 1 to N
        CompletePack(F,Ci,Wi);
    

顶部

多重背包

  1. 题目描述:较完全背包,第i种物品最多有Mi件可用。

  2. 状态:F[i,v]表示前i种物品恰放入一个容量为v的背包可以获得的最大价值。

  3. 状态转移方程:和完全背包类似,只是限制条件由⌊V/Ci⌋转换为Mi。
    F[i,v]=max{F[i−1,v−k∗Ci]+k∗Wi|0≤k≤Mi}

  4. 时间优化:

    • 同样考虑拆分物品转换为01背包,运用二进制的思想,则将物品转换为1,2,4…2(k−1),Mi−2k+1(k是满足Mi−2k+1>0的最大整数),则可以用这些系数组合成0-M的任何数字。将第i种物品分成了O(logMi)种物品。伪代码如下:
    def MultiplePack(F,C,W,M)
        // M的限制太小,转换为完全背包
        if C·M > V
            CompletePack(F,C,W)
            return
        // 遍历拆分的所有第i种物品,1,2,4...2^k
        k ← 1
        while k < M
            ZeroOnePack(F,kC,kW)
            M ← M - k
            k ← 2k
        // 最后一项
        ZeroOnePack(F,CM,WM)
    // 循环每种物品
    for i ← 1 to N
        MultiplePack(F,Ci,Wi,Mi);
    
  5. 延申:当考虑每种若干件的物品能否填满给定容量的背包时,即不考虑价值,只考虑可行性,可优化为O(VN)

顶部

混合背包

  1. 题目描述:存在只有1件、无限件、M件的物品。

  2. 直接的想法:判断该种物品为那种类型,调用对应的方法即可。

    for i ← 1 to N
        if 第i件物品属于01背包
            ZeroOnePack(F,Ci,Wi)
        else if 第i件物品属于完全背包
            CompletePack(F,Ci,Wi)
        else if 第i件物品属于多重背包
            MultiplePack(F,Ci,Wi,Ni)
    

顶部

二维费用背包

  1. 题目描述:每一个物品,有两种费用,对应的也有两个背包容量,求最大价值。

  2. 基本思路:在原有基础上,多加一维即可。

  3. 状态:F[i,v,u]表示前i件物品付出两种费用分别为v和u时可获得的最大价值。

  4. 状态转移方程:类似
    F[i,v,u]=max{F[i−1,v,u],F[i−1,v−Ci,u−Di]+Wi}

  5. 空间优化:同理,使用F[V,U]。

  6. 处理不同类型的物品和一维的一致。

  7. 一维到二维的变形:

    • 第二维费用可能是隐含给出,比如物品总件数的限制,注意抽象。
    • 若题目由熟悉的DP变形而来,先考虑能否运用之前的状态和转移方程,做一定改变,比如在原来的状态加一维

顶部

分组背包

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

  2. 状态:F[k,v]表示前k组物品花费费用v能取得的最大权值。

  3. 状态转移方程:即将问题转换为是选k组中的某一件物品,还是不选,转换为01背包问题。
    F[k,v]=max{F[k−1,v],F[k−1,v−Ci]+Wi|itemi∈groupk}

  4. 空间优化:和01背包一致,使用一维数组。

    for k ← 1 to K
        for v ← V to 0
            for all item i in group k
                F[v] ← max(F[v], F[v-Ci] + Wi)
    
  5. 时间优化:可参考完全背包中的数据筛选。

顶部

有依赖的背包问题

  1. 问题描述:物品i依赖于物品j,表示若选物品i,则必须选物品j。为了简化起见,我们先设没有某个物品既依赖于别的物品,又被别的物品所依赖;另外,没有某件物品同时依赖多件物品。

    主件:不依赖于别的物品的物品
    附件:依赖于某主件的物品

  2. 分析

    • 参考拆分物品和分组背包的思想,将一件主件和其n个附件集合做为一组,则可转换成一个也不选,仅选择主件,选择主件后再选择一个附件,选择主件后再选择两个附件…共2^n+1个等效物品。显然需要优化!
    • 参考完全背包中的时间优化第一点,对相同费用的物品进行筛选,则可将2^n+1个转换成V−Ck+1个。复杂度可接受了。
    • 最后,只需将所有物品进行分组,组内进行物品拆分,运用分组背包算法即可。
  3. 一般化:若依赖关系是森林的形式(树形DP),即附件仍可是其他附件的主件,但不存在多重依赖和循环依赖的情况。

    • 仍使用之前的算法,但将拥有附件集合的附件看作物品组,并解出其在不同容量的价值(组内实际上也是基本有依赖的背包问题);
    • 将所有有附件集合的附件解出后,转换成了基本的有依赖背包问题。

    树形DP:在求父节点的属性值之前,需要使用DP的方法先求得其所有子节点的所有属性

顶部

泛化物品

  1. 概念:没有固定的费用和价值,随分配的费用变化而变化,即给定费用c,可得到价值w = h©。
    • 01背包:if(c == Ci) h(c) = Wi; else h(c) = 0;
    • 完全背包:if(c%Ci == 0) h(c) = Wi*(v/Ci); else h(c) = 0;
    • 多重背包:if(c%Ci == 0 && v <= Ci*Mi) h(c) = Wi*(v/Ci); else h(c) = 0;
    • 物品组:一个物品组即为一个泛化物品h。if(存在费用为c的物品) h(c) = 所有费用为c的物品的最大价值; else h(c) = 0;

顶部

背包问题目标的变化

以01背包为例,其他问题类似!

  1. 总价值的改变

    • 最多可放多少件物品:所有物品的价值 = 1
    • 最多可装满多少空间:物品价值 = 物品费用
    • 最小:将max改为min
  2. 输出方案

    • 引入G[i,v]:0 - F[i,v] = F[i−1,v];1 - F[i,v] = F[i−1,v−Ci] + Wi
    • 输出结果:
    i = N
    v = V
    while i > 0
        if G[i,v] == 0
            print 未选第i项物品
        else if G[i,v] == 1
            print 选第i项物品
            v = v - Ci
        i = i - 1
    

    DP问题输出方案的一般方法:记录每个状态的最优值是由状态转移方程中哪一个状态转移而来,因此当给出一个状态,可由此推出上一个状态,循环往复得到完整的方案!

  3. 方案总数

    • 除求可得到的最大价值外,还需得到装满背包或将背包装至某一指定容量的方案总数。将max改成sum即可(初始条件是F[0,0]=1)F[i,v] = sum{F[i−1,v],F[i,v−Ci]}
    • 最优(物品总价值最大)方案的总数:引入G[i,v]表示这个子问题的最优方案的总数,则原代码修改为:
    G[0,0] = 1
    for i = 1 to N
        for j = 0 to V
            F[i,v] = max(F[i−1,v],F[i−1,v−Ci] + Wi)
            G[i,v] = 0
            if F[i,v] == F[i−1,v]
                G[i,v] = G[i,v] + G[i-1][v]
            else
                G[i,v] = G[i,v] + G[i-1][v-Ci]
    
  4. 求次优解、第 K 优解

    • 将状态转换为有序队列,将状态转移方程中的max/min/sum转换为有序队列的合并,实际上就是在原来的方程中加了一维来表示结果的优先次序。
    • 状态:F[i,v] => F[i,v,1…K] 表示前i个物品中,背包大小为v时,第k优解的值
    • 状态转移方程
      • F[i−1][V] => F[i−1,v,1…K]
      • F[i−1,v−Ci] + Wi => F[i−1,v−Ci,1…K]的每个数加上Wi
      • 合并上述两个队列,并将前K项存到F[i,v,1…K]中
    • 注意点:若将策略不同但权值相同的两个方案是看作同一个解,则需要保证队列中的数没有重复!

你可能感兴趣的:(C++)