背包 DP 前置知识:动态规划部分简介 。
在具体讲何为「背包 dp」前,先来看如下的例题:
「USACO07 DEC」Charm Bracelet 题意概要:有 
在上述例题中,由于每个物体只有两种可能的状态(取与不取),对应二进制中的 
0-1 背包 例题中已知条件有第 
设 DP 状态 
考虑转移。假设当前已经处理好了前 
由此可以得出状态转移方程:
这里如果直接采用二维数组对状态进行记录,会出现 MLE。可以考虑改用滚动数组的形式来优化。
由于对 
务必牢记并理解这个转移方程,因为大部分背包问题的转移方程都是在此基础上推导出来的。 
还有一点需要注意的是,很容易写出这样的错误核心代码:
// C++ Version 
for   ( int   i   =   1 ;   i   <=   n ;   i ++ )    for   ( int   l   =   0 ;   l   <=   W   -   w [ i ];   l ++ )      f [ l   +   w [ i ]]   =   max ( f [ l ]   +   v [ i ],   f [ l   +   w [ i ]]); // 由 f[i][l + w[i]] = max(max(f[i - 1][l + w[i]],f[i - 1][l] + w[i]),f[i][l + 
// w[i]]); 简化而来 
# Python Version 
for  i  in  range ( 1 ,  n  +  1 ): 
    l  =  0 
    while  l  <=  W  -  w [ i ]: 
        f [ l  +  w [ i ]]  =  max ( f [ l ]  +  v [ i ],  f [ l  +  w [ i ]]) 
        l  +=  1 
# 由 f[i][l + w[i]] = max(max(f[i - 1][l + w[i]],f[i - 1][l] + w[i]),f[i][l + 
# w[i]]) 简化而来 
这段代码哪里错了呢?枚举顺序错了。
仔细观察代码可以发现:对于当前处理的物品 
为了避免这种情况发生,我们可以改变枚举的顺序,从 
因此实际核心代码为
// C++ Version 
for   ( int   i   =   1 ;   i   <=   n ;   i ++ )    for   ( int   l   =   W ;   l   >=   w [ i ];   l -- )   f [ l ]   =   max ( f [ l ],   f [ l   -   w [ i ]]   +   v [ i ]); 
# Python Version 
for  i  in  range ( 1 ,  n  +  1 ): 
    l  =  W 
    while  l  >=  w [ i ]: 
        f [ l ]  =  max ( f [ l ],  f [ l  -  w [ i ]]  +  v [ i ]) 
        l  -=  1 
例题代码   1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 #include   <iostream> using   namespace   std ; const   int   maxn   =   13010 ; int   n ,   W ,   w [ maxn ],   v [ maxn ],   f [ maxn ]; int   main ()   {    cin   >>   n   >>   W ;    for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   cin   >>   w [ i ]   >>   v [ i ];    // 读入数据 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )      for   ( int   l   =   W ;   l   >=   w [ i ];   l -- )        if   ( f [ l   -   w [ i ]]   +   v [ i ]   >   f [ l ])   f [ l ]   =   f [ l   -   w [ i ]]   +   v [ i ];    // 状态方程 
   cout   <<   f [ W ];    return   0 ; } 
