[01] knapsack problem dynamic programming _ two solutions

Problem Description

0-1 knapsack problem: given \ (n \) kinds of goods and a backpack. I is the weight of the article \ (W_i \) , the value of \ (V_I \) , knapsack capacity \ (C \) . Q: How should select the items into a backpack, making the total value of the loaded backpack the largest items?
In selecting items loaded backpack, and for each item \ (i \) only two choices, that is loaded backpack or not into the backpack. Items can not be \ (I \) into a bag several times, not only the charged portion of the object \ (I \) . Therefore, the problem is called 0-1背包问题.
Formal description of this problem is given \ (C> 0 \) , \ (W_i> 0 \) , \ (V_I> 0 \) , \ (≦ i ≦ n \) , asked to identify \ (n- \) element vector 0-1 \ ((x_1, x_2, \ cdots, x_n), x_i \ in \ {0,1 \},. 1 \ I Leq \ n-Leq \) , so \ (\ sum_ {i = 1 n-w_ix_i} ^ {} \ Leq C \) , and \ (\ sum_ {i = 1 } ^ {n} v_ix_i \) is maximized. Therefore, the 0-1 knapsack problem is a special integer programming problem.\[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.\]

Optimal substructure property

0-1 knapsack problem has structural sub-optimal. Set \ ((y_1, y_2, \ cdots, y_n) \) 0-1 knapsack problem given an optimal solution, then the \ ((y_2, \ cdots, y_n) \) is below a respective sub-problems most An optimum solution: \ [max \ sum_ {I = 2} ^ {n-} v_ix_i \] \ [\ left \ {\ the 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. \] because if not, set \ ((z_2, \ cdots, z_n) \) is of the sub an optimal solution, but - problems \ ((y_2, \ cdots, y_n) \) instead of its optimal solution. It can be seen, \ (\ sum_ {I} = ^ {n-2} v_iz_i> \ sum_ {I} = ^ {n-2} v_iy_i \) , and \ (w_1y_1 + \ sum_ {i = 2} ^ {n} w_iz_i \ Leq C \) . Thus, \ [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 \]
this shows \ ((z_1, z_2, \ cdots, z_n) \) is given 0-1 knapsack problem better solution, whereby\ ((y_1, y_2, \ cdots, y_n) \) is not an optimal solution to the 0-1 knapsack problem. This is a contradiction.

Recursive relationship

Given sub-problem set 0-1 knapsack problem \ [max \ sum_ K = {}. 1} ^ {n-v_kx_k \] \ [\ left \ {\ the begin Matrix {} \ sum_ K = {}. 1} ^ {n- w_kx_k \ leq j & \\ x_k \ in \ {0,1 \}, & 1 \ leq k \ leq n \ end {matrix} \ right. \] the optimal value of \ (m (i, j) \ ) , i.e. \ (m (i, j) \) is the capacity of the backpack \ (J \) , select articles are \ (i, i + 1, ., n \) optimum value of the 0-1 knapsack problem . 0-1 knapsack problem a proton optimal structure can be established calculated as follows \ (m (i, j) \) recursive formula: \ [m (I, J) = \ left \ {\ the begin {max} Matrix (m (i + 1, j ), m (i + 1, j-w_i) + v_i) & j \ geq w_i & --- selected from \\ m (i + 1, j ) & 0 \ leq j <w_i --- & uncheck \ Matrix End {} \ right. \] \ [m (n-, J) = \ left \ {\ & V_n the begin {J} Matrix \ --- & GEQ W_i selected from 0 \\ 0 & \ leq j <w_i & --- not selected \ end {matrix} \ right. \]

-DP solution algorithm table

Examples

Code

基于以上讨论,当\(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
动态规划-背包问题(跳跃点解法)

Guess you like

Origin www.cnblogs.com/weixia14/p/11768990.html