LeetCode刷题之动态规划算法

作者: 奔跑吧李博 | 来源:发表于2020-09-23 14:07 被阅读0次
动态规划(英语:Dynamic programming,简称 DP)

是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。

动态规划往往用于优化递归问题,例如斐波那契数列,如果运用递归的方式来求解会重复计算很多相同的子问题,利用动态规划的思想可以减少计算量。斐波那契数列 0,1,1,2,3,5,8,13,…有着一个相当简单的描述方式,它的每个数字都与前两个紧邻的数字相关。如果 F(n) 是第 n 个数字,那么我们会有 F(n) = F(n-1) + F(n-2)。这个在数学上称作递归方程或者递推关系。为了计算后面的项,它需要前面项的计算结果作为输入。

解决方案
自上而下:

你从最顶端开始不断地分解问题,直到你看到问题已经分解到最小并已得到解决,之后只用返回保存的答案即可。这叫做记忆存储(Memoization)。

自下而上:

你可以直接开始解决较小的子问题,从而获得最好的解决方案。在此过程中,你需要保证在解决问题之前先解决子问题。这可以称为表格填充算法(Tabulation,table-filling algorithm**)。
至于迭代和递归与这两种方法的关系,自下而上用到了迭代技术,而自上而下则用到了递归技术。

动态规划、分治法、贪心算法异同点

相同点:

动态规划法与分治法和贪心法类似,它们都是将问题实例归纳为更小的、相似的子问题,并通过求解子问题产生一个全局最优解。

不同点:
  • 分治法

分治法中的各个子问题是独立的,利用子问题的解,合并成该问题的解。

  • 贪心算法

只有同一个问题,依赖于当前已经做出的所有选择。
自顶向下处理,每一步,根据策略得到一个当前最优解。传递到下一步,从而保证每一步都是选择当前最优的。

  • 动态规划

动态规划中的各个子问题是不独立的,动态规划任何一个i+1阶段都仅仅依赖 i 阶段的处理,而与i之前的选择无关。

自底向上处理,每一步,根据策略得到一个更小规模的问题。最后解决最小规模的问题,得到整个问题最优解。

121. 买卖股票的最佳时机

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。
注意:你不能在买入股票前卖出股票。

示例 1:

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

题解:
class Solution {
    public int maxProfit(int[] prices) {
        //思路:从最后一天往前遍历,看最后一天卖出能得到的最大利润,依次往前得出每天卖出的最大利润,取出最大值为最大利润,如果最大值还为负数,则
        //最大利润为0,不进行交易
        int maxProfit = 0;
        for (int i=prices.length-1;i>=1;i--) {  //从第二天到最后一天都可能是卖出的天数,但第一天不能卖,因为还没买

            for (int j=i-1;j>=0;j--) { //从卖出的前一天到第一天为可能买的日期,得出利润
                if (prices[i]-prices[j] > maxProfit) {
                    //利润有更大的需要更新
                    maxProfit = prices[i]-prices[j];
                }
            }
        }
        return maxProfit;
    }
}

70. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。

示例 1:

输入: 2
输出: 2

解释: 有两种方法可以爬到楼顶。

  1. 1 阶 + 1 阶
  2. 2 阶
题解1:
class Solution {

    public int climbStairs(int n) {
                    //思路:用动态规划思想,要爬到n阶,可以通过最后爬一步和两步2中爬法到,到n阶的方法等于到n-1阶方法加上n-2阶方法,即f(n)=f(n-1)+f(n-2),这个f(n)方法就用递归实现
        //此外,f(n)=f(n-1)+f(n-2),从n为3开始,该函数计算得出的是一个斐波那契数列,当前数为前两个数之和,所以有两种解法
        if (n < 3) {
            return n;
        }

        int a = 1;
        int b = 2;
        int cur = 0;
        for (int i=3;i<=n;i++) {
            cur = a + b;
            a = b;
            b = cur;
        }
        return cur;
    }

}
题解2:
class Solution {
     public int climbStairs(int n) {
        //思路:用动态规划思想,要爬到n阶,可以通过最后爬一步和两步2中爬法到,到n阶的方法等于到n-1阶方法加上n-2阶方法,即f(n)=f(n-1)+f(n-2),这个f(n)方法就用递归实现
        
        return getStepWay(n);
    }

    /**
     * 计算n级台阶不同走法
     * @param n
     * @return
     */
    public int getStepWay(int n) {
        if (n == 1) {
            return 1;
        }

        if (n == 2) {
            return 2;
        }

        return getStepWay(n-1) + getStepWay(n-2);
    }
}

1025. 除数博弈

