小言_互联网的博客

动态规划从入门到放弃【1】

325人阅读  评论(0)

动态规划

分治法

分治法是将问题划分为互不相交的子问题,递归的求解子问题,再将它们的解组合起来,求出原问题的解。

动态规划

动态规划是应用于子问题重叠的情况,即不同的子问题具有公共的子子问题。

最简单的动态规划问题是斐波那契数列问题,它既是递归中的典型例子也是动态规划的典型例子,在斐波那契之后将介绍典型的01背包问题,从而进一步理解动态规划。

一、斐波那契数列

斐波那契数列的递归求解方法:

#递归方法
def fib(n):
    if n<=0: return 0
    if n==1:return 1
    return fib(n-1)+fib(n-2)

下面看看算法的执行流程,假如输入6,那么执行的递归树如下所示。

上面Fibonacci的例子,上面的递归的求解方式就是分治算法,由于它的子问题是相互相关的,此时利用分治法就做了很多重复的工作,它会反复求解那些公共子子问题。而动态规划算法对每一个子子问题只求解一次,将其保存在一个表格中,从而避免重复计算。

下面我们介绍两种方法来说明动态规划的算法:自顶向下的备忘录法和自底向上的方法

1.自顶向下的备忘录法

#自顶向下——备忘录算法
def memo(n):
    mydict={}
    return fib_memo(n,mydict)
def fib_memo(n,mydict):
    if n<=0: return 0
    if n==1:return 1
    if  n in mydict:
        return dict[n]
    else:
        value=fib_memo(n-1,mydict)+fib_memo(n-2,mydict)
        mydict[str(n)]=value
        return value

利用mydict字典来存放斐波拉契数列中的每一个值,由于是自顶往下递归,它还是会最先递归到mydict[3],从此刻开始在往上计算,然后依次保存计算结果在mydict字典中,避免了重复运算。

2.自底向上的方法

自顶而下的方式来计算最终的结果,还是有一个递归的过程。既然最终是从fib(1)开始算,那么直接从fib(1)计算不就得了,先算子问题,再算父问题。

我们从接触斐波拉契数列开始,就是递归的方式,弄到最后,发现还是直接来方便很多啊。

#自下向上——动态规划算法
def fib_dp(n):
    if n<=0: return 0
    if n==1:return 1
    a=1
    b=1
    for i in range(2,n):
        a,b=b,a+b
    return b

从上面的例子可以看到自顶向下的方式的动态规划其实包含了递归,而递归就会有额外的开销的;而使用自底向上的方式可以避免。看来斐波那契真是个好东西,递归的时候用它来入门,现在动态规划也是用它来入门。

二、01背包问题

动态规划则具有记忆性,通过填写表把所有已经解决的子问题答案纪录下来,在新问题里需要用到的子问题可以直接提取,避免了重复计算,从而节约了时间,所以在问题满足最优性原理之后,用动态规划解决问题的核心就在于填表,表填写完毕,最优解也就找到

最优性原理是动态规划的基础,最优性原理是指**“多阶段决策过程的最优决策序列具有这样的性质:不论初始状态和初始决策如何,对于前面决策所造成的某一状态而言,其后各阶段的决策序列必须构成最优策略”**。

判断该问题是否满足最优性原理,采用反证法证明:

假设(X1,X2,…,Xn)是01背包问题的最优解,则有(X2,X3,…,Xn)是其子问题的最优解,假设(Y2,Y3,…,Yn)是上述问题的子问题最优解,则理应有(V2Y2+V3Y3+…+VnYn)+V1X1 > (V2X2+V3X3+…+VnXn)+V1X1;

然而(V2X2+V3X3+…+VnXn)+V1X1=(V1X1+V2X2+…+VnXn),则有(V2Y2+V3Y3+…+VnYn)+V1X1 > (V1X1+V2X2+…+VnXn);

该式子说明(X1,Y2,Y3,…,Yn)才是该01背包问题的最优解,这与最开始的假设(X1,X2,…,Xn)是01背包问题的最优解相矛盾,故01背包问题满足最优性原理。

假设我们现有以下4个物品,背包容量为8,物品的体积和价值如下表所示:

