经典算法之动态规划

一、引例

       先来看看生活中经常遇到的事吧——假设您是个土豪,身上带了足够的1、5、10、20、50、100元面值的钞票。现在您的目标是凑出某个金额w,需要用到尽量少的钞票。

       依据生活经验,我们显然可以采取这样的策略:能用100的就尽量用100的,否则尽量用50的……依次类推。在这种策略下,666=6×100+1×50+1×10+1×5+1×1,共使用了10张钞票。

  这种策略称为“贪心”:假设我们面对的局面是“需要凑出w”,贪心策略会尽快让w变得更小。能让w少100就尽量让它少100,这样我们接下来面对的局面就是凑出w-100。长期的生活经验表明,贪心策略是正确的。

       但是,如果我们换一组钞票的面值,贪心策略就也许不成立了。如果一个奇葩国家的钞票面额分别是1、5、11,那么我们在凑出15的时候,贪心策略会出错:
  15=1×11+4×1 (贪心策略使用了5张钞票)
  15=3×5 (正确的策略,只用3张钞票)
  为什么会这样呢?贪心策略错在了哪里?

       在这里我们发现,贪心是一种只考虑眼前情况的策略。

       贪心策略的纲领是:“尽量使接下来面对的w更小”。这样,贪心策略在w=15的局面时,会优先使用11来把w降到4;但是在这个问题中,凑出4的代价是很高的,必须使用4×1。如果使用了5,w会降为10,虽然没有4那么小,但是凑出10只需要两张5元。

       那么,现在我们怎样才能避免“鼠目寸光”呢?

       重新分析刚刚的例子。w=15时,我们如果取11,接下来就面对w=4的情况;如果取5,则接下来面对w=10的情况。我们发现这些问题都有相同的形式:“给定w,凑出w所用的最少钞票是多少张?”接下来,我们用f(n)来表示“凑出n所需的最少钞票数量”。

那么,如果我们取了11,最后的代价(用掉的钞票总数)是多少呢?
  明显cost = f(4)+1 = 4+1 = 5,它的意义是:利用11来凑出15,付出的代价等于f(4)加上自己这一张钞票。现在我们暂时不管f(4)怎么求出来。
  依次类推,马上可以知道:如果我们用5来凑出15,cost就是cost = f(10)+1 = 2+1 = 3 。

  那么,现在w=15的时候,我们该取那种钞票呢?当然是各种方案中,cost值最低的那一个

  - 取11:cost = f(4)+1 = 4+1 = 5
  - 取5:  cost = f(10)+1 = 2+1 = 3
  - 取1:  cost = f(14)+1 = 4+1 = 5

  显而易见,cost值最低的是取5的方案。我们通过上面三个式子,做出了正确的决策

  这给了我们一个至关重要的启示—— f(n) 只与 f(n-1) 、f(n-5) 、f(n-11) 相关;更确切地说:

        f(n) = min { f(n-1) 、f(n-5) 、f(n-11) } + 1

        这样,我们便在 O(n) 的复杂度下解决了这个问题。

        现在我们再来看一看它的原理。

       -  f(n) 只与f(n-1) 、f(n-5) 、f(n-11)相关。
  - 我们只关心 f(w) ,不关心是怎么凑出w的。

       这两个事实,保证了我们做法的正确性。它比起贪心策略,会分别算出取1、5、11的代价,从而做出一个正确决策,这样就避免掉了“鼠目寸光”!

       它与暴力的区别在哪里?我们的暴力枚举了“使用的硬币”,然而这属于冗余信息。我们要的是答案,根本不关心这个答案是怎么凑出来的。譬如,要求出f(15),只需要知道f(14),f(10),f(4)的值。其他信息并不需要。我们舍弃了冗余信息。我们只记录了对解决问题有帮助的信息——f(n).

  我们能这样干,取决于问题的性质:求出f(n),只需要知道几个更小的f(c)。我们将求解f(c)称作求解f(n)的“子问题”。

       这就是DP(动态规划,dynamic programming).

  将一个问题拆成几个子问题,分别求解这些子问题,即可推断出大问题的解

