暴力递归
1、把问题转化为规模缩小了的同类问题的子问题
2、有明确的不需要继续进行递归的终止条件
3、有当得到了子问题的结果之后的决策过程
4、不需要记录每一个子问题的解
动态规划
1、从暴力递归中来
2、将每一个子问题的解记录下来,避免重复计算(这是动态规划优于递归的本质原因)
3、把暴力递归的过程,抽象成了状态表达
4、并且存在化简状态表达,使其更加简洁的可能
开胃小菜,先来一道题感受一下如何暴力递归:
打印一个字符串的全部子序列,包括空字符串
例如abc的全部子序列为a,b,c,ab,bc,ac,abc和空字符串(注意子序列和子串的区别,子序列可以不连续,子串必须连续,前者去掉ac就是子串)。
暴力递归思路:还是以abc为例
对于每个位置,都有要和不要两种选择,穷尽所有位置即可得到答案,如下图
public static void printAllSubsquence(String str) {
char[] chs = str.toCharArray();
String res = "";
process(chs, 0, res);
}
public static void process(char[] str, int i,String res) {
if (i == str.length) {//终止条件,当i到达最后一个位置后打印结果
System.out.println(res);
return;
}
process(str, i + 1, res);//不要当前位置的值
process(str, i + 1, res+String.valueOf(str[i]));//要当前位置的值
}
运行结果(第一行是空串):
进入正题
给你一个二维数组,数组中的每一个数都是正数,要求从左上角走到右下角,每一步只能向右或者向下,沿途经过的数字要累加起来,返回最小的路径和。
举个例子,
1, 3, 0
2, 5, 1
7, 4, 2
很显然最小路径和=1+3+0+1+2=7
- 遇到这种题,首先我们应该先尝试写出它的暴力递归解法(最重要的一步)
- 其次再考虑改成动态规划
分析一下暴力递归的思路:
情况(1):当前点(i,j)来到最后一列,此时只能向下走,即走到(i+1,j)
情况(2):当前点(i,j)来到最后一行,此时只能向右走,即走到(i,j+1)
情况(3):普遍情况,当前点可以选择向右或者向下,选择其中路径和较小的一个
有了思路,代码不一会儿就刷刷刷的写好了
public static int minPath1(int[][] matrix) {
return process1(matrix, 0, 0);//从左上角开始
}
//matrix为二维数组,从(i,j)出发,到达最右下角位置的最小路径和
public static int process1(int[][] matrix, int i, int j) {
//终止条件,当(i,j)到达最右下角是,返回最小和
if (i == matrix.length - 1 && j == matrix[0].length - 1) {
return matrix[i][j];
}
//情况(1),当(i,j)来到最后一列,只能往下走
if (j == matrix[0].length - 1) {
return matrix[i][j] + process1(matrix, i + 1, j);//沿途数值累加
}
//情况(2),当(i,j)来到最后一行,只能往右走
if (i == matrix.length - 1) {
return matrix[i][j] + process1(matrix, i, j + 1);
}
//情况(3),选择往右或往下中较小的一个
return matrix[i][j] + Math.min(process1(matrix, i, j + 1), process1(matrix, i + 1, j));
}
public static void main(String[] args){
int[][] m = { { 1, 3, 0}, { 2,5,1 }, { 7,4,2 } };
System.out.println("最小和是:"+minPath1(m));
}
运行一下,果然没错哈哈哈!!!!!
有了暴力递归,怎么改成动态规划呢?
这里要注意,不是所有的暴力递归问题都能改成动态规划,需要两个条件
(1)暴力递归问题存在大量重复计算
重复计算很容易理解,以这道题为例,
1, 3, 0
2, 5, 1
7, 4, 2
3位置要计算0位置和5位置
2位置要计算7位置和5位置
也就是说5位置及其之后所有位置都有存在着无用的重复计算,这是暴力递归递归效率不行的本质原因
(2)该问题属于**“无后效性”问题**
所谓无后效性问题是指,不管经过什么方法到达当前位置,当前位置到其所要到的位置的值是固定的,不受之前方法的影响。以这道题为例::
若当前位置是5位置,那么它可能是1->3->5或者1->2->5,但不管是从那一条路到达5位置的,5位置到右下角的最小路径和是固定不变的,始终等于5+1+2=8。
很显然这道题可以改成动态规划,分析一下思路:
- 怎么解决重复计算问题?第一思路是记录每一个位置到右下角的最小路径和,因此我们需要一张二维表dp,这张表与原二维表matrix一一对应,dp表中每一位置的数值就是其到右下角的最小路径和,这个时候,求左上角到右下角的最小路径和就变成了求dp表中左上角即(0,0)位置的值。
- dp表中的值怎么求?这时就需要用到前面写的暴力递归函数了,
- 首先看递归的终止条件,终止条件的位置的值不依赖其他位置的值,所以 dp右下角的值 = matrix右下角的值
matrix --------> dp(x表示未知)
1, 3, 0 --------> x, x, x
2, 5, 1 --------> x, x, x
7, 4, 2 --------> x, x, 2 - 递归函数最后一列只能向下走,所以dp最后一列的值也可以求,(最后一列)倒数第二个位置 = matrix对应位置的值 + dp向下位置的值
matrix --------> dp(x表示未知)
1, 3, 0 --------> x,, x,, x
2, 5, 1 --------> x,, x, ,1+2
7, 4, 2 --------> x, ,x,, 2
-递归函数最后一行只能向右走,所以dp最后一行的值也可以求,(最后一行)倒数第二个位置 = matrix对应位置的值 + dp向右位置的值
matrix --------> dp(x表示未知)
1, 3, 0 --------> x,, x,, x
2, 5, 1 --------> x,, x, ,1+2
7, 4, 2 --------> x, ,4+2,, 2
-同理,dp普遍位置的值 = matrix对应位置的值 + dp向右或向下位置中较小的值
matrix --------> dp(x表示未知)
1, 3, 0 --------> x, x, x
2, 5, 1 --------> x, 5+(1+2),1+2
7, 4, 2 --------> x,4+2, 2
最终的dp表如下:
7,6,3
10,8,3
13,6,2
(0,0)位置的值7就是我们要求的答案,动态规划代码如下:
public static int minPath2(int[][] matrix) {
if (matrix == null || matrix.length == 0 || matrix[0] == null || matrix[0].length == 0) {
return 0;
}
int row = matrix.length;
int col = matrix[0].length;
int[][] dp = new int[row][col];
dp[row-1][col-1] = matrix[row-1][col-1];//右下角的值
for (int i = row-2; i >= 0; i--) {//最后一列的值
dp[i][col-1] = dp[i + 1][col-1] + matrix[i][col-1];
}
for (int j = col-2; j >= 0; j--) {//最后一行的值
dp[row-1][j] = dp[row-1][j + 1] + matrix[row-1][j];
}
for (int i = row-2; i >= 0; i--) {//普遍位置的值
for (int j = col-2; j >= 0; j--) {
dp[i][j] = Math.min(dp[i + 1][j], dp[i][j + 1]) + matrix[i][j];
}
}
return dp[0][0];
}
public static void main(String[] args){
int[][] m = { { 1, 3, 0}, { 2,5,1 }, { 7,4,2 } };
System.out.println("最小和是:"+minPath1(m));
System.out.println("最小和是:"+minPath2(m));
}
终于写完了…。。。mdzz明明保存了草稿,打开发现不见了,所以又得手撸一遍。。。难受