• 动态规划及其案例应用


    Wikipedia definition: “method for solving complex problems by breaking them down into simpler subproblems”,维基的定义看起来和分治算法相似,通过将一个复杂的问题分解成更简单的子问题来进行求解,但是这些子问题相互之间是有联系的,而分治算法定义的子问题之间是无关的,动态规划的两个重要特征:

    1、重叠子问题,例如求解斐波那契数,f(n)=f(n-1)+f(n-2)中,f(n-1)和f(n-2)就有很多重叠子问题,其实f(n-1)包含了f(n-2),整个f(n-2)都是其重叠子问题

    2、最优子结构,在求解问题的最优解时,需要构建问题的最优解结构,如果这个问题的解结构包含子问题的最优解,则称该问题具有最优子结构。

    动态规划问题的解题步骤:

    1、定义子问题(Define subproblems)

    2、记录重复出现的子问题(Write down the recurrence that relates subproblems),构造解方程

    3、识别并计算基本状态(Recognize and solve the base cases)

    动态规划问题示例:

    1、数的组和问题

      给定一个数n,计算有多少种方式可以作为1,3,4的和,也就是1,3,4有多少种组和方式其和的结果为n。求解过程如下:

    a 定义子问题

      设D(n)为数字1,3,4组和后和为n的不同方法数

    b 构造解方程

      假设n=x1+x2+.....+xm;如果xm=1,则剩下的数据之和为n-1,设D(n-1)为1,3,4不同组和的方法数;同理,如果xm=3,则D(n-3)为1,3,4不同组和为n-3的方法数,同理D(n-4);因为xm可能为1,3,4,所以D(n)=D(n-1)+D(n-3)+D(n-4)

    c 计算基本情况解,定义基本问题解

      D(0)=1,如果n<0,则D(n)=0,同理可知:D(0)=D(1)=D(2)=1,D(3)=2(3=3,3=1+1+1 两种情况);以上定义完毕,根据以上描述,可以定义程序结构:

      def sumToN(n):

        if(n<=2);

          return 1;

        if(n==3):

          return 2;

        D[0]=D[1]=D[2]=1;

        D[3]=2;

        for(i=4;i<=n;i++):

          D[i]=D[i-1]+D[i-3]+D[i-4];

        return D[n];

    2、钢条切割最大收益问题

      《算法导论》中动态规划第一个示例章节中的问题,钢条的长度和价格对应关系用下图来表示,给定一个长度n的钢条,如何切割才能使其价值最大?

     猛地一下就去找最优结构然后构造最优解的结构太难了,先从简单入手,给定长度i,对应的最大收益r(i)可以从以下计算进行递推:

    i=1,r(i)=1;i=1时,只有这一种情况

    i=2,r(i)=?,需要分析了,如果进行切割了,则r(i)=r(1)+r(1),如果不进行切割,r(i)=5,显然前者r(i)=2,取较大者,r(i)=5

    i=3,r(i)=?,同样,如果进行切割了,第一次切割可能是1,也可能是2,所以r(i)=max{r(1)+r(2),r(2)+r(1)}=6,显然这个计算是有重叠的,如果不切割,      r(i)=8,取较大者,r(i)=8

    i=4,r(i)=?,用同样的方法进行分析,则r(i)=max{r(1)+r(3),r(2)+r(2),r(3)+r(1),p(i)},其中p(i)表示不切割时的价格

    由此,可以更一般地得出结论,对于长度n的钢条,其收益情况如下:

    r(n)=max{p(n),r(1)+r(n-1),r(2)+r(n-2),r(3)+r(n-3),....,r(n-1)+r(1)}

    这是一种描述方法,还有一种更加简单的描述方法:对于r(n),所有的价值组成有:

    不切割,直接返回p(n)

    先切割长度为1,剩下n-1的用相同的方法继续求解,此时返回p(1)+r(n-1)

    先切割长度为2,剩下n-2的用相同的方法继续求解,此时返回p(2)+r(n-2)

    ....

    取最大值,可以得出:r(n)=max{p(i)+r(n-i)},其中1<=i<=n

    此时,可以设计一个自顶向下的方法进行求解:

    cud-rod(p,n):

      if n==0 return 0

      q=-1

      for i=1 to n:

        q = max(q,p[i]+cud-rod(p,n-i)

      return q

    这个递归是有严重的重复计算的,f(n)=max{p(i)+f(n-i)},f(n)需要计算从f(1)到f(n-1),f(n-1)需要计算从f(1)到f(n-2).....

    为了不重复计算,可以用备忘机制,自底向上的备忘模式如下:

    memoized-cut-rod(p,n):

      r[n]={-1,-1,...}// create a array with n's -1

      return memoized-cut-rod-aux(p,n,r)

    memoized-cut-rod-aux(p,n,r):

      if r[n]>-1

        return r[n];// the value has been calculated already

      if n==0 

         q=0

      else q=-1

      for i=1 to n:

        q=max(q,p[i]+memoized-cut-rod-aux(p,n-i,r))

      r[n]=q;// store the calculated result

      return q; //return the result

    自顶向下的计算是使用倒推的方法进行的,符合递归定义

    自底向上版本则更为简单:

    bottom-up-cut-rod(p,n):

      r[n]={-1,-1,....};//create an array

      r[0]=0; //set initial value

      for j = 1 to n:

        q=-1

        for i=1 to j:

          q=max(q,p[i]+r[j-i])

        r[j]=q

      return r[n]

    上述方法返回了最大价值的结果,但是并没有记录切割方法,所以还需要进行切割方法的记录,可以扩展自底向上版本方法:

    extended-bottom-up-cut-rod(p,n):

      r[n]={-1,-1,.....} //用于记录价值

      s[n]={-1,-1,.....} //用于记录切割长度

      r[0]=0

      for j=1 to n

        q= -1

        for i=1 to j

          if q < p[i]+r[j-i]

            q=p[i]+r[j-i]

            s[j]=i

        r[j]=q

      return r and s

    然后就是打印最优解:

    print-cut-rod-solution(p,n):

      r,s = extended-bottom-up-cut-rod(p,n);

      while n>0:

        print(s[n])

        n=n-s[n]

    3、多米诺骨牌组和问题,http://poj.org/problem?id=2663,3*n的矩形,需要用2*1的长方形进行组和,求组和方式,这是一种3*12的组和:

    当n=2,也就是前两个位置的组和方式有3种,故而剩下的摆放方法的种类有3*f(n-2)中

     当n=4,出去n=2的组和,组和方式有2种:

      

     注意这里,n=4时,除了用n=2的组和方式之外(f(n-2)已经包含了),还有上述图中的两种,故而剩下的摆放方法有2*f(n-4)种,

    同理可以得知f(n-6),f(n-8)....也是两倍的关系,递推方程为:

    f(2)=3

    f(4)=3*f(2)+2*1

    f(6)=3*f(4)+2{f(2)+1}

    f(n)=3*f(n-2)+2{f(n-4)+...+f(0)},其中f(0)=1

    简化方程可以得出:f(n)=4*f(n-2)-f(n-4);根据方程进行程序设计:

    combinations(n):

      if n % 2 != 0 return 0;

      if n==0 return 1;

      if n==2 return 3;

      return 4*combinations(n-2)-combinations(n-4);

    上述方法存在重复计算,可以使用记录法简化计算:

    combinations-aux(n):

      p[n]={-1,-1,...};

      combinations-memo(n,p);

      return p[n];

    combinations-memo(n,p):

      if(n==0) p[n]=1;

      if(n==2) p[n]=3;

      if(p[n]!=-1) return p[n];

      p[n]=4*combinations-memo(n-2,p)-combinations-memo(n-4,p);

      return p[n];

    因为已经有了递推公式,所以可以用自底向上的方法求解:

    bottom-up-combinations(n):

      p[n]={}

      p[0]=1

      p[2]=3

      for i= 4 to n:

        p[i]=4*p[i-2]-p[i-4];

        i+=2

      return p[n]

    自底向上的求解好像更加简单了...... 

  • 相关阅读:
    [GO]使用bufio的文件读取方式
    php开发工程师面必问题
    一位资深php程序员在北京的面试30个题目
    GIT 分支管理:创建与合并分支、解决合并冲突
    linux下挂在u盘,移动硬盘的方法,转移服务器资料的时候,使用移动硬盘什么最方便了
    php后台对接ios,安卓,API接口设计和实践完全攻略,涨薪必备技能
    navicat 官方使用手册,中文版,快捷键大全
    Memcached之缓存雪崩,缓存穿透,缓存预热,缓存算法
    Memcache 笔记(2)
    Memcache笔记(1)
  • 原文地址:https://www.cnblogs.com/codeMedita/p/15317527.html
Copyright © 2020-2023  润新知