完全背包 完全背包模型与 0-1 背包类似,与 0-1 背包的区别仅在于一个物品可以选取无限次,而非仅能选取一次。
我们可以借鉴 0-1 背包的思路,进行状态定义:设 
需要注意的是,虽然定义与 0-1 背包类似,但是其状态转移方程与 0-1 背包并不相同。
可以考虑一个朴素的做法:对于第 
状态转移方程如下:
考虑做一个简单的优化。可以发现,对于 
理由是当我们这样转移时,
与 0-1 背包相同,我们可以将第一维去掉来优化空间复杂度。如果理解了 0-1 背包的优化方式,就不难明白压缩后的循环是正向的(也就是上文中提到的错误优化)。
「Luogu P1616」疯狂的采药 题意概要:有 
例题代码   1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 #include   <iostream> using   namespace   std ; const   int   maxn   =   1e4   +   5 ; const   int   maxW   =   1e7   +   5 ; int   n ,   W ,   w [ maxn ],   v [ maxn ]; long   long   f [ maxW ]; int   main ()   {    cin   >>   W   >>   n ;    for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   cin   >>   w [ i ]   >>   v [ i ];    for   ( int   i   =   1 ;   i   <=   n ;   i ++ )      for   ( int   l   =   w [ i ];   l   <=   W ;   l ++ )        if   ( f [ l   -   w [ i ]]   +   v [ i ]   >   f [ l ])   f [ l ]   =   f [ l   -   w [ i ]]   +   v [ i ];    // 核心状态方程 
   cout   <<   f [ W ];    return   0 ; } 
多重背包 多重背包也是 0-1 背包的一个变式。与 0-1 背包的区别在于每种物品有 
一个很朴素的想法就是:把「每种物品选 
时间复杂度 
二进制分组优化 考虑优化。我们仍考虑把多重背包转化成 0-1 背包模型来求解。
显然,复杂度中的 
在朴素的做法中,
我们可以通过「二进制分组」的方式使拆分方式更加优美。
具体地说就是令 
举几个例子:
显然,通过上述拆分方式,可以表示任意 
时间复杂度 
二进制分组代码   1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 // C++ Version 
index   =   0 ; for   ( int   i   =   1 ;   i   <=   m ;   i ++ )   {    int   c   =   1 ,   p ,   h ,   k ;    cin   >>   p   >>   h   >>   k ;    while   ( k   -   c   >   0 )   {      k   -=   c ;      list [ ++ index ]. w   =   c   *   p ;      list [ index ]. v   =   c   *   h ;      c   *=   2 ;    }    list [ ++ index ]. w   =   p   *   k ;    list [ index ]. v   =   h   *   k ; } 
 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 # Python Version 
index  =  0 
for  i  in  range ( 1 ,  m  +  1 ): 
    c  =  1 
    p ,  h ,  k  =  map ( lambda  x : int ( x ),  input () . split ()) 
    while  k  -  c  >  0 : 
        k  -=  c 
        list [ index ] . w  =  c  *  p 
        index  +=  1 
        list [ index ] . v  =  c  *  h 
        c  *=  2 
    list [ index ] . w  =  p  *  k 
    index  +=  1 
    list [ index ] . v  =  h  *  k 
单调队列优化 见 单调队列/单调栈优化 。
习题:「Luogu P1776」宝物筛选_NOI 导刊 2010 提高(02) 
混合背包 混合背包就是将前面三种的背包问题混合起来,有的只能取一次,有的能取无限次,有的只能取 
这种题目看起来很吓人,可是只要领悟了前面几种背包的中心思想,并将其合并在一起就可以了。下面给出伪代码:
for (循环物品种类) {
  if (是 0 - 1 背包)
    套用 0 - 1 背包代码;
  else if (是完全背包)
    套用完全背包代码;
  else if (是多重背包)
    套用多重背包代码;
}
「Luogu P1833」樱花 题意概要:有 
二维费用背包 「Luogu P1855」榨取 kkksc03 有 
 现在有 
这道题是很明显的 0-1 背包问题,可是不同的是选一个物品会消耗两种价值(经费、时间),只需在状态中增加一维存放第二种价值即可。
这时候就要注意,再开一维存放物品编号就不合适了,因为容易 MLE。
例题核心代码:
// C++ Version 
for   ( int   k   =   1 ;   k   <=   n ;   k ++ )   {    for   ( int   i   =   m ;   i   >=   mi ;   i -- )      // 对经费进行一层枚举 
     for   ( int   j   =   t ;   j   >=   ti ;   j -- )    // 对时间进行一层枚举 
       dp [ i ][ j ]   =   max ( dp [ i ][ j ],   dp [ i   -   mi ][ j   -   ti ]   +   1 ); } 
