动态规划算法详解——三大基本要素、解题步骤、算法优化和例题详解

目录

  • 1动态规划思想
  • 2适用场景
  • 3动态规划的三大基本要素
  • 4解题步骤
  • 5例题分析
    • 5.1斐波拉契数列
      • 5.1.1递归法求解
      • 5.1.2动态规划求解
    • 5.2剑指offer 42 连续子数组的最大和
    • 3.1示例1:42.接雨水

1动态规划思想

动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题。
动态规划的过程是:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。

2适用场景

动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。

  • 最优化原理:假设问题的最优解所包括的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
  • 无后效性:即某阶段状态一旦确定。就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响曾经的状态。仅仅与当前状态有关。
  • 有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到(该性质并非动态规划适用的必要条件,可是假设没有这条性质。动态规划算法同其它算法相比就不具备优势)。

3动态规划的三大基本要素

动态规划简单来说就是,利用历史记录,来避免我们的重复计算。而这些历史记录,我们得需要一些变量来保存,一般是用一维数组或者二维数组来保存。下面我们先来讲下做动态规划题很重要的三大基本要素:

  • 确定状态和保存状态变量
    将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。最简单的就是用数组来保存当前的每一个状态,这个状态就是每个子问题的决策。
  • 确定决策并写出状态转移方程
    因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。
  • 确定边界条件
    确定边界条件其实就是跟递归的终止条件是类似的。给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。

4解题步骤

一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。
根据动态规划的三大基本要素可以设计解题步骤如下:

  • 状态定义: 每个状态的决策,存放每个状态的变量,
  • 状态转移方程: 当前状态与上一个状态之间的关系
  • 初始状态: 初始的状态或者边界条件

5例题分析

5.1斐波拉契数列

斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家莱昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:0、1、1、2、3、5、8、13、21、34、……
斐波那契数列以如下被以递推的方法定义:
F ( 1 ) = 1 , F ( 2 ) = 1 , F ( n ) = F ( n − 1 ) + F ( n − 2 ) ( n ≥ 3 , n ∈ N ∗ ) F(1)=1,F(2)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 3,n ∈ N^*) F(1)=1F(2)=1,F(n)=F(n1)+F(n2)n3nN

5.1.1递归法求解

由上篇文章递归算法递归算法详解——递归算法的三要素以及例题分析
.
可以写出递归形式的求解为

class Solution {
    private final int model = 1000000007;
    public int fib(int n) {
        if (n < 2){
            return n;
        }
        return ((fib(n - 1) % model + fib(n - 2) % model )) % model;
    }
}

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。防止溢出。

若用递归法提交答案后可以看出会超出时间限制。
动态规划算法详解——三大基本要素、解题步骤、算法优化和例题详解_第1张图片
分析可以看出,在递归时会重复计算,如下所示,以F(6)为例:
动态规划算法详解——三大基本要素、解题步骤、算法优化和例题详解_第2张图片
复杂度分析

  • 时间复杂度分析: O ( n ) O(n) O(n).。最大递归次数是 n n n
  • 空间复杂度分析: O ( 1 ) O(1) O(1)。使用常数大小的额外空间。

5.1.2动态规划求解

  • 状态定义: d p dp dp 为一维数组,其中 d p [ i ] dp[i] dp[i] 的值代表 斐波那契数列第 i i i 个数字 。
  • 转移方程: d p [ i + 1 ] = d p [ i ] + d p [ i − 1 ] dp[i + 1] = dp[i] + dp[i - 1] dp[i+1]=dp[i]+dp[i1],即对应数列定义 f ( n + 1 ) = f ( n ) + f ( n − 1 ) f(n + 1) = f(n) + f(n - 1) f(n+1)=f(n)+f(n1)
  • 初始状态: d p [ 0 ] = 0 , d p [ 1 ] = 1 dp[0]=0, dp[1] = 1 dp[0]=0,dp[1]=1 ,即初始化前两个数字; 返回值: d p [ n ] dp[n] dp[n] ,即斐波那契数列的第 n n n 个数字。

空间复杂度优化
若新建长度为 n n n d p dp dp 列表,则空间复杂度为 O ( N ) O(N) O(N)

由于 d p dp dp 列表第 i i i 项只与第 i − 1 i−1 i1 和第 i − 2 i-2 i2 项有关,因此只需要初始化三个整形变量 s u m , a , b sum, a, b sum,a,b ,利用辅助变量 s u m sum sum 使 a , b a, b a,b 两数字交替前进即可 (具体实现见代码) 。
节省了 d p dp dp 列表空间,因此空间复杂度降至 O ( 1 ) O(1) O(1)

