动态规划的介绍
动态规划算法最早是从控制论中发展过来的,在20世纪50年代由Bellman提出,许多看似NP的问题都可以巧妙地利用动态规划算法解决。对于一个实际问题,如何快速设计一个动态规划算法,很考验程序设计者的水平。对于动态规划设计的几个关键点,鄙人进行了总结。
如何判断是否使用动态规划算法?
适合用动态规划来解决的问题,都具有下面三个特点:最优化原理、无后效性、有重叠子问题。
(1)最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2)无后效性(马氏性):即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使看问题是否可以划分为几个相对独立的阶段,或者说整体的最优问题是否可以进行拆分。
以上三点用数学公式表示,就是看问题是否可以写出最优问题的递推方程,即:
f
k
(
s
k
)
=
o
p
t
u
k
∈
D
k
(
s
k
)
{
d
k
(
s
k
,
u
k
(
s
k
)
)
⊗
f
k
+
1
(
s
k
+
1
)
}
k
=
n
,
n
−
1
,
.
.
.
,
1
f_k(s_k)=\mathrm{opt}_{u_k\in D_k(s_k)}\{d_k(s_k,u_k(s_k))\otimes f_{k+1}(s_{k+1})\}\\ k=n,n-1,...,1
fk(sk)=optuk∈Dk(sk){dk(sk,uk(sk))⊗fk+1(sk+1)}k=n,n−1,...,1
其中,
f
k
(
⋅
)
f_k(\cdot)
fk(⋅)是第
k
k
k阶段的最优值函数,至于第
k
k
k个阶段的状态
s
k
s_k
sk有关;
D
k
(
⋅
)
D_k(\cdot)
Dk(⋅)代替一个决策集合,它也只与第
k
k
k个阶段的状
s
k
s_k
sk有关;
u
k
(
⋅
)
u_k(\cdot)
uk(⋅)是第
k
k
k个阶段可取的策略,它是该方程的优化变量,
u
k
(
s
k
)
u_k(s_k)
uk(sk)的写法表示它也只与状态
s
k
s_k
sk有关;
d
k
(
⋅
,
⋅
)
d_k(\cdot,\cdot)
dk(⋅,⋅)是从第
k
k
k阶段到第
k
+
1
k+1
k+1阶段所增加的损失,它由第
k
k
k阶段的状态
s
k
s_k
sk和在该阶段上所取的策略
u
k
u_k
uk有关;
⊗
\otimes
⊗表示该问题可以进行分割,该运算符通常取加法或者乘法,但是一定要满足一致最优性,也即
⊗
\otimes
⊗左右两部分都达到最优,连接后的整体也保持最优性;
s
k
+
1
s_{k+1}
sk+1可以通过
s
k
s_k
sk和
u
k
u_k
uk计算得到,即有状态转移方程
s
k
+
1
=
T
k
(
s
k
,
u
k
(
s
k
)
)
s_{k+1}=T_k(s_k,u_k(s_k))
sk+1=Tk(sk,uk(sk)).
设计动态规划算法的步骤?
1.对于一个动态规划问题,第一步划分阶段。2.状态和策略集的选取,这一步非常重要,如果可以选择一个好的状态表示方式,算法会变得非常精巧。当然这一步的状态不是唯一的取决于看问题的角度。3.考虑算法是采用递归+备忘录形式设计还是用自低向上的填表格形式设计,一般来说采用递归形式在写法上更符合对动态规划转移方程的直观理解(我也比较喜欢这样),特别是在状态的确定性不强时用自低向上的循环很难实现,因为栈底不可预见的时候,递归是无法有效地化为循环的。当然,从工程效率上来讲,循环当然是比递归快的。4.考虑边界条件,如果用递归方式编写,就要考虑递归终止条件和进一步递归的条件。如果是填表形式编写要考虑起始状态对应的最优值。
如何得到每一步的最优决策?
1.一种方法是分两步进行,第一步,动态规划方程求解得到
f
1
,
f
2
,
f
3
,
.
.
.
,
f
n
f_1,f_{2},f_{3},...,f_{n}
f1,f2,f3,...,fn的结果,然后再反推出
u
1
,
u
2
,
.
.
.
,
u
n
u_1,u_2,...,u_n
u1,u2,...,un。2.我们可以在求解过程中直接记录
f
k
(
s
k
)
f_k(s_k)
fk(sk)最优取值所对应的策略,随后输出即可。不过要注意的是动态规划的最优解不一定唯一。
算法实例
问题一
假设你有N元钱,现有M个物品(可重复选取)可以购买,现在需要恰好花光N元钱,并且要买最少的物品。设计程序,返回最少的物品数,如果没有匹配返回-1. (假设物品价格和拥有的钱数都是int类型)
实例:
钱数N=7,物品的价格向量为[1,2,3],则返回的是3。选取方法为:3,3,1或者2,2,3。
思考:
本题可以将购买物品的次数作为阶段,而拥有的钱数作为状态。可以写出最优迭代方程为:
f
k
(
s
k
)
=
max
{
min
u
k
∈
c
o
s
t
_
l
i
s
t
f
k
−
1
(
s
k
−
u
k
)
+
1
,
if
s
k
−
u
k
≥
0
−
1
,
if
s
k
−
u
k
<
0
or
f
k
−
1
(
s
k
−
u
k
)
=
−
1
f_k(s_k)=\max\begin{cases}\min_{u_k\in cost\_list}f_{k-1}(s_{k}-u_k)+1,\quad\text{if}\quad s_k-u_k\geq 0\\-1,\quad \text{if}\quad s_k-u_k<0\quad \text{or}\quad f_{k-1}(s_{k}-u_k)=-1 \end{cases}
fk(sk)=max{minuk∈cost_listfk−1(sk−uk)+1,ifsk−uk≥0−1,ifsk−uk<0orfk−1(sk−uk)=−1。迭代约束条件,当
s
k
−
u
k
>
0
s_k-u_k>0
sk−uk>0进入迭代,当
s
k
−
u
k
=
0
s_k-u_k=0
sk−uk=0时返回1,当
s
k
−
u
k
<
0
s_k-u_k<0
sk−uk<0时返回-1。
程序实现:
class Solution:
def __init__(self,cost):
self.memo=dict()
self.cost=cost
def dpfun(self,money):
cost=self.cost
temp_dp=[0]*len(cost)
for j in range(len(cost)):
if money-cost[j]==0:
return 1
elif money-cost[j]>0:
if money-cost[j] in self.memo:
#判断该状态是否在备忘录中,如果在就直接调用,如果不在就重新计算并记录
tmp=self.memo[money-cost[j]]
else:
tmp=self.dpfun(money-cost[j])
self.memo[money-cost[j]]=tmp
temp_dp[j]=tmp+1 if tmp>-1 else -1
else:
temp_dp[j]=-1
if max(temp_dp)>-1:
return min([x for x in temp_dp if x >-1])
else:
return -1
money=11
cost=[2,4,5]
a = Solution(cost)
print(a.dpfun(money))
#3
算法分析:
该图就是算法求解过程中的迭代过程,可以看到有很多重复的状态,因此利用备忘录是非常有效的。备忘录技术成功地将算法复杂度从
Ω
(
2
n
)
\Omega(2^n)
Ω(2n)降为了
n
3
n^3
n3.
输出决策过程的程序:
class Solution:
def __init__(self,cost):
self.memo=dict()
self.cost=cost
self.fmin=dict()
def dpfun(self,money):
cost=self.cost
temp_dp=[0]*len(cost)
for j in range(len(cost)):
if money-cost[j]==0:
self.fmin[money] = 1
return 1
elif money-cost[j]>0:
if money-cost[j] in self.memo:
#判断该状态是否在备忘录中,如果在就直接调用,如果不在就重新计算并记录
tmp=self.memo[money-cost[j]]
else:
tmp=self.dpfun(money-cost[j])
self.memo[money-cost[j]]=tmp
temp_dp[j]=tmp+1 if tmp>-1 else -1
else:
temp_dp[j]=-1
if max(temp_dp)>-1:
fmin_j=min([x for x in temp_dp if x >-1])
self.fmin[money]=fmin_j
return fmin_j
else:
self.fmin[money]=-1
return -1
def findstragy(self,money):
#第一步将每阶段的最优值转换为每阶段的状态,即每阶段剩余的钱数
if self.fmin[money]==-1:
print('No solution!')
else:
stage=dict()
stage[self.fmin[money]]=[money]
for k in range(self.fmin[money],1,-1):
for money in stage[k]:
for j in range(len(self.cost)):
if money - self.cost[j] in self.fmin.keys() and self.fmin[money - self.cost[j]] == self.fmin[money] - 1:
stage[k-1]=[]
stage[k-1].append(money - self.cost[j])
#通过每阶段的钱数,反推出购买的物品
#这里返回的是物品的索引值
strategy = dict()
for k in stage.keys():
strategy[k]=[]
if k==1:
for j in range(len(self.cost)):
if stage[k][0]== self.cost[j]:
strategy[k].append(j)
else:
for j in range(len(self.cost)):
if stage[k][0]-stage[k-1][0]==self.cost[j]:
strategy[k].append(j)
return strategy
money=13
cost=[2,4,5,2,7]
a = Solution(cost)
print(a.dpfun(money))
print(a.findstragy(money))
#这里输出了所有的解
#3
#{3: [4], 2: [0, 3], 1: [1]}
输出决策过程的程序2:
import math
class Solution:
def __init__(self,cost):
self.memo=dict()
self.cost=cost
self.strategy=dict()
def dpfun(self,money):
cost=self.cost
temp_dp=[0]*len(cost)
stragy_tmp=dict()
for j in range(len(cost)):
if money-cost[j]==0:
self.strategy[money]=cost[j]
return 1
elif money-cost[j]>0:
if money-cost[j] in self.memo:
#判断该状态是否在备忘录中,如果在就直接调用,如果不在就重新计算并记录
tmp=self.memo[money-cost[j]]
else:
tmp=self.dpfun(money-cost[j])
self.memo[money-cost[j]]=tmp
temp_dp[j]=tmp+1 if tmp>-1 else -1
stragy_tmp[temp_dp[j]]=cost[j]
else:
temp_dp[j]=-1
if max(temp_dp)>-1:
fmin_j=min([x for x in temp_dp if x >-1])
self.strategy[money]=stragy_tmp[fmin_j]#记录money状态下的
return fmin_j
else:
return -1
def findstragy2(self,money):
strategy=dict()
k=1
while money>0:
strategy[k]=[]
for j in range(len(self.cost)):
if self.strategy[money]==self.cost[j]:
strategy[k].append(j)
money=money-self.strategy[money]
k=k+1
return strategy
money=13
cost=[2,4,5,2,7]
a = Solution(cost)
print(a.dpfun(money))
print(a.findstragy2(money))