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

Posted weixia14

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划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_iin{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背包问题是一个特殊的整数规划问题。[maxsum_{i=1}^{n} v_ix_i] [left{egin{matrix} sum_{i=1}^{n} w_ix_i leq C & \ x_iin{0,1}, & 1 leq i leq n end{matrix} ight.]

最优子结构性质

0-1背包问题具有最优子结构性质。设((y_1,y_2,cdots, y_n))是所给0-1背包问题的一个最优解,则((y_2,cdots, y_n))是下面相应子问题的一个最优解:[maxsum_{i=2}^{n} v_ix_i] [left{egin{matrix} sum_{i=2}^{n} w_ix_i leq C-w_1y_1 & \ x_iin{0,1}, & 2 leq i leq n end{matrix} ight.]
因若不然,设((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背包问题的子问题[maxsum_{k=1}^{n} v_kx_k] [left{egin{matrix} sum_{k=1}^{n} w_kx_k leq j & \ x_kin{0,1}, & 1 leq k leq n end{matrix} ight.] 的最优值为(m(i,j)),即(m(i,j))是背包容量为(j),可选择物品为(i,i+1,.,n)时0-1背包问题的最优值。由0-1背包问题的最优子结构性质,可以建立如下计算(m(i,j))的递归式:[m(i,j)=left{egin{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} ight.] [m(n,j)=left{egin{matrix} v_n & j geq w_i & ---选\ 0 & 0 leq j < w_i & ---不选 end{matrix} ight.]

算法实现-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{egin{matrix} 6 & j geq 4 \ 0 & 0 leq j < 4 end{matrix} ight.]该函数是关于变量(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
动态规划-背包问题(跳跃点解法)








以上是关于动态规划01背包问题_两种解法的主要内容,如果未能解决你的问题,请参考以下文章

ACM:动态规划,01背包问题

动态规划本质理解:01背包问题

动态规划_01背包_完全背包_多重背包_分组背包

动态规划_01背包

01背包问题与动态规划(DP)

动态规划2——完全背包问题解析