class Solution {
    public int fib(int n) {
        int a = 0, b = 1, sum;
        for(int i = 0; i < n; i++){
            sum = (a + b) % 1000000007;
            a = b;
            b = sum;
        }
        return a;
    }
}

复杂度分析

  • 时间复杂度分析: O ( n ) O(n) O(n).。最大循环次数是 n n n
  • 空间复杂度分析: O ( 1 ) O(1) O(1)。使用常数大小的额外空间。

5.2剑指offer 42 连续子数组的最大和

题目描述
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。

要求时间复杂度为 O ( n ) O(n) O(n)
示例1:

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

动态规划解析:

  • 状态定义: 设动态规划列表 d p dp dp d p [ i ] dp[i] dp[i] 代表以元素 n u m s [ i ] nums[i] nums[i]为结尾的连续子数组最大和。
    \qquad 为何定义最大和 d p [ i ] dp[i] dp[i] 中必须包含元素 n u m s [ i ] nums[i] nums[i] :保证 d p [ i ] dp[i] dp[i] 递推到 d p [ i + 1 ] dp[i+1] dp[i+1] 的正确性;如果不包含 n u m s [ i ] nums[i] nums[i] ,递推时则不满足题目的 连续子数组 要求。

  • 转移方程: d p [ i − 1 ] ≤ 0 dp[i-1] \leq 0 dp[i1]0,说明 d p [ i − 1 ] dp[i - 1] dp[i1] d p [ i ] dp[i] dp[i] 产生负贡献,即 d p [ i − 1 ] + n u m s [ i ] dp[i-1] + nums[i] dp[i1]+nums[i] 还不如 n u m s [ i ] nums[i] nums[i] 本身大。
    \qquad d p [ i − 1 ] > 0 dp[i - 1] > 0 dp[i1]>0 时:执行$ dp[i] = dp[i-1] + nums[i]$ ;
    \qquad d p [ i − 1 ] ≤ 0 dp[i - 1] \leq 0 dp[i1]0 时:执行 d p [ i ] = n u m s [ i ] dp[i] = nums[i] dp[i]=nums[i]

  • 初始状态: d p [ 0 ] = n u m s [ 0 ] dp[0] = nums[0] dp[0]=nums[0],即以 n u m s [ 0 ] nums[0] nums[0] 结尾的连续子数组最大和为 n u m s [ 0 ] nums[0] nums[0]

  • 返回值: 返回 d p dp dp 列表中的最大值,代表全局最大值。

class Solution {
    public int maxSubArray(int[] nums) {
        int res = nums[0];
        for(int i = 1; i < nums.length; i++) {
            nums[i] += Math.max(nums[i - 1], 0);
            res = Math.max(res, nums[i]);
        }
        return res;
    }
}

复杂度分析

  • 时间复杂度 O ( N ) O(N) O(N) : 线性遍历数组 n u m s nums nums 即可获得结果,使用 O ( N ) O(N) O(N) 时间。
  • 空间复杂度 O ( 1 ) O(1) O(1) : 使用常数大小的额外空间。

3.1示例1:42.接雨水

题目描述
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
动态规划算法详解——三大基本要素、解题步骤、算法优化和例题详解_第3张图片
上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
输入输出描述
输入: [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6
图解模型
动态规划算法详解——三大基本要素、解题步骤、算法优化和例题详解_第4张图片
直观思想

在暴力方法中,我们仅仅为了找到最大值每次都要向左和向右扫描一次。但是我们可以提前存储这个值。因此,可以通过动态编程解决。
算法流程

  • 找到数组从下标 i i i到最左端最高的条形块高度left_max
  • 找到数组从下表 i i i到最右端最高的条形块高度right_max
  • 扫描数组height并更新答案:
    \quad 累加min(max_left[i], max_right[i]) - height[i]到ans上

代码实现

    public int trap(int[] height) {
        int len = height.length;
        if (len < 2) return 0;
        int ans = 0;
        int[] max_left = new int[len];
        int[] max_right = new int[len];
        max_left[0] = height[0];
        for (int i = 1; i < len; i++) {
            max_left[i] = Math.max(height[i], max_left[i - 1]);
        }
        max_right[len - 1] = height[len - 1];
        for (int i = len - 2; i >= 0; i--) {
            max_right[i] = Math.max(height[i], max_right[i + 1]);
        }
        for (int i = 1; i < len - 1; i++) {
            ans += Math.min(max_right[i], max_left[i]) - height[i];
        }
        return ans;
    }

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n)。储存最大高度数组需要两次遍历,计算出ans结果遍历一次。
  • 空间复杂度: O ( n ) O(n) O(n)。使用2个n数组存放left_max和right_max数组。

你可能感兴趣的:(数据结构与算法,算法,动态规划,数据结构,java)