玩转动态规划DP算法

「这是我参与2022首次更文挑战的第13天,活动详情查看:2022首次更文挑战」。

动态规划可谓是大名鼎鼎,笔试面试中的高频考点,也是重点难点,动态规划类型题目灵活多变,难度系数也相对较高,往往我们做不好动态规划的题目就会与心仪的offer失之交臂,本篇文章我们就一起来研究一下动态规划算法。

基本概念

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

起源: 20世纪50年代初,美国数学家贝尔曼(R.Bellman)等人在研究多阶段决策过程的优化问题时,提出了著名的最优化原理,从而创立了动态规划。

特点: 动态规划在寻找有很多重叠子问题的情况的最佳解时有效。它将问题重新组合成子问题。为了避免多次解决这些子问题,它们的结果都逐渐被计算并被储存,从简单的问题直到整个问题都被解决。

动态规划能应用于符合以下条件的问题

  • 有最佳子结构:最佳子结构就是说局部最佳解能决定全域最佳解

  • 无后效性:某状态以后的过程不会影响以前的状态,只与当前状态有关。(没有后悔药,前边选定就不能变了)

  • 有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(非必要条件)

    若有重叠子问题,则可以自底向上从最终子问题向原问题逐步求解,缓存每一个子问题求得的结果,减少重复计算。

题目特点

动态规划题目考点:

  • 计数

    • 有多少种方式走到右下角
    • 有多少种方法选出k个数使和是Sum
  • 求最值

    • 从左上角到右下角路径的最大数字和
    • 最长上升子序列长度
  • 求存在性

    • 取石子游戏,先手是否必胜
    • 能不能选出k个数使得和是Sum

常见动态规划类型:

  • 坐标型
  • 序列型
  • 划分型
  • 区间型
  • 背包型
  • 最长序列型
  • 博弈型
  • 综合型

想要玩转动态规划就多刷题!!!

求解思路

动态规划问题一直都是笔试面试中的高频考点,那么我们该如何通过动态规划算法求解问题呢?

在动态规划的实践中很重要的就是递推关系初始条件和边界情况

所谓边界条件就是最简单的情况,所谓递推关系就是如果你已经知道最佳子结构的解法,再多给你一个,你怎么得到下一个解。

解题具体步骤如下:

  • 确定状态

    • 研究最优策略最后一步
    • 化为子问题
  • 转移方程

    • 更具子问题定义直接得到
  • 初始条件和边界情况

    • 细心,考虑周全,有些题没有边界但有初始条件
  • 计算顺序

    • 利用之前计算结果,提高效率

举例一:金币问题(最值型)

假设你有三种硬币,分别为面值2元、5元、7元,每种硬币足够多。

买一本书需要27元,如何用最少的硬币组合正好付清,不需要对方找钱?

关键点1: 我们不需要关心K-1枚硬币是如何拼成27-ak,甚至我们不知道ak和K,我们只能确定前边硬币拼出27-ak

关键点2: 最优策略,所以27-ak的硬币数一定是最少的,否则就不是最优策略

image.png

子问题:最少用多少枚硬币可以拼出27-ak 原问题:最少用多少枚硬币可以拼出27

简化定义,我们设状态F(x) = 最少硬币数

ak是2,f(27) = f(27 - 2) + 1(最后一枚硬币是2)

ak是5,f(27) = f(27 - 5) + 1(最后一枚硬币是5)

ak是7,f(27) = f(27 - 7) + 1(最后一枚硬币是7)

题意最小硬币数,f(27)=min(f(27 - 2) + 1,f(27 - 5) + 1,f(27 - 7) + 1)

递归求解:

public int fun(int X) {
    if(X == 0) return 0;
    // 结果+1会造成溢出,所以这里最大值-1
    int res = Integer.MAX_VALUE - 1;
    if(X >= 2) {
        res = Math.min(fun(X-2) + 1, res);
    }
    if(X >= 5) {
        res = Math.min(fun(X-5) + 1, res);
    }
    if(X >= 7) {
        res = Math.min(fun(X-7) + 1, res);
    }
    return res;
}
复制代码

