本博客主要是对Anany Levitin著、潘彦译的算法设计与分析基础(第三版)之第8章 动态规划进行代码梳理与讲解,代码语言主要为Python,其他语言也类似,可以把Python的解题方法看成是伪代码进行转换。
动态规划是数据结构中最重要的一环,也是面试中大家觉得头大的一类面试题型。关于动态规划的解题方法可以参考博主另一篇博文:一文详解动态规划【更新ing】。这篇博文的主要目的在于对书中的题目进行代码化。
例题1:币值最大化问题
这个是个经典题目了,也有多种变体,我们先看书中题目。题目是这个样子的:
给定一排
n
{n}
n个硬币,其面值均为正整数
c
1
,
c
2
,
.
.
.
,
c
n
{c_1, c_2, ..., c_n}
c1,c2,...,cn,这些整数并不一定两两不同。请问如何选择硬币,使得其原始位置互不相邻的条件下,所选硬币总额最大。
该题是一个动态规划的典型题目,重点在于理解原始位置互不相邻及子问题的构建上。整体上,解决动态规划问题四步走:
(1)划分子问题
(2)构建状态转移
(3)确定初始状态
(4)循环迭代求解
我们思考如下:
1、题目理解:
互不相邻,我们可以理解为下标间隔2,即
1
,
3
,
5
,
.
.
.
,
2
n
+
1
{1, 3, 5,..., 2n+1 }
1,3,5,...,2n+1 或者
0
,
2
,
4
,
.
.
.
,
2
n
{0, 2, 4,..., 2n}
0,2,4,...,2n这种方式,原始位置一定互不相邻。
2、划分子问题
我们自顶向下思考这个问题,如果说我们对于已经选择好的硬币组合
f
(
n
)
f(n)
f(n),代表的含义为我们已经选择好下标到
n
n
n的硬币组合,此时的总额记为
f
(
n
)
f(n)
f(n),那么对于
f
(
n
)
f(n)
f(n)来说,它的选择计划对于最后一个硬币
c
n
c_n
cn有两个来源:
(1)选择硬币
c
n
{c_n}
cn,因题目要求选择的是互不相邻的位置,那么子问题就变成必须要求
f
(
n
−
2
)
f(n-2)
f(n−2)最大,此时
f
(
n
)
=
f
(
n
−
2
)
+
c
n
{f(n) = f(n-2) + c_n}
f(n)=f(n−2)+cn
(2)不选择硬币
c
n
{c_n}
cn,此时子问题变成—>如何在
c
1
,
c
2
,
.
.
.
,
c
n
−
1
{c_1, c_2, ..., c_{n-1}}
c1,c2,...,cn−1中选择硬币,使得其要求总额最大。即要求
f
(
n
−
1
)
f(n-1)
f(n−1)最大就好。
3、构建状态转移
基于(1)、(2),对于
f
(
n
)
f(n)
f(n)整体上来说,总额最大的关系表达式为:
f
(
n
)
=
m
a
x
(
f
(
n
−
2
)
+
c
n
,
f
(
n
−
1
)
)
{f(n) = max( f(n-2) + c_n, f(n-1))}
f(n)=max(f(n−2)+cn,f(n−1))
完成了动态规划解题第一步划分子问题和第二步构建状态转移。
3、确定初始状态
这里初始状态有两个:
n
=
0
和
n
=
1
n=0和n=1
n=0和n=1这两个状态。
当
n
=
0
n=0
n=0时,表明此时硬币数组为空,不论如何选择,最大金额一定为0,即
f
(
0
)
=
0
f(0)=0
f(0)=0
当
n
=
1
n=1
n=1时,表明此时硬币数组只有一个元素,而且该元素为正整数。不论如何选择,最大金额一定为该元素,即
f
(
1
)
=
c
1
f(1)=c_1
f(1)=c1。
4、循环迭代求解
先设定一个长度为
n
n
n【为什么是
n
n
n而不是
n
+
1
n+1
n+1呢?欢迎评论区讨论~】的数组
a
r
r
arr
arr用来装一排
i
,
0
<
=
i
<
=
n
i, 0<=i<=n
i,0<=i<=n的硬币中选择互不相邻的硬币的最大总金额,那么从一排n个硬币中选择出互不相邻的硬币总金额
a
r
r
[
n
]
arr[n]
arr[n]。
class CoinSelection(object):
def __init__(self, n, arr_coin):
self.n = n
self.arr_coin = arr_coin
def solution(self):
if self.n == 0 or self.arr_coin is None:
return 0
else:
arr_solution = [0] * self.n
arr_solution[0] = 0
arr_solution[1] = self.arr_coin[0]
for coin_number in range(2, n, 1):
arr_solution[coin_number] = max(arr_solution[coin_number-2] + self.arr_coin[coin_number], arr_solution[coin_number - 1] )
return arr_solution[-1]
if __name__ == "__main__":
n = 6
coin_arr = [5, 1, 2, 10, 6, 2]
coin_solution = CoinSelection(n, coin_arr)
result = coin_solution.solution()
print(result)
此种接法的空间复杂度和时间复杂度均为 O ( n ) O(n) O(n)。
例题2:找零问题
需找零金额为
n
n
n,最少要用多少面值为
d
1
<
d
2
<
.
.
.
<
d
m
d_1<d_2<...<d_m
d1<d2<...<dm的硬币?假设
m
m
m种面值为
d
1
<
d
2
<
.
.
.
<
d
m
d_1<d_2<...<d_m
d1<d2<...<dm的硬币数量无限可得,
d
1
=
1
d_1=1
d1=1。
该题思考:
一开始这个题目博主理解有误,主要在对【找零金额】这四个字的理解上,博主开始理解成了找零钱,也就是数学中的余数,然后就纳闷:这个题目也没告诉博主需要付多少钱啊,零钱怎么找呢?懵逼了一圈后,看了解答发现,这里的找零金额是指总金额。后续解答讲解中博主都用总金额替换掉找零金额。。
假设
f
(
n
)
f(n)
f(n)表示选用最少的硬币组成的总金额为
n
n
n,上一个阶段为从
m
m
m个面值硬币中任意选一个,且组成硬币总额为
n
−
d
j
n-d_j
n−dj的所需硬币数最少,即
f
(
n
)
=
m
i
n
f
(
n
−
d
j
)
+
1
f(n) = min{f(n-d_j)} + 1
f(n)=minf(n−dj)+1。
初始边界条件:
f
(
0
)
=
0
f(0) = 0
f(0)=0,
f
(
1
)
f(1)
f(1)表示总金额为1,此时只能选择
d
1
d_1
d1,故而只有选择
d
1
d_1
d1这一种方式,因此
f
(
1
)
=
1
f(1) = 1
f(1)=1。
import numpy as np
class SumMoney(object):
def __init__(self, n, coin_value):
self.n = n
self.coin_value = coin_value
def solution(self):
coin_solution = [0] * (self.n + 1)
coin_solution[0] = 0
coin_solution[1] = 1
for sum_money in range(2, self.n + 1, 1):
tmp = np.inf
for single_coin_value_index in range(0, len(self.coin_value)):
if sum_money >= self.coin_value[single_coin_value_index]:
tmp = min(coin_solution[sum_money - self.coin_value[single_coin_value_index]], tmp)
coin_solution[sum_money] = tmp + 1
return coin_solution[self.n]
if __name__ == "__main__":
n = 6
coin_arr = [1, 3, 4]
coin_solution = SumMoney(n, coin_arr)
result = coin_solution.solution()
print(result)
此时的输出结果为:
2
Process finished with exit code 0
变体1:
输出例题2的具体最优解即最少的硬币组合方案中使用了哪些硬币。
寻找组合的过程与例题2的思路反着来,即先找到最少的硬币组合数,然后从最少硬币组合数的最后一个数字出发,看其所选硬币数,然后再往上反着推。
此题需要对例题2的每一步的最少硬币数用到的硬币进行存储,然后在找到最少硬币后反推着来。
变体2:对于每种面值的硬币限制其使用数量
例题3:最短路径问题
Ref:
1、硬币找零问题–动态规划参考
2、动态规划从入门到专家