动态规划题目特点:
- 计数类
- 求最大最小值类【最优策略】
- 求存在性类
1. 确定状态
确定问题的最后一步【确定最优解的最后一步】
找到最后一步前的所有可能:{A, B, C, …},并用其表示出最优解。— 寻找前一步与最后一步关系
2. 转移方程
寻找到的关系
3. 初始条件和边界情况
(1) 初始条件【什么时候停下来?最开始可以人为确定的值F(0)/F(1)–最小问题的最优解】
(2) 边界情况【实现时不要让数组容器越界】
4. 计算顺序
二维的:从左到右,从上到下
从小到大【确定当前问题必须确定子问题的情况】
从大到小【确定当前问题必须确定父问题的情况】
另外一种思路:
- 实现暴力递归方法。
- 在暴力搜索方法的函数中看看哪些参数可以代表递归过程。
[本质是利用申请的空间来记录每一个暴力搜索的计算结果,下次要用结果的时候直接使用,而不在进行重复计算的递归过程]
[动态规划规定每一种递归状态的计算顺序,依次进行计算] - 找到代表递归过程的参数之后,记忆化搜索的方法非常容易实现。
- 通过分析记忆化搜索的依赖路径,进而实现动态规划。
[整理各个状态的依赖关系,简单的容易得到的先计算。例如:二维的第一行第一列,复杂的利用简单的去计算] - 根据记忆化搜索方法改出动态规划方法,进而看看是否能化简,如果能化简,还能实现时间复杂度更低的动态规划方法。
一、最值型
package 动态规划;
/**
* 〈最值型〉<br>
* 有n种硬币,分别是2元,5元和7元,每种硬币都有足够多
* 如果用最少的硬币组合正好付清m元,不需要对方找钱
* [其中最少最大可以用动态规划]
*
* 1. 最后一步:最后一枚的硬币->ak, 除掉这枚硬币,前面硬币的面值加起来是27-ak
* 得到子问题,状态:f[i] 就是拼出ai所需的最少硬币
* 子问题:最后那枚硬币只可能是2,5,7
* 如果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)
* 状态f(x) = 最少用多少枚硬币拼出x
*
* 2.转移方程
* 对于任意x:
* f(27) = min(f(27 - 2) + 1, f(27-5) + 1, f(27-7) + 1)
*
* 3.初始化条件
* f[0] = 0
*
* 边界条件
*
*
* 4.计算顺序
* 计算f[1],f[2],...,f[27],由于f[x]需要用到f[x-2], f[x-5], f[x-7],所以是从前向后
*
*/
public class Solution1 {
// {2, 5, 7} total = 27
// A是零钱的种类 M是硬币个数
public int coinChange(int[] A, int M){
// java中的数组都是从0开始的
// 0……n:开的数组是[n+1]
// 0……n-1:开的数组是[n]
int[] f = new int[M + 1];
// 零钱的种类
int n = A.length;
// 初始化
f[0] = 0;
int i, j;
// f[1], f[2],...,f[27]
for(i = 1; i <= M; ++i){
f[i] = Integer.MAX_VALUE;
// 最后一个硬币A[j]
// f[i] = min{f[i-A[0]] + 1,..., f[i-A[n-1]] +1}
for (j = 0; j < n; ++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];
}
}
二、计数型
package 动态规划;
/**
* <计数型 br>
*题意:
* 给定m行n列的网格,有一个机器人从左上角(0,0)出发,每一次可以向右或者向下走一步
* 问有多少种不同的方式走到右下角?
*
* 1.最后一步:无论机器人用何种方式到达右下角,总有最后挪动的一步:向右或者向下
* 右下角坐标设为(m-1, n-1)
* 那么前一步机器人一定实在(m-2, n-1)或者(m-1, n - 2)
* 加入机器人有X种方式从左上角走到(m-2, n-1),有Y中方式从左上角走到(m-1, n - 2),则机器人
* 有X + Y种方式走到(m-1, n-1) [加法原理-- a.无重复 b.无遗漏]
*
* 子问题:
* 那么此时问题就变成了有多少种方式从左上角走到(m-2, n-1) 和 (m-1, n - 2)【这里有两个变量所以需要开一个二维数组】
*
* 状态:设f[i][j] 为机器人有多少种方式从左上角走到(i, j)--所开数组表示的意义
*
* 2.转移方程
* f[i][j] = f[i - 1][j] + f[i][j - 1]
*
* 初始条件
* f[0][0] = 1,机器人只有一种方式到左上角,就是不动。
*
* 边界情况
* i = 0 或j = 0, 则前一步只能有一个方向过来f[i][j] = 1:只有一种方式
*
* 3.计算顺序->由于刚刚好满足下一步计算的要求上边和左边的都计算过
* f[0][0] = 1
* 计算第0行: f[0][0], f[0][1], ..., f[0][n-1]
* 计算第1行: f[1][0], f[1][1], ..., f[1][n-1]
* ...
* 计算第m-1行: f[m-1][0], f[m-1][1], ...,f[m-1][n-1]
*
*/
public class Solution2 {
public static int uniquePaths(int m, int n){
// 1. 定义数组
// 由于只是从0 -> m-1, 0-> n - 1
int[][] f = new int[m][n] ;
// 2. 初始化
f[0][0] = 1;
// 第0行都只有一种方式
for (int i = 0; i < n; i ++){
f[0][i] = 1;
}
// 第0列也只有一种方式
for (int i = 0; i < m; i ++){
f[i][0] = 1;
}
// 3. 定义转移方程--计算顺序按照行算
for (int i = 0; i < m; ++i){ // 行
for (int j = 0; j < n; ++j){// 列
if (i != 0 && j != 0){
f[i][j] = f[i-1][j] + f[i][j-1];
}
}
}
return f[m-1][n-1];
}
public static void main(String[] args) {
System.out.println(uniquePaths(2, 4));
}
}
三、可行性型
package 动态规划;
/**
* 〈一句话功能简述〉<br>
* 〈可行性型〉
* 有n块石头分别在x轴的0,1,...,n-1位置
* 一只青蛙在石头0,想跳到石头n-1
* 如果青蛙在第i块石头上,它最多可以向右跳距离ai
* 问青蛙能否跳到石头n-1
* example:
* a=[2, 3, 1, 1, 4]
* True
* a=[3, 2, 1, 0, 4]
* False
* 1. 最后一步:如果青蛙能跳到最后一块石头n-1, 我们考虑它跳的最后一步
* 这一步是从石头i跳过来,i < n -1,这需要满足两个条件:
* -青蛙可以跳到石头i
* -最后一步不超过跳跃的最大距离:n-1-i <= ai [比较好看出来]
*
* 子问题:我们需要知道青蛙能不能跳到石头i(i < n - 1),而我们原来求青
* 蛙能不能跳到n-1
* 状态:设f[j]表示青蛙能不能跳到石头j---所开数组表示的意义
*
* 2.转移方程
* 设f[j]表示青蛙能不能跳到石头j, OR[遍历的时候只要有一个满足条件](0<=i<j):枚举上一跳到的石头i
* f[i] 青蛙能不能跳到石头i, i + a[i] > = j:最后一步不能超过ai
* f[j] = OR(0<=i<j)(f[i] AND i + a[i] > = j)
*
* 3.初始条件和边界情况
* 初始条件:f[0] = True,因为青蛙一开始就在石头0
* 由于遍历时限定了边界所以不会越界
*
* 4.计算顺序
* f[1], f[2],...,f[n-1]:由于计算f[n]需要用到f[0], f[1], ..., f[n-1],
* 所以是从左向右计算
* 答案是f[n-1]
*/
public class solution3 {
public static boolean exitPath(int[] n){
// 1.定义数组 -- 状态:f[j]表示能不能跳到石头j
boolean[] f = new boolean[n.length];
// 2.初始化条件
f[0] = true;
// 转移方程
for (int i = 1; i < n.length; ++i){
f[i] = false; // 默认条件
for (int j = 0; j < i; ++j){
// 能跳到石头j,并且能够从石头j跳到i
if (f[j] && n[j] + j >= i){
f[i] = true;
break;
}
}
}
return f[n.length-1];
}
public static void main(String[] args) {
int[] a = new int[]{3, 2, 1, 0, 4};
System.out.println(exitPath(a));
}
}