递归到动态规划- X-空间压缩技巧

空间压缩技巧的示例代码代码, LeetCode第64题

验证链接:力扣

package dataStructure.recurrence.practice;

/**
 * https://leetcode.cn/problems/minimum-path-sum/
 * Leecode第64题
 * 给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
 *
 * 说明:每次只能向下或者向右移动一步。
 * 输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
 * 输出:7
 * 解释:因为路径 1→3→1→1→1 的总和最小。
 * 示例 2:
 *
 * 输入:grid = [[1,2,3],[4,5,6]]
 * 输出:12
 *
 *
 * 提示:
 *
 * m == grid.length
 * n == grid[i].length
 * 1 <= m, n <= 200
 * 0 <= grid[i][j] <= 100
 */
public class MinPathSum {
    public static int min1(int[][] grid) {
        int rows = grid.length;
        int cols = grid[0].length;
        return process1(grid, rows - 1, cols - 1, 0, 0);
    }

    public static int minDp(int[][] grid) {
        int N = grid.length;
        int M = grid[0].length;
        //row和col的变化范围分别是0~N-1、0~M-1
        int[][] dp = new int[N][M];
        //从右下角开始走只需要走过当前的格子
        dp[N - 1][M - 1] = grid[N - 1][M - 1];
        //最后一行只能往右走
        for(int j = M - 2; j >= 0; j --) {
            dp[N - 1][j] = grid[N - 1][j] + dp[N - 1][j + 1];
        }
        //最后一列只能往下走
        for(int i = N - 2; i >= 0; i --) {
            dp[i][M - 1] = grid[i][M - 1] + dp[i + 1][M - 1];
        }
        //非最后一行和最后一列的普通情况
        for(int curRow = N - 2; curRow >=0; curRow --) {
            for(int curCol = M - 2; curCol >= 0; curCol --) {
                int p1 = grid[curRow][curCol] + dp[curRow][curCol + 1];
                int p2 = grid[curRow][curCol] + dp[curRow + 1][curCol];
                dp[curRow][curCol] =  Math.min(p1, p2);
            }
        }
        return dp[0][0];
    }

    public static int minDp2(int[][] grid) {
        int N = grid.length;
        int M = grid[0].length;
        //原来的二维数组:row和col的变化范围分别是0~N-1、0~M-1
        //每个位置依赖于自己下行同列(最后一列)或者同行下列(最后一行)或者二者的最小值(除了前两种情况)
        int[] dp = new int[M];
        //从右下角开始走只需要走过当前的格子
        dp[M - 1] = grid[N - 1][M - 1];
        //初始化最后一行的动态规划数组
        for(int j = M - 2; j >=0; j--) {
            //最后一行每个位置到右下角最小累加和都是当前位置的数字加上当前位置右边的动态规划格子的值
            dp[j] = grid[N-1][j] + dp[j+1];
        }

        //对于从倒数第二行到第一行进行dp数组的值替换(没一行执行完成的时候,dp[j]就是当前行的j列到右下角的累加和最小值)
        for(int i = N - 2; i >= 0; i--) {
            //没一行的最后一列没有选择,他只能是当前值+下一行同位置的值
            dp[M-1] = dp[M - 1] + grid[i][M-1];
            //其他位置是他右边和他下边的值取最小加上当前位置的值
            for(int j = M - 2; j >=0; j --) {
                //dp[j+1]是当前位置右边的值,因为从右到左更新,所以dp[j+1]已经根据dp和j+1更新过了
                int p1 = grid[i][j] + dp[j + 1];
                //dp[j]这个时候还是下一行同列的值
                int p2 = grid[i][j] + dp[j];
                //根据二者最小值赋值给当前行的dp[j]
                dp[j] = Math.min(p1, p2);
            }
        }

        //dp数组是常规动态规划解法的第一行的数据,取dp[0]就是原来的dp[0][0]
        return dp[0];
    }

    /**
     * 暴力递归版本,过于简单,没啥可解释的,从(curRow, curRow)到(targetRow, targetCol)的最小累加和(包括当前节点)
     * @param grid 原始数组
     * @param targetRow 目前的行
     * @param targetCol 目标的列
     * @param curRow 当前所在行
     * @param curCol 当前所在列
     * @return
     */
    public static int process1(int[][] grid, int targetRow, int targetCol, int curRow, int curCol) {
        //当前已经是目标点,直接返回当前节点值即可
        if(curRow == targetRow && curCol == targetCol) {
            return grid[curRow][curCol];
        }
        //最后一行,所有点只能往右走
        if(curRow == targetRow) {
            return grid[curRow][curCol] + process1(grid, targetRow, targetCol, curRow, curCol + 1);
        }
        //最后一列,所有点只能往下走
        if(curCol == targetCol) {
            return grid[curRow][curCol] + process1(grid, targetRow, targetCol, curRow + 1, curCol);
        }
        //非最后一行和最后一列的点,取下一步向右和下一步向下走到目标节点的最小值
        int p1 = grid[curRow][curCol] + process1(grid, targetRow, targetCol, curRow, curCol + 1);
        int p2 = grid[curRow][curCol] + process1(grid, targetRow, targetCol, curRow + 1, curCol);
        return Math.min(p1, p2);
    }

    public static void main(String[] args) {
        int[][] grid = {{1,2,3},{4,5,6}};
        int minPath = min1(grid);
        System.out.println(minPath);
        int minPathDp = minDp(grid);
        System.out.println(minPathDp);
        int minPathDp2 = minDp2(grid);
        System.out.println(minPathDp2);
    }
}

验证结果如下:

代码中的DP2方法是我们使用了空间压缩技巧的

public static int minDp2(int[][] grid) {
        int N = grid.length;
        int M = grid[0].length;
        //原来的二维数组:row和col的变化范围分别是0~N-1、0~M-1
        //每个位置依赖于自己下行同列(最后一列)或者同行下列(最后一行)或者二者的最小值(除了前两种情况)
        int[] dp = new int[M];
        //从右下角开始走只需要走过当前的格子
        dp[M - 1] = grid[N - 1][M - 1];
        //初始化最后一行的动态规划数组
        for(int j = M - 2; j >=0; j--) {
            //最后一行每个位置到右下角最小累加和都是当前位置的数字加上当前位置右边的动态规划格子的值
            dp[j] = grid[N-1][j] + dp[j+1];
        }

        //对于从倒数第二行到第一行进行dp数组的值替换(没一行执行完成的时候,dp[j]就是当前行的j列到右下角的累加和最小值)
        for(int i = N - 2; i >= 0; i--) {
            //没一行的最后一列没有选择,他只能是当前值+下一行同位置的值
            dp[M-1] = dp[M - 1] + grid[i][M-1];
            //其他位置是他右边和他下边的值取最小加上当前位置的值
            for(int j = M - 2; j >=0; j --) {
                //dp[j+1]是当前位置右边的值,因为从右到左更新,所以dp[j+1]已经根据dp和j+1更新过了
                int p1 = grid[i][j] + dp[j + 1];
                //dp[j]这个时候还是下一行同列的值
                int p2 = grid[i][j] + dp[j];
                //根据二者最小值赋值给当前行的dp[j]
                dp[j] = Math.min(p1, p2);
            }
        }

        //dp数组是常规动态规划解法的第一行的数据,取dp[0]就是原来的dp[0][0]
        return dp[0];
    }

标准的动态规划应该是和原数组的大小相同的,这个方法值使用了一个和列数相同的一维数组来做动态规划。

步骤如下:

(1)先把原来的dp[N-1][M-1](右下角的点到它自己的最小累加和,也就是grid右下角的节点值)填到dp[M-1]位置

(2) 根据右下角的值从右向左填满dp[M-2]~dp[0]

(3)依次从下往上填满每一行的值(最后一列等于它本身加上下一行同列,其他位置取同行列+1和同列行+1的最小值+它本身)

(4)最后dp数组代表的就是第一行的每个位置到右下角的最小累加和。0位置就是我们的解。

比如10000*4的矩阵,我们的dp数组只需要4个位置,对比原来和矩阵一样的40000个位置节省了很多空间。

反过来,如果矩阵是4*10000,我们就可以把行列反过来用,用长度为4的dp数组,而不是10000的长度。

这个技巧是可选技巧,加分项,不会也不影响面试。

可以使用空间压缩技巧的大概场景:某一行某一列的值只依赖本行或者上一行或者下一行的某个值

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值