动态规划:

转移方程:f(X)=min(f(X- 2) + 1,f(X - 5) + 1,f(X - 7) + 1)

初始条件和边界情况:f(0)=0

我们通过f(X-2)、f(X-5)、f(X-7)可以看出我们应该自底向上,这样我们求解f(X)时f(X-2)、f(X-5)、f(X-7)的值都以及求过了。

/**
 * 求解:最少多少枚硬币
 * 参数:A 是有多少元的硬币
 * 参数:M 是需要花多少钱
 */
public int funDP(int[] A, int M) {
    // 结果集
    int[] f = new int[M + 1];
    // 初始化
    f[0] = 0;
    // 计算
    for (int i = 1; i<= M; i++) {
        f[i] = Integer.MAX_VALUE;
        for (int j = 0; j < A.length; j++) {
            if (i >= A[j] && f[i - A[j]] != Integer.MAX_VALUE) {
                f[i] = Math.min(f[i - A[j]] + 1, f[i]);
            }
        }
    }
    // 结果处理
    if (f[M] == Integer.MAX_VALUE) {
       f[M] = -1;
    }
    return f[M];
}
复制代码

解题步骤:

  • 确定状态

    • 最后一步(最优策略中使用的最后一枚硬币ak)
    • 化成子问题(最少硬币拼出面值27-ak)
  • 转移方程

    f(X)=min(f(X- 2) + 1,f(X - 5) + 1,f(X - 7) + 1)

  • 初始条件和边界情况

    f(0) = 0

  • 计算顺序

    f(0)、f(1)、f(2)...f(X)

举例二:机器人多少种路线(计数型)

给定m行n列的网格,有一个机器人从左上角(0,0)出发,每一步可以向下或者向右走一步,有多少中不同方式从左上角走到右下角?

image.png

解题步骤:

  • 确定状态

    • 最后一步(最后一步只能是向下,或者向右)
    • 化成子问题(机器人有多少种方式从左上角走到(m-2,n-1)和(m-1,n-2))
  • 转移方程

    f[i][j] = f[i-1][j] + f[i][j-1]

  • 初始条件和边界情况

    f[0][0] = 1,i = 0或者 j = 0,前进一步只有一个方向,初始化为1

  • 计算顺序

    f(0)、f(1)、f(2)...f(X)

    public static int uniquePaths(int m, int n) {
        int[][] f = new int[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (i == 0 || j == 0) {
                    f[i][j] = 1;
                } else {
                    f[i][j] = f[i-1][j] + f[i][j-1];
                }
            }
        }
        return f[m-1][n-1];
    }
复制代码

举例三:青蛙过河(可能性型)

青蛙过河,需要跳过n个石头,石头的位置为0,1,...n-1位置

一直青蛙在石头0,想要跳到石头n-1,如果青蛙在第i块石头上,它最远可跳的距离为ai

问:青蛙能否跳到石头n-1

解题步骤:

  • 确定状态

    • 最后一步

      如果青蛙能够跳到最后一块石头n-1,我们考虑它跳的最后一步,从石头i跳过来,i<n-1,需要满足两个条件

      • 青蛙可以跳到石头i
      • 最后一步不超过跳跃的最大距离:n-1-i < ai
    • 化成子问题(青蛙能不能跳到石头i(i<n-1))

  • 转移方程

    f[j] = OR(0<=i<j)(f[i] AND i+a[i] >=j)

  • 初始条件和边界情况

    f[0] = true

  • 计算顺序

    f(0)、f(1)、f(2)...f(X)

    public boolean canJump(int[] A) {
        int n = A.length;
        boolean[] f = new boolean[n];
        f[0] = true;
​
        for (int j = 1; j < n; j++) {
            f[j] = false;
            for (int i = 0; i < j; i++) {
                if (f[i] && i + A[i] >= j) {
                    f[j] = true;
                    break;
                }
            }
        }
        return f[n-1];
    }
复制代码

猜你喜欢

转载自juejin.im/post/7061566389894512671