2.1 背包问题的表示

  1. 把背包问题抽象化(X1,X2,…,Xn,其中 Xi 取0或1,表示第 i 个物品选或不选),Vi表示第 i 个物品的价值,Wi表示第 i 个物品的体积(重量);

  2. 建立模型,即求max(V1X1+V2X2+…+VnXn);

    约束条件,W1X1+W2X2+…+WnXn<capacity;

  3. 定义V(i,j):表示前 i 个物品最佳组合对应的价值,当前背包容量 j;

  4. 寻找递推关系式,面对当前商品有两种可能性:

    • 第一,包的容量比该商品体积小,装不下,此时的背包中的总价值与前i-1个的价值是一样的,即V(i,j)=V(i-1,j),相当于不考虑当前物品所得到的价值;

    • 第二,还有足够的容量可以装该商品,但装了也不一定达到当前最优价值,所以在装与不装之间选择最优的一个,即V(i,j)=max{V(i-1,j),V(i-1,j-w(i))+v(i)}

      其中V(i-1,j)表示不装,最终价值就是前面i-1的价值;

      V(i-1,j-w(i))+v(i) 表示装了第i个商品,背包容量减少w(i);用剩余的背包容量去装前面的物品,然后把总的价值加起来。

      1. j<w(i) V(i,j)=V(i-1,j)

      2. j>=w(i) V(i,j)=max{ V(i-1,j),V(i-1,j-w(i))+v(i) }

  5. 填表

    • 首先初始化边界条件,V(0,j)=V(i,0)=0;

  • 然后一行一行的填表

    1)i=1,j=1,w(1)=2,v(1)=3,有j<w(1),故V(1,1)=V(1-1,1)=0;

    2)又如i=1,j=2,w(1)=2,v(1)=3,有j=w(1),故V(1,2)=max{ V(1-1,2),V(1-1,2-w(1))+v(1) }=max{0,0+3}=3;

    3)如此下去,填到最后一个,i=4,j=8,w(4)=5,v(4)=6,有j>w(4),故V(4,8)=max{ V(4-1,8),V(4-1,8-w(4))+v(4) }=max{ V(3,8), V(3,3)+V(4)} =max{9,4+6}=10;​

转化为程序为:

import numpy as np

def solve(vlist,wlist,totalWeight,totalLength):
    resArr = np.zeros((totalLength+1,totalWeight+1),dtype=np.int32)
    for i in range(1,totalLength+1):
        for j in range(1,totalWeight+1):
            if wlist[i] <= j:
                resArr[i,j] = max(resArr[i-1,j-wlist[i]]+vlist[i],resArr[i-1,j])
            else:
                resArr[i,j] = resArr[i-1,j]
    return resArr[-1,-1]

if __name__ == '__main__':
    v = [0,3,4,5,6]
    w = [0,2,3,4,5]
    weight = 8
    n = 4
    result = solve(v,w,weight,n)
    print(result)

2.2 背包中物品的组成

表格填完,最优解即是V(number,capacity)=V(4,8)=10,但还不知道解由哪些商品组成,故要根据最优解回溯找出解的组成,根据填表的原理可以有如下的寻解方式:

  1. V(i,j)=V(i-1,j)时,说明没有选择第i 个商品,则回到V(i-1,j);

  2. V(i,j)=V(i-1,j-w(i))+v(i)实时,说明装了第i个商品,该商品是最优解组成的一部分,随后我们得回到装该商品之前,即回到V(i-1,j-w(i));

  3. 一直遍历到i=0结束为止,所有解的组成都会找到。

正如在上述例子中:

  1. 最优解为V(4,8)=10,而V(4,8)!=V(3,8)却有V(4,8)=V(3,8-w(4))+v(4)=V(3,3)+6=4+6=10,所以第4件商品被选中,并且回到V(3,8-w(4))=V(3,3);

  2. 有V(3,3)=V(2,3)=4,所以第3件商品没被选择,回到V(2,3);

  3. 而V(2,3)!=V(1,3)却有V(2,3)=V(1,3-w(2))+v(2)=V(1,0)+4=0+4=4,所以第2件商品被选中,并且回到V(1,3-w(2))=V(1,0);

  4. 有V(1,0)=V(0,0)=0,所以第1件商品没被选择;

到此,01背包问题已经解决,利用动态规划解决此问题的效率即是填写此张表的效率,所以动态规划的时间效率为O(numbercapacity)=O(nc),由于用到二维数组存储子问题的解,所以动态规划的空间效率为O(n*c);

参考文献:

https://www.cnblogs.com/Christal-R/p/Dynamic_programming.html

https://www.cnblogs.com/cthon/p/9251909.html


转载:https://blog.csdn.net/qq_33414271/article/details/100847219
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场