「这是我参与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的硬币数一定是最少的,否则就不是最优策略
子问题:最少用多少枚硬币可以拼出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)出发,每一步可以向下或者向右走一步,有多少中不同方式从左上角走到右下角?
解题步骤:
-
确定状态
- 最后一步(最后一步只能是向下,或者向右)
- 化成子问题(机器人有多少种方式从左上角走到(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];
}
复制代码