动态规划入门

Posted Cool Coding

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划入门相关的知识,希望对你有一定的参考价值。

什么是动态规划

动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。

20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。

1957年出版了他的名著《Dynamic Programming》,这是该领域的第一本著作。

动态规划与递归

动态规划是自底向上,递归树是自顶向下

为什么动态规划一般都脱离了递归,而是由循环迭代完成计算。

自顶向下

什么是自顶向下呢?我们用斐波拉基数列来举例,这应该是我编程接触递归碰到的第一个经典例子吧。斐波拉基数列规定任意项的值fib(i) = fib(i-1) + fib(i-2),第一项和第二项都是1,即斐波拉基数列前几项就是:1,1,2,3,5,8等等。

用递归来写就是

public int fib1(int n) {
    if (n == 1 || n == 2) {
      return 1;
    } else {
      return fib1(n - 2) + fib1(n - 1);
    }
  }

很显然的可以看出,递归是从顶部的目标值开始计算,向下嵌套计算前置项的值。

递归的实现看起来很优雅,实际上有很大的限制。计算的数值稍微大一点,基本上效率是很差的。

自底向上

同样我们用斐波拉基数列举例子。自底向上就是放弃递归,用循环迭代来完成。
实现代码如下:

public static int fib2(int n) {
    int[] c = new int[n];
    if (n == 1 || n == 2) {
      return 1;
    } else {
      c[0] = c[1] = 1;
      for (int i = 3; i <= n; i++) {
        c[i - 1] = c[i - 2] + c[i - 3];
      }
    }
    return c[n - 1];
  }

这样我们一个循环就计算出,同时保存到一个数组c里面,这也是带记忆的实现方式,可以大大减少我们的时间开销。动态规划里经常用到这种方式来保存原子事件的处理结果。

状态转移方程

所谓状态转移方程,其实就是找出事件,原子事件的关系,很像数学里面的数列关系。比如在斐波拉基数列中状态转移方程就是fib(i) = fib(i-1) + fib(i-2)。动态规划中最难的一步就是找到正确的状态转移方程,有了正确的转移方程我们只需要看着方程写迭代代码即可。

爬楼梯问题

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

我们可以想象,假如爬到第十层有多少种方法?很显然是爬到第十层的方法等于爬到第九层的方法加上爬到第八层的方法,即f(10) = f(9)+f(8)。通用之后就是f(i) = f(i-1) + f(i-2)。

最后这个问题类似求斐波拉基数列第n项,唯一区别就是斐波拉基数列第二项值是1,而这个题目是2,因为爬到第二层可以一次爬上去也可以爬两次一阶。

public int climbStairs(int n) {
    if (n == 1) {
      return 1;
    }
    if (n == 2) {
      return 2;
    }
    int[] dp = new int[n];
    dp[0] = 1;
    dp[1] = 2;
    for (int i = 3; i <= n; i++) {
      dp[i - 1] = dp[i - 2] + dp[i - 3];
    }
    return dp[n - 1];
  }

使用最小花费爬楼梯

数组的每个索引做为一个阶梯,第i个阶梯对应着一个非负数的体力花费值cost(索引从0开始)。

每当你爬上一个阶梯你都要花费对应的体力花费值,然后你可以选择继续爬一个阶梯或者爬两个阶梯。

您需要找到达到楼层顶部的最低花费。在开始时,你可以选择从索引为 0 或 1 的元素作为初始阶梯。

例如:

输入: cost = [10, 15, 20]

输出: 15

解释: 最低花费是从cost[1]开始,然后走两步即可到阶梯顶,一共花费15。

我们来看看状态方程是什么。首先注意的是比如数组有三个数字,有三个楼梯,但其实我们要爬到第四个楼梯才算爬完楼梯。

我们假定dp[i]为爬到i层(楼顶)所需要的耗费,那么有以下方程:

dp[i] = min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])

因为可以爬一层或者两层,因此我们需要爬到i-1层的消耗加上在i-1层的消耗和我们需要爬到i-2层的消耗加上在i-2层的消耗中的最小值作为最终结果。

  public int minCostClimbingStairs(int[] cost) {
    if (cost.length == 0) {
      return 0;
    }
    //多一层表示爬完楼梯
    int[] dp = new int[cost.length + 1];
    //题目规定可以从0或者1开始爬,因此到这两层的消耗是0
    dp[0] = 0;
    dp[1] = 0;
    for (int i = 2; i <= cost.length; i++) {
      dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
    }
    return dp[cost.length];
  }

小偷问题

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

例如:

输入: [1,2,3,1]

输出: 4

解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

根据题目意思来找状态方程,只不过这次的稍微复杂点,因为题目有选择我们可以偷或者不偷。

假定 dp[i]/1 为偷到第i户并且要偷第i户的价值那么有:

dp[i]/1 = dp[i-1]/0 + cost[i]

因为要偷第i户那么第i-1户就不能偷。

假如不偷第i户有方程:

dp[i]/0 = max(dp[i-1]/0,dp[i-1]/1)

不偷第i户,目前的价值就是偷到第i-1户的价值并且偷了第i-1户和偷到第i-1户的价值并且不偷第i-1户中的最大值。

最后我们再从dp[i]/1和dp[i]/0 找出最大值就是我们最后所需要的答案。

public int rob(int[] nums) {
    if (nums.length == 0) {
      return 0;
    }
    //不偷第i-1户
    int dp_i_1_0 = 0;
    //偷第i-1户
    int dp_i_1_1 = nums[0];

    for (int i = 1; i < nums.length; i++) {
      //偷第i户
      int dp_i_1 = nums[i] + dp_i_1_0;
      //不偷第i户
      int dp_i_0 = Math.max(dp_i_1_0, dp_i_1_1);

      //更新数据
      dp_i_1_0 = dp_i_0;
      dp_i_1_1 = dp_i_1;
    }
    return Math.max(dp_i_1_0, dp_i_1_1);
  }

结尾

动态规划不仅仅于此,还有诸如经典的背包问题等。

总结下来就是寻找状态方程,使用自底向上的迭代方式来解决问题。大家多尝试就有感觉了。