暴力递归转动态规划----以矩阵最小路径和问题展开

暴力递归
1、把问题转化为规模缩小了的同类问题的子问题
2、有明确的不需要继续进行递归的终止条件
3、有当得到了子问题的结果之后的决策过程
4、不需要记录每一个子问题的解

动态规划
1、从暴力递归中来
2、将每一个子问题的解记录下来,避免重复计算(这是动态规划优于递归的本质原因)
3、把暴力递归的过程,抽象成了状态表达
4、并且存在化简状态表达,使其更加简洁的可能

示例题目:

给你一个二维数组,二维数组中的每个数都是正数,要求从左上角走到右下角,每一步只能向右或者向下。沿途经过的数字要累
加起来。返回最小的路径和。

举个例子,
1, 3, 0
2, 5, 1
7, 4, 2
很显然最小路径和=1+3+0+1+2=7

思路1:

此题是经典动态规划方法。这里首先给出一个递归方法(尝试版),是比较容易想到和理解的,其思想:从左上角开始,在走的过程中无非以下情况之一:

1.当前位置是第一个元素所在位置,则此时只有两种选择,要么往下走,要么往右走,则问题就变成了当前元素值 + (往下元素走到右下角的路径和,往右元素走到右下角的路径和)取较小值。

2. 当前位置正好在最后一行,则此时只有一种选择,就是往右走;

3.当前位置正好在最后一列,则此时只有一种选择,就是往下走;

4.当前位置正好是右下角元素位置,则此时整体路径和加上当前元素值即可。

代码实现如下:

package com.gxu.dawnlab_algorithm8;

/**
 * 矩阵最小路径和问题
 * 
 * @author junbin
 *
 *         2019年7月12日
 */
public class MinPath {
	public static int minPath1(int[][] matrix) {
		return process1(matrix, 0, 0);
	}

	public static int process1(int[][] matrix, int i, int j) {
		if (i == matrix.length - 1 && j == matrix[0].length - 1) { //最后一个元素 
			return matrix[i][j];
		}
		if (i == matrix.length - 1) { //最后一行
			return matrix[i][j] + process1(matrix, i, j + 1);
		}
		if (j == matrix[0].length - 1) { //最后一列
			return matrix[i][j] + process1(matrix, i + 1, j);
		}
		int right = process1(matrix, i, j + 1);
		int down = process1(matrix, i + 1, j);
		return matrix[i][j] + Math.min(right, down); //两种情况,选路径和小的
	}

	// for test
	public static int[][] generateRandomMatrix(int rowSize, int colSize) {
		if (rowSize < 0 || colSize < 0) {
			return null;
		}
		int[][] result = new int[rowSize][colSize];
		for (int i = 0; i != result.length; i++) {
			for (int j = 0; j != result[0].length; j++) {
				result[i][j] = (int) (Math.random() * 10);
			}
		}
		return result;
	}

	public static void main(String[] args) {
		int[][] m = { { 1, 3, 5, 9 }, { 8, 1, 3, 4 }, { 5, 0, 6, 1 },
				{ 8, 8, 4, 0 } };
		System.out.println(minPath1(m));
		// System.out.println(minPath2(m));

		m = generateRandomMatrix(6, 7);
		System.out.println(minPath1(m));
		// System.out.println(minPath2(m));
	}
}

这是一个暴力递归过程,有大量的重复解,比如f(0,0)会考虑到往下的f(1,0)和往右的f(0,1),而f(0,1)和f(1,0)接下去都会考虑到f(1,1)这个位置。所以可以把f(1,1)存起来,递归时重复使用。这同样属于递归的优化过程。

接下来我们来看怎么将暴力递归转动态规划求解。

这里要注意,不是所有的暴力递归问题都能改成动态规划,需要两个条件:

(1)暴力递归问题存在大量重复计算。如上面所述f(1,1)就是一个重复计算。

(2)该问题属于“无后效性”问题。所谓无后效性问题是指,不管经过什么方法到达当前位置,当前位置到其所要到的位置的值是固定的,不受之前方法的影响。以这道题为例:不管前面通过哪条路径到达f(1,1),f(1,1)位置到右下角位置的过程是不产生任何影响的。(汉诺塔问题就属于有后效性问题,因为其要求打印所有过程,之前作出的选择必然影响后续的解过程;还有N皇后问题也是有后效性问题)

则很显然,上面这道题可以改为动态规划。

过程:(以上述例子为例)

  1. 怎么解决重复计算问题?第一思路是记录每一个位置到右下角的最小路径和,首先看有多少个可变参数,有多少个就需要多少维的表,这里的可变参数是i和j,因此我们需要一张二维表dp,这张表与原二维表matrix一一对应,dp表中每一位置的数值就是其到右下角的最小路径和,这个时候,求左上角到右下角的最小路径和就变成了求dp表中左上角即(0,0)位置的值(注意这是我们要求的终极目标)。
  2. dp表中的值怎么求?这时就需要用到前面写的暴力递归函数了,首先看递归的终止条件(base case),终止条件的位置的值不依赖其他位置的值,所以 dp右下角的值 = matrix右下角的值:

         matrix --------> dp(x表示未知)
         1, 3, 0 --------> x, x, x
         2, 5, 1 --------> x, x, x
         7, 4, 2 --------> x, x, 2

     3. 递归函数最后一列只能向下走,所以dp最后一列的值也可以求,(最后一列)倒数第二个位置 = matrix对应位置的值 + dp           向下位置的值,(最后一列)倒数第三个位置 = matrix对应位置的值 + dp向下位置的值:
         matrix --------> dp(x表示未知)
         1, 3, 0 --------> x, x, 0+1+2
         2, 5, 1 --------> x, x,1+2
         7, 4, 2 --------> x ,x, 2

     4. 递归函数最后一行只能向右走,所以dp最后一行的值也可以求,(最后一行)倒数第二个位置 = matrix对应位置的值 + dp向右位置的值,(最后一行)倒数第三个位置 = matrix对应位置的值 + dp向右位置的值:
         matrix --------> dp(x表示未知)
         1, 3, 0 --------> x, x, 0+1+2
         2, 5, 1 --------> x, x, 1+2
         7, 4, 2 --------> 7+4+2,4+2, 2

     5.同样,dp普遍位置的值 = matrix对应位置的值 + dp向右或向下位置中较小的值
        matrix --------> dp(x表示未知)
        1, 3, 0 --------> x, x, 0+1+2
        2, 5, 1 --------> x, 5+(1+2),1+2
        7, 4, 2 --------> 7+4+2,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));
	}

本文重点参考了博客《如何将暴力递归改为动态规划?

发布了61 篇原创文章 · 获赞 9 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_33204444/article/details/95655580