【动态规划】01背包问题_两种解法

问题描述

0-1背包问题:给定\(n\)种物品和一背包。物品i的重量是\(w_i\),其价值为\(v_i\),背包的容量为\(C\)。问:应该如何选择装入背包的物品,使得装人背包中物品的总价值最大?
在选择装人背包的物品时,对每种物品\(i\)只有两种选择,即装人背包或不装入背包。不能将物品\(i\)装入背包多次,也不能只装入部分的物品\(i\)。因此,该问题称为0-1背包问题
此问题的形式化描述是,给定\(C>0\),\(w_i>0\),\(v_i>0\),\(1≤i≤n\),要求找出\(n\)元0-1向量\((x_1,x_2,\cdots,x_n), x_i\in\{0,1\},1 \leq i \leq n\),使得\(\sum_{i=1}^{n} w_ix_i \leq C\),而且\(\sum_{i=1}^{n} v_ix_i\)达到最大。因此,0-1背包问题是一个特殊的整数规划问题。\[max\sum_{i=1}^{n} v_ix_i\] \[\left\{\begin{matrix} \sum_{i=1}^{n} w_ix_i \leq C & \\ x_i\in\{0,1\}, & 1 \leq i \leq n \end{matrix}\right.\]

最优子结构性质

0-1背包问题具有最优子结构性质。设\((y_1,y_2,\cdots, y_n)\)是所给0-1背包问题的一个最优解,则\((y_2,\cdots, y_n)\)是下面相应子问题的一个最优解:\[max\sum_{i=2}^{n} v_ix_i\] \[\left\{\begin{matrix} \sum_{i=2}^{n} w_ix_i \leq C-w_1y_1 & \\ x_i\in\{0,1\}, & 2 \leq i \leq n \end{matrix}\right.\]
因若不然,设\((z_2,\cdots, z_n)\)是上述子问题的-一个最优解,而\((y_2,\cdots, y_n)\)不是它的最优解。由此可知,\(\sum_{i=2}^{n} v_iz_i > \sum_{i=2}^{n} v_iy_i\),且\(w_1y_1 + \sum_{i=2}^{n} w_iz_i \leq C\)。因此,\[v_1y_1 + \sum_{i=2}^{n} v_iz_i > \sum_{i=1}^{n} v_iy_i\] \[w_1y_1 + \sum_{i=2}^{n} w_iz_i \leq C\]
这说明\((z_1,z_2,\cdots, z_n)\)是所给0-1背包问题的更优解,从而\((y_1,y_2,\cdots, y_n)\)不是所给0-1背包问题的最优解。此为矛盾。

递归关系

设所给0-1背包问题的子问题\[max\sum_{k=1}^{n} v_kx_k\] \[\left\{\begin{matrix} \sum_{k=1}^{n} w_kx_k \leq j & \\ x_k\in\{0,1\}, & 1 \leq k \leq n \end{matrix}\right.\] 的最优值为\(m(i,j)\),即\(m(i,j)\)是背包容量为\(j\),可选择物品为\(i,i+1,.,n\)时0-1背包问题的最优值。由0-1背包问题的最优子结构性质,可以建立如下计算\(m(i,j)\)的递归式:\[m(i,j)=\left\{\begin{matrix} max(m(i+1,j),m(i+1,j-w_i)+v_i) & j \geq w_i & ---选\\ m(i+1,j) & 0 \leq j < w_i & ---不选 \end{matrix}\right.\] \[m(n,j)=\left\{\begin{matrix} v_n & j \geq w_i & ---选\\ 0 & 0 \leq j < w_i & ---不选 \end{matrix}\right.\]

算法实现-DP表解法

示例

代码实现

基于以上讨论,当\(w_i(1 \leq i \leq n)\)为正整数时,用二维数组\(m[][]\)存储\(m(i,j)\)的相应值,可设计解0-1背包问题的动态规划算法knapsack如下:


01backpack_DPTable-python

class Kbackpack(object):
   def knapsack(self, c, w, v):
       m = []
       for i in range(len(v)):
           m.append([0] * (c + 1))
       n = len(v) - 1
       # 步骤①:将m(n,j)记录在表中
       jMax = min(w[n], c)
       for t in range(jMax, c + 1):
           if t >= w[n]:
               m[n][t] = v[n]
       # 步骤②:逐个记录m(i,j)
       for i in range(n - 1, 0, -1):
           # j<w_i: 不选
           jMax = min(w[i], c)
           for j in range(jMax):
               m[i][j] = m[i + 1][j]
           # j>w_i: 选
           for j in range(jMax, c + 1):
               m[i][j] = max(m[i + 1][j], m[i + 1][j - w[i]] + v[i])
       # 步骤③:单独算最后一个物品(最后一个物品无需再计算除C以外的其他容量的最优解)
       m[0][c] = m[1][c]
       if c > w[0]:
           m[0][c] = max(m[1][c], v[0] + m[1][c - w[0]])
       return m

回溯打印最优解

按上述算法knapsack计算后,\(m[1][c]\)给出所要求的0-1背包问题的最优值。相应的最优解可由算法traceback计算如下:
如果\(m[1][c]=m[2][c]\),则\(x_1='choose'\);否则\(x_1='discard'\)
\(x_1='discard'\)时,由\(m[2][c]\)继续构造最优解;
\(x_1='choose'\)时,由\(m[2][c-w_1]\)继续构造最优解。依此类推,可构造出相应的最优解\((x_1,x_2, \cdots, x_n)\)


traceback-python

    def traceback(self, m, w, c):
        x = ['discard'] * len(w)
        for i in range(len(w) - 1):
            if m[i][c] != m[i + 1][c]:
                x[i] = 'choose'
                c -= w[i]
        x[len(w) - 1] = 'choose' if m[len(w) - 1][c] > 0 else 0
        return x

计算复杂性分析

从计算\(m(i,j)\)的递归式容易看出,上述算法knapsack需要\(O(nC)\)计算时间,而算法traceback需要\(O(n)\)计算时间。

上述算法knapsack有两个较明显的缺点:
① 算法要求所给物品的重量\(w_i(1≤i≤n)\)是整数;
② 当背包容量C很大时,算法需要的计算时间较多。例如,当\(c>2^n\)时,算法knapsack 需要\(O(n2^n)\)计算时间。

事实上,注意到计算\(m(i,j)\)的递归式在变量\(j\)是连续变量,即背包容量为实数时仍成立,可以采用以下方法克服算法knapsack的上述两个缺点。

算法实现-跳跃点解法

首先考查0-1背包问题的上述具体实例:
物品(n):0, 1, 2, 3, 4
重量(w):2, 2, 6, 5, 4
价值(v):6, 3, 5, 4, 6
C=10

由计算\(m(i,j)\)的递归式,当\(i=4\)时,\[m(4,j)=\left\{\begin{matrix} 6 & j \geq 4 \\ 0 & 0 \leq j < 4 \end{matrix}\right.\]该函数是关于变量\(j\)的阶梯状函数。由\(m(i,j)\)的递归式容易证明,在一般情况下,对每一个确定的\(i(1≤i≤n)\),函数\(m(i,j)\)是关于变量\(j\)的阶梯状单调不减函数。跳跃点是这一类函数的描述特征。如函数\(m(4,j)\)可由其两个跳跃点(0,0)和(4,6)唯一确定。在一般
情况下,函数m(i,j)由其全部跳跃点唯一确定,如下图所示:

在变量\(j\)是连续变量的情况下,可以对每一个确定的\(i(1≤i≤n)\),用一个表\(p[i]\)存储函数\(m(i,j)\)的全部跳跃点。对每一个确定的实数\(j\),可以通过查找表\(p[i]\)确定函数m(i,j)的值。\(p[i]\)中全部跳跃点\((j ,m(i,j))\)\(j\)的升序排列。由于函数\(m(i,j)\)是关于变量\(j\)的阶梯状单调不减函数,故\(p[i]\)中全部跳跃点的\(m(i,j)\)值也是递增排列的。

\(p[i]\)可依计算\(m(i,j)\)的递归式递归地由表\(p[i+1]\)计算,初始时\(p[n+ 1]={(0,0)}\)。事实上,函数\(m(i,j)\)是由函数\(m(i+1,j)\)与函数\(m(i+1,j-w_i)+v_i\)做max运算得到的。因此,函数\(m(i,j)\)的全部跳跃点包含于函数\(m(i+1,j)\)的跳跃点集\(p[i+1]\)与函数\(m(i+1,j-w_1)+v_1\)的跳跃点集\(q[i+1]\)的并集中。易知,\((s,t)∈q[i+1]\)当且仅当\(w_i≤s≤C\)\((s-w_i,t-v_i)∈p[i+1]\)。因此,容易由\(p[i+1]\)确定跳跃点集\(q[i+1]\)如下:
\[q[i+1]= p[i+1] \oplus (w_i,v_i) = \{(j+w_i,m(i,j)+v_i) | (j,m(i,j))∈p[i+ 1]\}\]
另一方面,设\((a,b)\)\((c,d)\)\(p[i+1]∪q[i+ 1]\)中的两个跳跃点,则当\(c≥a\)\(d < b\)时,\((c,d)\)受控于\((a,b)\),从而\((c,d)\)不是\(p[i]\)中的跳跃点。除受控跳跃点外,\(p[i+1]Uq[i+1]\)中的其他跳跃点均为\(p[i]\)中的跳跃点。由此可见,在递归地由表\(p[i+1]\)计算表\(p[i]\)时,可先由\(p[i+ 1]\)计算出\(q[i+1]\),然后合并表\(p[i+1]\)和表\(q[i+ 1]\),并清除其中的受控跳跃点得到表\(p[i]\)
对于上面的例子,表\(p[]\)和表\(q[]\)分别如下:

代码实现

综上所述,可设计解0-1背包问题改进的动态规划算法如下:
代码在实现过程中,把二维表用作三维表,通过head[]数组划分,达到三维表的效果。


jumpPoint1-python

class Kbackpack(object):
    def knapsack1(self, c, w, v):
        p = [(0, 0)]
        q = []
        res = [] + p
        # head[i]表示p[i]在res[]中首元素的位置
        # 区分不同物品i对应的数据,相当于行标记
        head = [0] * len(v)
        head[-1] = len(res)
        for i in range(len(w) - 1, -1, -1):
            wv = (w[i], v[i])
            for ele in p:
                q.append(ele)
                qEle = tuple(map(lambda x: x[0] + x[1], zip(ele, wv)))
                if qEle[0] <= c:
                    q.append(qEle)
            q.sort(key=lambda x: x[0])
            p.clear()
            tempEle = q[0]
            for t in range(1, len(q)):  # 清除受控点
                if tempEle[0] <= q[t][0] and tempEle[1] > q[t][1]:
                    continue
                else:
                    p.append(tempEle)
                    tempEle = q[t]
            p.append(tempEle)
            res += p
            if i != 0:
                head[i - 1] = len(res)
            q.clear()
        return res, head

 

jumpPoint2-python ```python def knapsack2(self, c, w, v): p = [(0, 0)] # head[i]表示p[i]在res[]中首元素的位置 # 区分不同物品i对应的数据,相当于行标记 head = [0] * len(v) head[-1] = len(p) # 通过p[i+1]推导p[i]时,借助left,right指针 # 卡在p[i+1]的左右边界 left = 0 right = 0 for i in range(len(w) - 1, -1, -1): k = left # 从p[i+1]的左边界移至右边界 wv = (w[i], v[i]) for j in range(left, right + 1): qEle = tuple(map(lambda x: x[0] + x[1], zip(p[j], wv))) if qEle[0] > c: break while (k <= right and p[k][0] < qEle[0]): p.append(p[k]) k += 1 if (k <= right and p[k][0] == qEle[0]): qEle = (qEle[0], max(qEle[1], p[k][1])) k += 1 if (qEle[1] > p[-1][1]): p.append(qEle) while (k <= right and p[k][1] <= p[-1][1]): k += 1 while (k <= right): p.append(p[k]) k += 1 left = right + 1 right = len(p) - 1 if i != 0: head[i - 1] = len(p) return p, head ```

回溯打印最优解

tracebact4Jump-python ```python def traceback(self, w, v, p, head): bestRes = p[-1] x = ['discard'] * len(w) head.append(-1) for i in range(len(w)): # 从p[i-1]的开头遍历到p[i-1]的末尾 for k in range(head[i + 1], head[i] - 1): k = 0 if k == -1 else k # 判断第i个物品是否选择,若选,则将该物品对应p[i-1]的元素赋值给bestRes if (p[k][0] + w[i] == bestRes[0] and p[k][1] + v[i] == bestRes[1]): x[i] = 'choose' bestRes = p[k] break return x ```

计算复杂度分析

上述算法的主要计算量在于计算跳跃点集\(p[i](1≤i≤n)\)。由于\(q[i+1]=p[i+ 1] \oplus (w_i,v_i)\),故计算\(q[i+1]\)需要\(O(|p[i+1]|)\)计算时间。合并\(p[i+1]\)\(q[i+1]\)并清除受控跳跃点也需要\(O(|p[i+1]|)\)计算时间。从跳跃点集\(p[i]\)的定义可以看出,\(p[i]\)中的跳跃点相应于\(x_1,x_2, \cdots, x_n\)的0/1赋值。因此,\(p[i]\)中跳跃点个数不超过\(2^{n-i+1}\)。由此可见,算法计算跳跃点集\(p[i](1≤i≤n)\)所花费的计算时间为\[O(\sum_{i=2}^n |p[i+1]|)=O(\sum_{i=2}^n 2^{n-i})=O(2^n)\]从而,改进后算法的计算时间复杂性为\(O(2^n)\)。当所给物品的重量,\(w_i\)是整数时,\(|p[i]|≤c+1\),其中,\(1≤i≤n\)。在这种情况下,改进后算法的计算时间复杂性为\(O( min{nc,2^n})\)

参考

算法设计与分析(第3版) P75~P80
动态规划-背包问题(跳跃点解法)

猜你喜欢

转载自www.cnblogs.com/weixia14/p/11768990.html
今日推荐