动态规划:DP导入
Posted 程序员Hotel
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划:DP导入相关的知识,希望对你有一定的参考价值。
动态规划
DP导入
动态规划不是一个算法,是一种解决问题的思路,简称DP。它要解决的是多阶段决策过程的最优化问题。它把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解。美国数学家Bellman在1957年出版名著《Dynamic Programming》。
比如,以前我们讲解Fibonacci数列的时候,讲过走楼梯的问题,一次可以走一等或者两等,求有多少种组合,我们的解题思路就是把阶梯N的走法化成了阶梯N-1加上阶梯N-2的走法之和。
还有,我们前面提到过的,生物信息学中经常要进行序列比对,也是用的动态规划,只不过当时我没有说出这个名字而已。
我们用一个例子导入动态规划。假定你去了一个游乐场,先拿钱换了一些游戏币,然后用游戏币买东西或者玩乐一下。这个游乐场不同币值的游戏币都是一样重的,大小也一样,只是币值不同,有1,有5,有10。换东西的时候,希望以尽可能少的币买东西。比如有个东西要值10个币,你一般用一个价值10的游戏币,而不会用10个价值1的游戏币。
一般的经验,是尽量先用大币值的币,如18元的东西,我们先用一个10元,剩下8,我们再用一个5元,剩下3,最后我们用三个1元。总共用了五个币。这种办法也叫贪心算法,经常是有效的。
但是会碰到无效的时候。比如,这个游乐场发的币为1,9,10。还是上面的例子,用贪心算法,先用一个10元币,剩下8,好家伙,这能再用八个1元币了,总共要用九个币。起始这种情况下,我们用两个9元币即可。这就是贪心算法本质上的问题,它只考虑当前这一刻。
好,有些人说了,那是不是要把每一种可能的组合计算一遍后才能得出答案呢?对于复杂一定的问题,这可是一个极其花时间的事情,大部分时候是不可行的。那我们看要怎样思考这个问题。
我们把三种不同的币值定义为A,B和C,分别对应币值10,9和1,记为w(A)=10,w(B)=9和w(C)=1。总数记为N,此例子中为18。
我们假定第一次用A币(币值为10),那么需要的币的个数f(N)=1+f(N-10),这种凑法,用了一个A币,再加上凑齐剩余的N-10=8的币个数。这是一个思维上的转变,我们不真的去计算每一种组合,只是把任务递推到第二阶段的小任务。
我们又假定第一次用B币(币值为9),那么需要的币的个数f(N)=1+f(N-9),这种凑法,用了一个B币,再加上凑齐剩余的N-9=9的币个数。
我们还要假定第一次用C币(币值为1),那么需要的币的个数f(N)=1+f(N-1),这种凑法,用了一个C币,再加上凑齐剩余的N-1=17的币个数。
现在我们手头有三种选择:
1,先用A币,f(N)=1+f(N-10),结果为1+8=9
2,先用B币,f(N)=1+f(N-9),结果为1+1=2
3,先用C币,f(N)=1+f(N-1),结果为1+8=9
所以我们要用第二种选择。
你看了可能还有一些疑惑,不对啊,怎么算f(N-10),f(N-9)和f(N-1)的值呢?这个例子简单,人手工可以计算,实际上不应该靠人来计算的。
这就是DP方法的思维的第二个转变,不直接求更小的任务的值,而是更关心本任务与递推出来的小任务之间有什么关系。通过上面的分析,我们现在知道了f(N)其实只与f(N-10),f(N-9),f(N-1)三者有关系。而这三者又可以再次用这个思路进行递推。
关系式:
f(N) = min(f(N-10),f(N-9),f(N-1)) + 1
这个关系式也叫状态转移方程。好,我们可以动手编程序了。
def f(n):
if n<9:
return n
if n==9:
return 1
if n==10:
return 1
s1 =f(n-10)+1
s2 =f(n-9)+1
s3 =f(n-1)+1
return min(s1,s2,s3)
测试一下f(18),得出了正确的值:2
上面使用得递归实现,更好的方式是用数组递推来实现,如下:
def f1(n):
c=[1,2,3,4,5,6,7,8,1,1]
if n<=10:
return c[n-1]
else:
i=10
while i<n:
s1 = c[i-10]+1
s2 = c[i-9]+1
s3 = c[i-1]+1
s = min(s1,s2,s3)
c.append(s)
i += 1
return c[n-1]
递推过程中,把前面的数都记录下来。这样比递归省时间,而递归式把过去计算的历史给遗忘了,数据量大的时候递归其实是不可行的。
本质上探究为什么DP会快?不仅仅是递归递推的原因,还有另外一个原因,就是如果用穷举法,需要计算所有可能的组合,而递推关系式,是在所有组合中选择了最优解的组合,用技术的话语,这叫对搜索空间的剪枝。DP其实就是在分析状态转移过程中进行了剪枝。
DP的基本模型:
对决策过程划分阶段。
对各阶段确定状态变量。
建立各阶段状态变量的转移过程,确定状态转移方程。
我们下面用更多的例子来理解DP。
这是一个简单而经常碰到的题目。初级的编程竞赛中也喜欢考。
假设有一个数字三角形,当然不是Pascal三角,里面的数是随机的,如:
12
9 4
1 10 15
7 8 2 6
13 3 5 14 11
我们要求从三角形顶上一步一步往下走,每次只能走正下方或者右下方的位置,求经过的路径的数字最大的和。
我们这么思考,假定走到了最后是[n,m]这个位置,那么它的前一个位置只有两个:正上方([n-1,m])和左上方([n-1,m-1])。这样我们得到关系式:
f(i,j)=max(f(n-1,m), f(n-1,m-1))+T[i,j]
有了关系式,这个程序不难:
def f2(arr,n): #arr为三角,n为总行数
sumarr=[ [0] * n for i in range(n)] #记录走到每一个位置的最大值
if n==1: #只有一行,直接返回
sumarr[0][0] = arr[n-1][n-1]
else: #多行
sumarr[0][0] = arr[0][0] #初始化第一行
i=0
for i in range(1,n):#第二行开始循环
for j in range(0,i+1): #一行内列循环
if j>0: #递推
sumarr[i][j]=max(sumarr[i-1][j],sumarr[i-1][j-1])+arr[i][j]
else:#左边界的递推
sumarr[i][j]=sumarr[i-1][j]+arr[i][j]
return sumarr
测试一下:
arr=[[12],[9,4],[1,10,15],[7,8,2,6],[13,3,5,14,11]]
print(f2(arr,5))
结果输出:
[[12, 0, 0, 0, 0],
[21, 16, 0, 0, 0],
[22, 31, 31, 0, 0],
[29, 39, 33, 37, 0],
[42, 42, 44, 51, 48]]
我按行输出了每一个位置的最大值。
有些人比较用心地看了代码,发现一个问题,我们的递推中,下一行的数其实只与上一行有关,其实我们不需要用这么大一个二维数组全部记录下来,只用记录两行就够了,这样省空间。
我们可以改造一下程序:
def f3(arr,n):
sumarr=[0]*n
newsumarr=[0]*n
if n==1:
sumarr[0] = arr[0][0]
newsumarr[0]=arr[0][0]
else:
sumarr[0] = arr[0][0]
newsumarr[0] = arr[0][0]
i=0
for i in range(1,n):#第二行开始循环
for k in range(0,n): #copy 上一行
sumarr[k]=newsumarr[k]
j=0
for j in range(0,i+1): #一行内列循环
if j>0:#递推
newsumarr[j]=max(sumarr[j],sumarr[j-1])+arr[i][j]
else:
newsumarr[j]=sumarr[j]+arr[i][j]
return newsumarr
测试一下:
print(f3(arr,5))
结果输出:
[42, 42, 44, 51, 48]
点击“阅读原文”,可进入原创书籍,阅读目录
以上是关于动态规划:DP导入的主要内容,如果未能解决你的问题,请参考以下文章
动态规划算法(Dynamic Programming,简称 DP)