爱丽丝和鲍勃一起玩游戏,他们轮流行动。爱丽丝先手开局。
最初,黑板上有一个数字 N 。在每个玩家的回合,玩家需要执行以下操作:
选出任一 x,满足 0 < x < N 且 N % x == 0 。
用 N - x 替换黑板上的数字 N 。
如果玩家无法执行这些操作,就会输掉游戏。

只有在爱丽丝在游戏中取得胜利时才返回 True,否则返回 False。假设两个玩家都以最佳状态参与游戏。

示例 1:

输入:2
输出:true
解释:爱丽丝选择 1,鲍勃无法进行操作。
示例 2:

输入:3
输出:false

解释:爱丽丝选择 1,鲍勃也选择 1,然后爱丽丝无法进行操作。

题解:
class Solution {
       /**
     * 思路:
     * 动态规划,先从dp[1],dp[2]
     * dp[n] 表示N=n时的结果,dp[n]类型为boolean类型 true为爱丽丝赢
     * 那么dp[1]=false,dp[2]=true
     * 然后往后推,如果i中有约数j,dp[i-j]=false,那么dp[i]=true
     * 如果i中,所有dp[i-j]没有为false情况,那么dp[i]=false;
     * 
     * 我们用DP来求解这个问题,首先new一个长度为N+1的数组,dp[i]表示i这个数是否可以赢,如果为true则N=i可以赢,为false则输。
     * N=1,爱丽丝就肯定会输,所以我们首先让dp[1]=false;
     * 然后我们从i=2开始,一直遍历到i=N
     * 按照题意,我们让j每次从1到i-1的区间里取数,且需要满足
     * 选出任一 x,满足 0 < x < N 且 N % x == 0 。
     * 这个条件,如果发现dp[j]=false,那么dp[i]就一定会赢。
     */
    public boolean divisorGame(int N) {
        boolean[] dp = new boolean[N+1];

        if (N == 1) {
            return false;
        }

        dp[1] = false;
        dp[2] = true;

        for (int i=3;i<=N;i++) {
            //i表示某次开始选择的数,用j表示下一次选的数,j为i的约数,所以j的范围为1到i/2
            dp[i] = false;
            for (int j=1;j<=i/2;j++) {
                if (i%j==0 && dp[i-j]==false) {
                    //下一次人选的为false,那么该人就胜了,为ture
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[N];
    }

        /**
     * 思路: 归纳法
     * 如果数是奇数,因为只用1与本身才能让余数为0,所以必然会让接下来的数变为偶数
     * 首先要明确的是,到谁是数为2,那么就是谁赢,所以如果刚开始数是偶数,那么每次就让爱丽丝选1,这样每次在奇数是就让鲍勃来选,再让数变为偶数,最后一定是到爱丽丝选时,数为2
     * 同理,如果数为奇数,那么到鲍勃时数为偶数,鲍勃也这么做就能取得胜利,所以最后判断胜利条件,就是数的奇偶性
     * @param N
     * @return
     */
    public boolean divisorGame(int N) {
        return N%2==0;
    }
}

53. 最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:

输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

题解:
class Solution {
    /**
     * 思路:
     * 状态定义: 设动态规划列表dp ,dp[i]代表以元素nums[i]为结尾的连续子数组最大和。
     *
     * ​我们发现dp[i]就是以数组下标为i的数做为结尾的最大子序列和,
     * 注意是以i为结尾,比如说现在有一个数组{6,-3,-2,7,-15,1,2,2},为了不搞,我们就下标以1开始,dp[3]就是以-2为结尾的,
     * 那么显然dp[3]的最大值就是1咯(6,-3,-2),dp[4]要以7结尾那么以7结尾的子序列最大和就是8(6,-3,-2,7)。
     *
     * ​ 知道dp[i]是啥后,现在我们开始细细品一下上面这个递推式,求dp[i]的时候是不是有两种可能,
     * 要么就是像上面的dp[4]一样,dp[3]求出来是1了,再加上自己array[4]是最大的,那么还有一种可能就是说如果dp[3]我求出来是-100,
     * 那如果我也是dp[3]+array[4]的话是-93,这时候dp[3]反而是累赘,最大就是自己(因为前面定义了必须以i为结尾,也就说必须以7结尾)。
     *
     * @param nums
     * @return
     */
    public int maxSubArray(int[] nums) {
        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        int max = dp[0];
        for (int i=1;i<nums.length;i++) {
            //如果i前一个数的子数组和小于0,就不要加上前面的累赘了,否则就加上
            dp[i] = nums[i] + Math.max(dp[i-1], 0);

            //遍历的同时获取dp数组中的最大值
            if (dp[i] > max) {
                max = dp[i];
            }
        }
        return max;
    }
}

相关文章

网友评论

    本文标题:LeetCode刷题之动态规划算法

    本文链接:https://www.haomeiwen.com/subject/pbmacktx.html