以上内容参考:
作者:阮行止
链接:https://www.zhihu.com/question/23995189/answer/613096905
来源:知乎

二、分析

       动态规划与分治法相似,都是通过组合原问题的解来求解原问题。不同的是,分治法将问题划分成互不相交的子问题,递归地求解子问题,再将它们的解组合起来,求出原问题的解。而动态规划应用于子问题重叠的情况,即不同的子问题具有公共的子子问题(子问题的求解都是递归进行的,将其划分成更小的子子问题)。在这种情况下,分治法会做许多不必要的工作,它会反复求解那些公共子子问题。而动态规划对每个子子问题只求解一次,将其保存在一个表格中,从而无需每次求解一个子子问题时都重新计算,避免了许多不必要的计算工作。

       动态规划问题通常用来求解最优化问题。这类问题可以有许多可行解,我们希望寻找其中的最优解(如最大或最小)。

       因此,分析一个问题是否为动态规划问题,主要是判断它是否具有动态规划的两个必要条件

     (1)重叠子问题

     (2)最优子结构

       设计动态规划算法的4个步骤:

       1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。

       2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。

       3)确定决策并写出状态转移方程:状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。

       (4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。

        其中,能否写出正确的状态转移方程,是整个动态规划算法能否正确的关键
        

        实际应用中可以按以下4个简化的步骤进行设计:

        1)刻画一个最优解的结构特征

        2)递归地定义最优解的值

        3)计算最优解的值,通常采用自底向上的方法

        4)利用计算出的信息构造一个最优解

三、经典问题

斐波那契数列

        现在要求输入一个整数n,请你输出斐波那契数列的第n项。n<=39
        斐波那契数列指的是这样一个数列:1、1、2、3、5、8、13、21、34、……
        在数学上,斐波纳契数列以如下被以递归的方法定义:F(0)=0,F(1)=1, F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)

        解决这一经典问题,有三种常用的解法:

(1)迭代法

    public int Fibonacci(int n) {
       if (n == 0) {
           return 0;
       }
       if (n == 1) {
           return 1;
       }
       int num1 = 0, num2 = 1;
       int result = 0;
       for (int i = 2; i <= n; i++) {
           result = num1 + num2;
           num1 = num2;
           num2 = result;
       }
       return result;
    }

(2)递归法

    public int Fibonacci(int n) {
        if (n == 0) {
            return 0;
        }
        if (n == 1) {
            return 1;
        }
        return Fibonacci(n - 1) + Fibonacci(n - 2);
    }

(3)动态规划法

    //递归写法
    public int Fibonacci(int n) {
      int f[100];
      if (n == 0) {
        return 0;
      }
      if (n == 1) {
        return 1;
      }
      f[n] = Fibonacci(n - 1) + Fibonacci(n - 2);
      return f[n];
    }

    //非递归写法
    public int Fibonacci(int n) {
      int f[100];
      int k = 2;
      if (n == 0) {
        return 0;
      }
      if (n == 1) {
        return 1;
      }
      while(k <= n) {
        f[k] = f[k-1] + f[k-2];
        k++;
      }
      return f[n];

       当然,还有一种比较小众的解法,一般不用,就是可以根据递推式,来解出通项公式。然后根据通项公式直接求出结果。

       由f(n) = f(n-1) + f(n-2), 易得通项公式为:

    public int Fibonacci(int n) {
       double sqrt5 = Math.sqrt(5);
       double root1 = (1 + sqrt5) / 2;
       double root2 = (1 - sqrt5) / 2;
       return (new Double((Math.pow(root1, n) - Math.pow(root2, n)) / sqrt5)).intValue();
    }
发布了35 篇原创文章 · 获赞 37 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_34519487/article/details/103918706