动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。
能采用动态规划求解的问题的一般要具有3个性质:
- 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理
- 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
- 有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。
动态规划一般都是使用表格法进行求解。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。
这里我已背包问题为例详细介绍如何利用动态规划解决实际问题:
0/1背包问题
假设有一个小偷,有一个承重容量为35kg的背包,可以盗窃的物品有三种,每种只有一个,包括:A(30kg,价值3000元)、B(20kg,价值2000元)、C(15kg,价值1500元)。怎么选择才能使装入背包的物品价值最高?
接下来我用表格演示动态规划过程,如下图:
- 第一行,考虑物品A。容量为15、20,25时,都无法装入背包,此时最大价值为0;当容量为30、35时可以装入,所以此时最大价值为3000元;
- 第二行,考虑物品A、B。容量为15时,都无法装入背包,此时最大价值为0;容量为20、25时,只能把物品B装入背包,此时价值最大为2000元;容量为30时,A与B都可以装入,但是装入A时价值更大,此时最大价值为3000元;容量为35时,A与B都可以装入,但是装入A时价值更大,此时最大价值为3000元;
- 第三行,考虑物品A、B、C。容量为15时,只能放入C,此时最大价值为1500元;容量为20时,B与C都可以放入,但是装入B时价值更大,此时最大价值为2000元;容量为25时,B与C都可以放入,但是装入B时价值更大,此时最大价值为2000元;容量为30时,A、B与C都可以放入,但是装入A时价值更大,此时最大价值为3000元;容量为35时,A、B与C都可以放入,但是同时装入B和C时价值更大,此时最大价值为3500元;
通过以上三步,我们就得到最大的方案,也就是把B和C同时放入背包时价值最大,最大为3500元。在表格推理过程中我们隐藏了一个细节,在计算每个单元格的 价值时,都会遵循一下公式:
当
前
最
大
价
值
=
max
{
当
前
容
量
对
应
的
上
一
行
单
元
格
的
价
值
当
前
物
品
的
价
值
+
剩
余
容
量
对
应
的
上
一
行
单
元
格
对
应
的
价
值
当前最大价值= \max \begin{cases} 当前容量对应的上一行单元格的价值\\ 当前物品的价值 + 剩余容量对应的上一行单元格对应的价值 {} \end{cases}
当前最大价值=max{当前容量对应的上一行单元格的价值当前物品的价值+剩余容量对应的上一行单元格对应的价值
这里我们用数学公式来表达,方便程序编写。用
g
i
g_i
gi表示物品种类,用
w
i
w_i
wi表示物品对应的重量,用
v
i
v_i
vi表示物品对应的价值。用
V
V
V表示当前表格,维度为
(
M
(M
(Mx
N
)
N)
N),M为物品的种类,N为容量划分数量(可以根据自己需要划分),
V
m
a
x
V_{max}
Vmax表示当前的最大值,
G
G
G表示背包容量,则用公式表示为:
V
[
m
]
[
n
]
=
max
(
V
[
m
−
1
]
[
n
]
,
v
[
m
]
+
V
[
m
−
1
]
[
G
−
g
[
m
]
]
)
V[m][n]=\max(V[m-1][n],\space\space v[m]+V[m-1][G-g[m]])
V[m][n]=max(V[m−1][n], v[m]+V[m−1][G−g[m]])
代码实现如下:
def package_1(weights, values, capacities):
"""
该函数是利用动态规划解决0/1背包问题
:param weights: 每个物品的重量
:param values: 每个物品对应的价值
:param capacities: 预先设定的容量变化列表
:return:
"""
table_value = [[ 0 for _ in range(len(capacities))] for _ in range(len(weights))]
# 填充表格第一行
for k in range(len(capacities)):
if weights[0] <= capacities[k]:
table_value[0][k] = values[0]
for i in range(1, len(weights)):
for j in range(len(capacities)):
if weights[i] < capacities[j]:
continue
# 假定放入第i个物品后,剩余容量对应的索引
ind = capacities.index(capacities[-1] - weights[i])
table_value[i][j] = max(table_value[i-1][j], values[i]+table_value[i-1][ind])
完全背包问题
假设有一个小偷,有一个承重容量为10kg的背包,可以盗窃的物品有三种,每种都不限数量,包括:A(3kg,价值30元)、B(2kg,价值20元)、C(1kg,价值15元)。怎么选择才能使装入背包的物品价值最高?
接下来我用表格演示动态规划过程,如下图:
填写步骤和0/1背包问题相似,唯一不同的地方就是可以选择不同数量的值,也就是考虑0-n个某种物品的重量,具体表格步骤就不想说了,情况太多,不好描述,直接看表吧(注:这里的最优以第一次出现的最优为准)。
在表格推理过程中我们隐藏了一个细节,在计算每个单元格的 价值时,都会遵循一下公式:
当
前
最
大
价
值
=
max
{
当
前
容
量
对
应
的
上
一
行
单
元
格
的
价
值
当
前
k
个
物
品
的
价
值
+
剩
余
容
量
对
应
的
上
一
行
单
元
格
对
应
的
价
值
当前最大价值= \max \begin{cases} 当前容量对应的上一行单元格的价值\\ 当前k个物品的价值 + 剩余容量对应的上一行单元格对应的价值 {} \end{cases}
当前最大价值=max{当前容量对应的上一行单元格的价值当前k个物品的价值+剩余容量对应的上一行单元格对应的价值
这里我们用数学公式来表达,方便程序编写。用
g
i
g_i
gi表示物品种类,用
w
i
w_i
wi表示物品对应的重量,用
v
i
v_i
vi表示物品对应的价值。用
V
V
V表示当前表格,维度为
(
M
(M
(Mx
N
)
N)
N),M为物品的种类,N为容量划分数量(可以根据自己需要划分),
V
m
a
x
V_{max}
Vmax表示当前的最大值,
G
G
G表示背包容量,
k
k
k表示当前物品选取的数量,则用公式表示为:
V
[
m
]
[
n
]
=
max
(
V
[
m
−
1
]
[
n
]
,
v
[
m
]
∗
k
+
V
[
m
−
1
]
[
G
−
g
[
m
]
∗
k
]
)
V[m][n]=\max(V[m-1][n],\space\space v[m]*k+V[m-1][G-g[m]*k])
V[m][n]=max(V[m−1][n], v[m]∗k+V[m−1][G−g[m]∗k])
代码实现如下:
def package_2(weights, values, capacities):
"""
该函数是利用动态规划解决0/1背包问题
:param weights: 每个物品的重量
:param values: 每个物品对应的价值
:param capacities: 预先设定的容量变化列表
:return:
"""
table_value = [[0 for _ in range(len(capacities))] for _ in range(len(weights))]
# 填充表格第一行
for k in range(len(capacities)):
num = capacities[k] // weights[k]
table_value[0][k] = num * values[0]
for i in range(1, len(weights)):
for n in range(1000):
# 提前终止
if n * weights[i] > capacities[-1]:
break
for j in range(len(capacities)):
if weights[i]*n < capacities[j]:
continue
# 假定放入第i个物品后,剩余容量对应的索引
ind = capacities.index(capacities[-1] - weights[i]*n)
table_value[i][j] = max(table_value[i - 1][j], values[i] + table_value[i - 1][ind])
多重背包问题
假设有一个小偷,有一个承重容量为10kg的背包,可以盗窃的物品有三种,每种物品数量都有限制,包括:A(3kg,价值30元,2个)、B(2kg,价值20元,3个)、C(1kg,价值15元,3个)。怎么选择才能使装入背包的物品价值最高?
接下来我用表格演示动态规划过程,如下图:
填写步骤和完全背包问题相似,唯一不同的地方就是物品的数量有限,也就是考虑0-所有个某种物品的重量,具体表格步骤就不想说了,情况太多,不好描述,直接看表吧(注:这里的最优以第一次出现的最优为准)。
在表格推理过程中我们隐藏了一个细节,在计算每个单元格的 价值时,都会遵循一下公式:
当
前
最
大
价
值
=
max
{
当
前
容
量
对
应
的
上
一
行
单
元
格
的
价
值
当
前
k
个
物
品
的
价
值
+
剩
余
容
量
对
应
的
上
一
行
单
元
格
对
应
的
价
值
当前最大价值= \max \begin{cases} 当前容量对应的上一行单元格的价值\\ 当前k个物品的价值 + 剩余容量对应的上一行单元格对应的价值 {} \end{cases}
当前最大价值=max{当前容量对应的上一行单元格的价值当前k个物品的价值+剩余容量对应的上一行单元格对应的价值
这里我们用数学公式来表达,方便程序编写。用
g
i
g_i
gi表示物品种类,用
w
i
w_i
wi表示物品对应的重量,用
v
i
v_i
vi表示物品对应的价值,用
s
i
s_i
si表示物品对应的最大数量。用
V
V
V表示当前表格,维度为
(
M
(M
(Mx
N
)
N)
N),M为物品的种类,N为容量划分数量(可以根据自己需要划分),
V
m
a
x
V_{max}
Vmax表示当前的最大值,
G
G
G表示背包容量,
k
k
k(
k
<
s
i
k<s_i
k<si)表示当前物品选取的数量,则用公式表示为:
V
[
m
]
[
n
]
=
max
(
V
[
m
−
1
]
[
n
]
,
v
[
m
]
∗
k
+
V
[
m
−
1
]
[
G
−
g
[
m
]
∗
k
]
)
其
中
:
k
<
s
[
m
]
V[m][n]=\max(V[m-1][n],\space\space v[m]*k+V[m-1][G-g[m]*k]) \space\space\space\space 其中:k < s[m]
V[m][n]=max(V[m−1][n], v[m]∗k+V[m−1][G−g[m]∗k]) 其中:k<s[m]
代码实现如下:
def package_3(weights, values, limits, capacities):
"""
该函数是利用动态规划解决0/1背包问题
:param weights: 每个物品的重量
:param values: 每个物品对应的价值
:param limits: 每个物品对应的数量
:param capacities: 预先设定的容量变化列表
:return:
"""
table_value = [[0 for _ in range(len(capacities))] for _ in range(len(weights))]
# 填充表格第一行
for k in range(len(capacities)):
num = capacities[k] // weights[k]
if num > limits[0]:
if k != 0:
table_value[0][k] = table_value[0][k-1]
else:
table_value[0][k] = num * values[0]
for i in range(1, len(weights)):
for n in range(limits[i]+1):
# 提前终止
if n * weights[i] > capacities[-1]:
break
for j in range(len(capacities)):
if weights[i]*n < capacities[j]:
continue
# 假定放入第i个物品后,剩余容量对应的索引
ind = capacities.index(capacities[-1] - weights[i]*n)
table_value[i][j] = max(table_value[i - 1][j], values[i] + table_value[i - 1][ind])
代码还有很大的优化空间,后续会继续优化,另外我将结合一个实际工程案例,更深入的理解动态规划,敬请期待!