动态规划初入门

268 阅读7分钟

动态规划

动态规划(Dynamic Programming, DP)是运筹学的一个分支,是求解决策过程最优化的过程。

动态规划作为算法题目中比较经典,也是难题之一,曾困扰我非常久。每次刷题的时候,看到动态规划求解的题型,我都唯恐避之不及。但是一直逃避也不是办法,于是我打算重:fist:出击,迎难而上,克服恐惧!

什么是动态规划

经过我查找多篇博客,总结出动态规划的核心思想是:记录以前做过的操作,等到下一次要再使用时,可以不用重复操作。

我们可以把一类活动分成多个互相联系的各个阶段,在每一个阶段经过操作之后得到的结果记录下来,并使用到下一个阶段之中。举个例子,就是当要去计算1+1+1+1的结果时,可以先记录第一个1和第二个1相加的结果为2,然后再将2加上第三个1结果为3,然后再将3加上最后一个1结果为4,这样一来,就符合了动态规划的思想。很明显,动态规划是一个能够通过空间换取时间的算法,将结果储存起来,避免多次计算。

动态规划问题如何解决

想要解决动态规划问题,理解了上面的概念之后,就很好办了。

其实就是要把问题划分成一个个的小问题,然后找到每一个小问题的突破口,并将这些串联起来,最后将大问题解决!

其实网上动态规划的思路非常多,但终究还是需要分成了以上几点,我这里也是借鉴了博主吴师兄的文章

题目实战

接下来,我将分享一下,通过这个思路,如何解决leetcode上面的一些动态规划的问题。

  1. 这一题是leetcode的第70题,这个是一题非常经典的动态规划的题,是关于爬楼梯的,题目描述如下:
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
    每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
    注意:给定 n 是一个正整数。
    示例 1:
    输入: 2
    输出: 2
    解释: 有两种方法可以爬到楼顶。
    1.  1 阶 + 1 阶
    2.  2 阶
    示例 2:
    输入: 3
    输出: 3
    解释: 有三种方法可以爬到楼顶。
    1.  1 阶 + 1 阶 + 1 阶
    2.  1 阶 + 2 阶
    3.  2 阶 + 1 阶

看完题目之后,按照之前说的解题思路来,首先将问题分成若干个子问题,爬 n 阶楼梯,每次可以爬 1 或 2 个台阶,所以可以看成是从 n-1 阶爬上来的或者是从 n-2 阶爬上来的,而 n-1 阶可以看成是从 n-2 阶 或者是从 n-3 阶,n-2 阶可以看成是从 n-3 阶或者是 n-4 阶,以此类推,到最后就是算从第 0 阶楼梯开始爬。

然后就是找小问题的解决的出口,不管是从 n-1 阶开始还是从 n-2 阶开始,这两种情况都是不同的方法,是题目需要我们去计算的。所以到达 n 阶的方法个数是从起点到 n-1 的方法个数加上起点到 n-2 阶的方法个数,所以根据上面的结论,每一阶的方法个数都等于前两阶的方法个数之和(排除掉阶数为1的),所以能得出一个状态公式:dp[n] = dp[n-1]+dp[n-2],当 n=0 时,不需要爬楼梯,dp[0]=0。n=1 时,只需要且只有一种方法爬到第 1 阶,所以dp[1]=1。有了这两个基石,就能推算出每一阶台阶的次数了。

下面是实现代码

    public static int climbStairs(int n) {
        if (n == 1) { // 当台阶为1的时候,就只有一种方法
            return 1;
        }
        // 多申请一个空间,因为要加上台阶为0的情况
        int[] dp = new int[n + 1]; 
        dp[1] = 1;
        // 台阶2的情况特殊处理
        dp[2] = 2;
        for (int i = 3; i < n + 1; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
  1. 第二题是leetcode上的第121题,这一个题目我曾经在B站的校招题上遇到过,不过当时的我比较菜,没有做出来,如果当时做出来了,也许我现在也是小破站的一员了吧!下面来看看题目
    给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
    如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。
    注意:你不能在买入股票前卖出股票。
    示例 1:
    输入: [7,1,5,3,6,4]
    输出: 5
    解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
         注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
    示例 2:
    输入: [7,6,4,3,1]
    输出: 0
    解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

首先来分解问题,最大利润肯定是使用最大值减去最小值,且最大值必须在最小值左边,所以要满足这两个条件,我们就可以从左开始,第一天的值就是prices[0],这个时候存入一个卖出的变量,用以比较之后每一天的小的那一个值,buy = Math.min(prices[i], buy),从第二天开始就可以根据prices和buy来计算利润了,第一题的利润肯定是 0,第二天开始,如果第二天卖出的价格比buy大,则表示亏本,利润是负的,此时就选择不买,利润保持 0;如果比buy小,则表示可以卖出,并且利润是prices[1]-buy。这样就能产生一个公式price = Math.max(prices[i] - buy, price),表示每一天都卖,但都和之前的最大利润做比较,取大的那个。

下面是实现代码:

public static int maxProfit(int[] prices) {
    if (prices == null || prices.length == 0) {
        return 0;
    }
    int price = 0;
    int buy = prices[0];
    for (int i = 1; i < prices.length; i++) {
        buy = Math.min(prices[i], buy);
        price = Math.max(prices[i] - buy, price);
    }
    return price;
}
  1. 接下来这一题也曾经在某个公司的算法题遇到过,以此而见,算法对于面试来说真的很重要
    你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
    给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
    示例 1:
    输入:[1,2,3,1]
    输出:4
    解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
         偷窃到的最高金额 = 1 + 3 = 4 。
    示例 2:
    输入:[2,7,9,3,1]
    输出:12
    解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
         偷窃到的最高金额 = 2 + 9 + 1 = 12 。

这题的解题思路也能够非常清晰,我们可以使用一个数组来存储每次偷窃第 n 间房的最高金额。

遍历到第 n 间房的时候,只要判断偷与不偷的情况,比如偷第 1 间房的时候肯定是最高的,所以dp[0] = nums[0] (nums为每间房的金钱数)。

到了第 2 间房的时候,就判断是偷第 1 间划算还是偷第 2 间房划算,所以dp[1] = max(dp[0],nums[1])。

当到了第 3 间房的时候,就需要去判断是否偷第 3 间房,如果偷了第 2 间房,那就不能偷第 3 间,那么就要判断是只偷第 2 间房划算还是偷 1、3 间房划算,所以dp[2] = max(dp[0]+nums[2],dp[1])。

以此类推,就能得出一个状态方程:dp[i] = max(nums[i] + dp[i - 2], dp[i - 1]);

下面是代码实现:

public static int rob(int[] nums) {
    if (nums == null || nums.length == 0) {
        return 0;
    }
    if (nums.length == 1) {
        return nums[0];
    }
    int[] dp = new int[nums.length];
    dp[0] = nums[0];
    dp[1] = Math.max(nums[0], nums[1]);
    for (int i = 2; i < nums.length; i++) {
        dp[i] = Math.max(nums[i] + dp[i - 2], dp[i - 1]);
    }
    return dp[nums.length - 1];
}