# Python Version 
for  k  in  range ( 1 ,  n  +  1 ): 
    i  =  m 
    while  i  >=  mi :  # 对经费进行一层枚举 
        j  =  t 
        while  j  >=  ti :  # 对时间进行一层枚举 
            dp [ i ][ j ]  =  max ( dp [ i ][ j ],  dp [ i  -  mi ][ j  -  ti ]  +  1 ) 
            j  -=  1 
        i  -=  1 
分组背包 「Luogu P1757」通天之分组背包 有 
这种题怎么想呢?其实是从「在所有物品中选择一件」变成了「从当前组中选择一件」,于是就对每一组进行一次 0-1 背包就可以了。
再说一说如何进行存储。我们可以将 
例题核心代码:
// C++ Version 
for   ( int   k   =   1 ;   k   <=   ts ;   k ++ )            // 循环每一组 
   for   ( int   i   =   m ;   i   >=   0 ;   i -- )           // 循环背包容量 
     for   ( int   j   =   1 ;   j   <=   cnt [ k ];   j ++ )    // 循环该组的每一个物品 
       if   ( i   >=   w [ t [ k ][ j ]])          dp [ i ]   =   max ( dp [ i ],                      dp [ i   -   w [ t [ k ][ j ]]]   +   c [ t [ k ][ j ]]);    // 像0-1背包一样状态转移 
# Python Version 
for  k  in  range ( 1 ,  ts  +  1 ):  # 循环每一组 
    for  i  in  range ( m ,  - 1 ,  - 1 ):  # 循环背包容量 
        for  j  in  range ( 1 ,  cnt [ k ]  +  1 ):  # 循环该组的每一个物品 
            if  i  >=  w [ t [ k ][ j ]]: 
                dp [ i ]  =  max ( dp [ i ],  \
                    dp [ i  -  w [ t [ k ][ j ]]]  +  c [ t [ k ][ j ]])  # 像0-1背包一样状态转移 
这里要注意:一定不能搞错循环顺序 ,这样才能保证正确性。
有依赖的背包 「Luogu P1064」金明的预算方案 金明有 
 目标是让所有购买的物品的 
考虑分类讨论。对于一个主件和它的若干附件,有以下几种可能:只买主件,买主件 + 某些附件。因为这几种可能性只能选一种,所以可以将这看成分组背包。
如果是多叉树的集合,则要先算子节点的集合,最后算父节点的集合。
泛化物品的背包 这种背包,没有固定的费用和价值,它的价值是随着分配给它的费用而定。在背包容量为 
杂项 小优化 根据贪心原理,当费用相同时,只需保留价值最高的;当价值一定时,只需保留费用最低的;当有两件物品 
背包问题变种 输出方案 输出方案其实就是记录下来背包中的某一个状态是怎么推出来的。我们可以用 
int   v   =   V ;    // 记录当前的存储空间 
// 因为最后一件物品存储的是最终状态,所以从最后一件物品进行循环 
for   ( 从最后一件循环至第一件 )   {    if   ( g [ i ][ v ])   {      选了第   i   项物品 ;      v   -=   第   i   项物品的重量 ;    }   else   {      未选第   i   项物品 ;    } } 
求方案数 对于给定的一个背包容量、物品费用、其他关系等的问题,求装到一定容量的方案总数。
这种问题就是把求最大值换成求和即可。
例如 0-1 背包问题的转移方程就变成了:
初始条件:
因为当容量为 
求最优方案总数 要求最优方案总数,我们要对 0-1 背包里的 
这样修改之后,每一种 DP 状态都可以用一个 
转移方程:
如果 
如果 
如果 
初始条件:
memset ( f ,   0x3f3f ,   sizeof ( f ));    // 避免没有装满而进行了转移 
f [ 0 ]   =   0 ; g [ 0 ]   =   1 ;    // 什么都不装是一种方案 
因为背包体积最大值有可能装不满,所以最优解不一定是 
最后我们通过找到最优解的价值,把 
核心代码:
 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 for   ( int   i   =   0 ;   i   <   N ;   i ++ )   {    for   ( int   j   =   V ;   j   >=   v [ i ];   j -- )   {      int   tmp   =   std :: max ( dp [ j ],   dp [ j   -   v [ i ]]   +   w [ i ]);      int   c   =   0 ;      if   ( tmp   ==   dp [ j ])   c   +=   cnt [ j ];                         // 如果从dp[j]转移 
     if   ( tmp   ==   dp [ j   -   v [ i ]]   +   w [ i ])   c   +=   cnt [ j   -   v [ i ]];    // 如果从dp[j-v[i]]转移 
     dp [ j ]   =   tmp ;      cnt [ j ]   =   c ;    } } int   max   =   0 ;    // 寻找最优解 
