1.暴力递归求解问题的特点
- 把问题转化为规模缩小了的同类问题的子问题
- 有明确的不需要继续进行递归的条件
- 有当得到了子问题的结果之后的决策过程
- 不记录每一个子问题的解
2.动态规划求解问题的特点
- 1.从暴力递归中来
- 2.把每一个子问题的解记录下来,避免重复计算
- 3.把暴力递归的过程,抽象成了状态表达
- 4.并且存在化简状态表达,使其更加简洁的
3.例子:最短路径问题问题从暴力递归求解方式优化到动态规划
解法一:暴力递归方式求解最短路径,时间复杂度为O(2^N)
/**
* 最小路径和问题
* 给你一个二维数组,二维数组中的每个数都是正数,要求从左上角
* 走到右下角,每一步只能向右或者向下,沿途经过的数字要累加起来。
*
* 要求:返回最小的路径和
*
* 解法一:暴力递归方式
*/
public class MinPath {
public static int minPath(int[][] array){
if(array == null || array.length == 0 || array[0].length == 0){
throw new IllegalArgumentException("参数非法");
}
//从(0, 0)位置开始走,得到最终结果
return minPath(array, 0, 0 );
}
/**
* 暴力递归方式求解最短路径问题
* @param array 二维数组
* @param i 当前走到的行
* @param j 当前走到的列
* @return
*/
private static int minPath(int[][] array, int i, int j){
//当i的值为array.length - 1并且j的值为array[0].length - 1时表示走到了右下角
if(i == array.length - 1 && j == array[0].length - 1){
//走到了右下角则直接返回右下角的数值
return array[i][j];
}
//当i的值为array.length - 1并且j的值不为array[0].length - 1时,只能往右走
if(i == array.length - 1 && j != array[0].length - 1){
return array[i][j] + minPath(array, i ,j + 1);
}else if(i != array.length - 1 && j == array[0].length - 1){
//当i的值不为array.length - 1并且j的值为array[0].length - 1时,只能往下走
return array[i][j] + minPath(array, i + 1, j);
}
//否则既可以向下走也可以向右走,此时选取路径最短的那个
return array[i][j] + Math.min(minPath(array, i, j + 1), minPath(array, i + 1, j));
}
//测试函数
public static void main(String[] args) {
int[][] array = { { 3, 1, 0, 2 }, { 4, 3, 2, 1 }, { 5, 2, 1, 0 } };
System.out.println("最小路径和为 ;" + minPath(array));
}
}
运行结果:
此种通过暴力递归求解的方式很明显的一个缺点就是子问题被重复计算了多遍,时间复杂度为指数级,性能低下。所以我们开始尝试优化暴力递归的解法。
解法二:使用记忆化搜索的方式求解最短路径,时间复杂度为O(N^2)
import java.util.HashMap;
/**
* 最小路径和问题
* 给你一个二维数组,二维数组中的每个数都是正数,要求从左上角
* 走到右下角,每一步只能向右或者向下,沿途经过的数字要累加起来。
*
* 要求:返回最小的路径和
*
* 解法二:记忆化搜索方式
* 时间复杂度为O(N^2)
*/
public class MinPath1 {
public static int minPath(int[][] array){
if(array == null || array.length == 0 || array[0].length == 0){
throw new IllegalArgumentException("参数非法");
}
//从(0, 0)位置开始走,得到最终结果
return minPath(array, 0, 0);
}
//cache用于存放子问题的解,存放后,每次调用子问题时,先查看cache里是否存在子问题的解,没有再执行调用,
//有则直接使用而无需重复计算子问题
private static HashMap<String, Integer> cache = new HashMap<>();
public static int minPath(int[][] array, int i, int j){
int result = 0;
if(i == array.length - 1 && j == array[0].length - 1){
//走到了右下角则直接返回右下角的数值
result = array[i][j];
}else if(i == array.length - 1 && j != array[0].length - 1){
//当i的值为array.length - 1并且j的值不为array[0].length - 1时,只能往右走
int next = 0;//用于接收子问题(往右走)的执行结果
String nextKey = String.valueOf(i) + "_" + String.valueOf(j + 1);//子问题的路径
if(cache.containsKey(nextKey)){
//先查看缓存里是否存在子问题的解,有则直接使用,不用重复计算
next = cache.get(nextKey);
}else{
//缓存中没有子问题的解,则调用子问题进行计算
next = minPath(array, i, j + 1);
}
result = array[i][j] + next;
}else if(i != array.length - 1 && j == array[0].length - 1){
//当i的值不为array.length - 1并且j的值为array[0].length - 1时,只能往下走
int next = 0;//用于接收子问题(往下走)的执行结果
String nextKey = String.valueOf(i + 1) + "_" + String.valueOf(j);//子问题的路径
if(cache.containsKey(nextKey)){
//先查看缓存里是否存在子问题的解,有则直接使用,不用重复计算
next = cache.get(nextKey);
}else{
//缓存中没有则调用子问题进行计算
next = minPath(array, i + 1, j);
}
result = array[i][j] + next;
}else if(i != array.length - 1 && j != array[0].length - 1){
//否则既可以向下走也可以向右走,此时就有两个子问题需要计算,即向下走和向右走
//先计算向下走子问题的答案
int downNext = 0;//用于接收子问题(往下走)的执行结果
String downNextKey = String.valueOf(i + 1) + "_" + String.valueOf(j);//子问题的路径
if(cache.containsKey(downNextKey)){
//先查看缓存里是否存在子问题的解,有则直接使用,不用重复计算
downNext = cache.get(downNextKey);
}else{
//缓存中没有则调用子问题进行计算
downNext = minPath(array, i + 1, j);
}
//再计算向右走子问题的答案
int rightNext = 0;//用于接收子问题(往右走)的执行结果
String rightNextKey = String.valueOf(i) + "_" + String.valueOf(j + 1);//子问题的路径
if(cache.containsKey(rightNextKey)){
//先查看缓存里是否存在子问题的解,有则直接使用,不用重复计算
rightNext = cache.get(rightNextKey);
}else{
//缓存中没有则调用子问题进行计算
rightNext = minPath(array, i,j + 1);
}
//有了两个子问题的答案后,选取路径更短的那个
result = array[i][j] + Math.min(downNext, rightNext);
}
//先将此问题的解放入缓存以便减少计算,再将解返回
String key = String.valueOf(i) + "_" + String.valueOf(j);
cache.put(key, result);
return result;
}
//测试函数
public static void main(String[] args) {
int[][] array = { { 3, 1, 0, 2 }, { 4, 3, 2, 1 }, { 5, 2, 1, 0 } };
System.out.println("最小路径和为:" + minPath(array));
}
}
运行结果同暴力递归解法相同:
此种求解方式相比暴力递归方式有了时间复杂度上的优化,但由于不涉及依赖状态,所以还不是最优解。下面我们尝试用动态规划的方式求解最短路径问题
解法三:使用动态规划的方式求解最短路径
- 1.(先找解空间大小)先看函数中有哪些可变参数,哪些参数可以代表解的状态,也就是这些参数一旦确定,解一定是固定的。(若可变参数为2个则dp表为二维表,3个则为三维表,以此类推)
- 2.(确定"最终目标")看看最终要的状态是哪个。此问题中是(0, 0)状态
- 3. (dp表中"不被依赖的位置")回到base case,看看不被依赖的点是什么
- 4.然后看看普遍的i,j位置,看看它依赖哪些位置,怎么看?回到递归中
- 当我来到矩阵最后一行的时候,我只依赖我右边的位置,于是,dp表中,任何一个处在最底的位置,它只依赖它右边的位置,所以dp表的最后一行可以从右往左求,这样可以算出最后一行全部的值
- 当我来到矩阵最后一列的时候,我只依赖我下边的位置,于是,dp表中。任何一个处在最右的位置,它只依赖它下边的位置,所以dp表的最后一列可以从下网上求,这样可以算出最右一列全部的值
- 一个普遍的位置(既不是最右也不是最下)既依赖下边的位置又依赖右边的位置,于是,除去最后一行和最后一列,dp表中,剩下的空间,从右往左,从下往上,推出每一个位置的解,直到最顶的位置,就是我们要的答案,就像搭积木一样
/**
* 最小路径和问题
* 给你一个二维数组,二维数组中的每个数都是正数,要求从左上角
* 走到右下角,每一步只能向右或者向下,沿途经过的数字要累加起来。
*
* 要求:返回最小的路径和
*
* 解法三:动态规划方式
*/
public class MinPath2 {
public static int minPath(int[][] array){
if(array == null || array.length == 0 || array[0].length == 0){
throw new IllegalArgumentException("参数非法");
}
int row = array.length;//矩阵总行数
int col = array[0].length;//矩阵总列数
int[][] dp = new int[row][col];//dp表,在本体中可变参数为2个,所以dp表为二维表
dp[row - 1][col - 1] = array[row - 1][col - 1];//dp表中最不被依赖的点
/*
当我来到矩阵最后一行的时候,我只依赖我右边的位置,于是,
dp表中,任何一个处在最底的位置,它只依赖它右边的位置,所以
dp表的最后一行可以从右往左求,这样可以算出最后一行全部的值
*/
for(int i = col - 2; i >= 0; i--){
dp[row - 1][i] = array[row - 1][i] + dp[row - 1][i + 1];
}
/*
当我来到矩阵最后一列的时候,我只依赖我下边的位置,于是,
dp表中,任何一个处在最右的位置,它只依赖它下边的位置,所以
dp表的最后一列可以从下网上求,这样可以算出最后一列全部的值
*/
for(int j = row - 2; j >= 0; j--){
dp[j][col - 1] = array[j][col - 1] + dp[j + 1][col - 1];
}
/*
一个普遍的位置(既不是最右也不是最下)既依赖下边的位置又依赖右边的位置,于是,
除去最后一行和最后一列,dp表中,剩下的空间,从右往左,从下往上,推出每一个位置
的解,直到最顶的位置,就是我们要的答案,这个过程就像搭积木一样
*/
for(int i = row - 2; i >= 0; i--){
for(int j = col - 2; j >= 0; j --){
dp[i][j] = array[i][j] + Math.min(dp[i + 1][j], dp[i][j + 1]);
}
}
return dp[0][0];
}
//测试函数
public static void main(String[] args) {
int[][] array = { { 3, 1, 0, 2 }, { 4, 3, 2, 1 }, { 5, 2, 1, 0 } };
System.out.println("最小路径和为:" + minPath(array));
}
}
运行结果: