【动态规划】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 p[-1][1]):
p.append(qEle)
while (k

?

回溯打印最优解

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
动态规划-背包问题(跳跃点解法)

原文地址:https://www.cnblogs.com/weixia14/p/11768990.html

时间: 2024-12-31 03:12:42

【动态规划】01背包问题_两种解法的相关文章

涨姿势题2_水题_两种解法

Problem Description 涨姿势题就是所谓的优化题,在组队赛中,队伍发现了一题水题,那么应该交给谁去处理?作为处理水题的代码手,应该具备什么样的素养?1,要快,水题拼的就是速度!2,不能卡水题!水题都卡,绝对不是一个代码手的风范!3,不能出错,错一次即罚时20分钟,对于水题来讲是致命的!4,要能看出来一题是水题!没有这条,上面三条都是没有意义的! 如果你希望你成团队中一个合格的代码手,那么这套题是你最好的选择,快AC吧! 本系列即是为了提高水题代码手的素养而准备的!水题经常需要用到

动态规划01背包问题

动态规划0-1背包问题 ? 问题描写叙述: 给定n种物品和一背包.物品i的重量是wi,其价值为vi,背包的容量为C.问应怎样选择装入背包的物品,使得装 入背包中物品的总价值最大? ? 对于一种物品,要么装入背包,要么不装.所以对于一种物品的装入状态能够取0和1.我们设物品i的装入状态为xi,xi∈ (0,1),此问题称为0-11背包问题. 过程分析 数据:物品个数n=5,物品重量w[n]={0,2,2,6,5,4},物品价值V[n]={0,6,3,5,4,6}, (第0位,置为0,不參与计算,仅

动态规划——01背包问题

0-1 背包问题 给定 n 种物品和一个容量为 C 的背包,物品 i 的重量是 wi,其价值为 vi . 问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大? 面对每个物品,我们只有选择拿取或者不拿两种选择,不能选择装入某物品的一部分,也不能装入同一物品多次. 解决办法:声明一个 大小为 m[n][c] 的二维数组,m[ i ][ j ] 表示 在面对第 i 件物品,且背包容量为 j 时所能获得的最大价值 ,那么我们可以很容易分析得出 m[i][j] 的计算方法, (1). j <

hdu 4521 小明系列问题——小明序列 (间隔至少为d的LIS 两种解法)

先附上资源地址:http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html 进程(process)和线程(thread)是操作系统的基本概念,但是它们比较抽象,不容易掌握. 最近,我读到一篇材料,发现有一个很好的类比,可以把它们解释地清晰易懂. 1. 计算机的核心是CPU,它承担了所有的计算任务.它就像一座工厂,时刻在运行. 2. 假定工厂的电力有限,一次只能供给一个车间使用.也就是说,一个车间开工的时候,其他车间都必须停工

算法导论三剑客之 动态规划 0-1背包问题

1 #include "iostream" 2 using namespace std; 3 4 float MAX(float m1,float m2){ 5 if(m1>=m2) 6 return m1; 7 else 8 return m2; 9 } 10 11 float bag_Zero_One(int n,float v,float p[],float w[]){ 12 if(n==0||v==0) 13 return 0; 14 else{ 15 float m2;

POJ 1515 Street Directions --一道连通题的双连通和强连通两种解法

题意:将一个无向图中的双向边改成单向边使图强连通,问最多能改多少条边,输出改造后的图. 分析: 1.双连通做法: 双连通图转强连通图的算法:对双连通图进行dfs,在搜索的过程中就能按照搜索的方向给所有边定向,其中桥不能改造,只能保留双向边. 代码: #include <iostream> #include <cstdio> #include <cstring> #include <cmath> #include <cstdlib> #includ

动态规划——0-1背包问题

 0-1背包问题: 描述:给定n中物品和一背包.物品i的重量是wi,其价值为vi,背包的容量为c,问应如何选择装入背包中的物品,使得装入背包中的物品总价值最大? 0-1背包问题是一个特殊的整数规划问题. 设所给0-1背包问题的子问题; 其最优值为m(i,j),即m(i,j)是背包容量为j,可选择物品为i,i+1,-,n时0-1背包问题的最优值.由0-1背包问题的最优子结构性质,可以建立计算m(i,j)的递归式如下: NO1:递归实现 1 /* 2 *Description 递归实现 3 *设有一

求最大公约数的两种解法(欧几里得算法和素数分解)

最大公约数的两种解法(欧几里得算法和素数分解) 方法一: 欧几里得算法,又称辗转相除法 定理(欧几里得算法):设a和b是正整数,则存在最大求最大公因子d=(a,b)的一种算法,且存在求一组整数s,t使得d = sa+tb 举个例子:求168和60的最大公约数? 168 = 2 * 60 + 48 60  = 1 * 48 +12 48  = 4 * 12 由此得最大公约数为12 关于最大公倍数 C语言程序代码:很简单就不加注释了 #include<stdio.h> #define SWAP(a

POJ 3628 Bookshelf 2 0/1背包和DFS两种解法

题目链接:POJ 3628 Bookshelf 2 Bookshelf 2 Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 7462   Accepted: 3436 Description Farmer John recently bought another bookshelf for the cow library, but the shelf is getting filled up quite quickly,