for   ( int   i   =   0 ;   i   <=   V ;   i ++ )   {    max   =   std :: max ( max ,   dp [ i ]); } int   res   =   0 ; for   ( int   i   =   0 ;   i   <=   V ;   i ++ )   {    if   ( dp [ i ]   ==   max )   {      res   +=   cnt [ i ];    // 求和最优解方案数 
   } } 
背包的第 k 优解 普通的 0-1 背包是要求最优解,在普通的背包 DP 方法上稍作改动,增加一维用于记录当前状态下的前 k 优解,即可得到求 0-1 背包第 
例题 hdu 2639 Bone Collector II 求 0-1 背包的严格第 
核心代码   1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 memset ( dp ,   0 ,   sizeof ( dp )); int   i ,   j ,   p ,   x ,   y ,   z ; scanf ( "%d%d%d" ,   & n ,   & m ,   & K ); for   ( i   =   0 ;   i   <   n ;   i ++ )   scanf ( "%d" ,   & w [ i ]); for   ( i   =   0 ;   i   <   n ;   i ++ )   scanf ( "%d" ,   & c [ i ]); for   ( i   =   0 ;   i   <   n ;   i ++ )   {    for   ( j   =   m ;   j   >=   c [ i ];   j -- )   {      for   ( p   =   1 ;   p   <=   K ;   p ++ )   {        a [ p ]   =   dp [ j   -   c [ i ]][ p ]   +   w [ i ];        b [ p ]   =   dp [ j ][ p ];      }      a [ p ]   =   b [ p ]   =   -1 ;      x   =   y   =   z   =   1 ;      while   ( z   <=   K   &&   ( a [ x ]   !=   -1   ||   b [ y ]   !=   -1 ))   {        if   ( a [ x ]   >   b [ y ])          dp [ j ][ z ]   =   a [ x ++ ];        else          dp [ j ][ z ]   =   b [ y ++ ];        if   ( dp [ j ][ z ]   !=   dp [ j ][ z   -   1 ])   z ++ ;      }    } } printf ( "%d \n " ,   dp [ m ][ K ]); 
参考资料与注释 build 本页面最近更新:2022/4/22 14:58:26 ,更新历史 edit 发现错误?想一起完善? 在 GitHub 上编辑此页! people 本页面贡献者:greyqz , H-J-Granger , Ir1d , Link-cute , LuoshuiTianyi , Marcythm , StudyingFather , xyf007 , Alisahhh , Alphnia , CBW2007 , countercurrent-time , dhbloo , Enter-tainer , fps5283 , Henry-ZHR , hsfzLZH1 , hydingsy , iamtwz , kenlig , Konano , ksyx , NachtgeistW , Odeinjul , odeinjul , ouuan , partychicken , sbofgayschool , SiyuanQAQ , sshwy , SukkaW , WAAutoMaton , weiranfu , wolfdan666 , x4Cx58x54 , Xeonacid , zhb2000 copyright 本页面的全部内容在 CC BY-SA 4.0  和 SATA