动态规划——矩阵中的最短路径长度

文章出处:极客时间《数据结构和算法之美》-作者:王争。该系列文章是本人的学习笔记。

题目

假设我们有一个 n 乘以 n 的矩阵 w[n][n]。矩阵存储的都是正整数。棋子起始位置在左上角,终止位置在右下角。我们将棋子从左上角移动到右下角。每次只能向右或者向下移动一位。从左上角到右下角,会有很多不同的路径可以走。我们把每条路径经过的数字加起来看作路径的长度。那从左上角移动到右下角的最短路径长度是多少呢?

回溯法

从 (0, 0) 走到 (n-1, n-1),每一步都有向下或者向右2种选择方式。当走到 (n-1, n-1)停止。
写回溯代码中,递归函数f的参数,仅仅与状态有关,与状态无关的变量(例如n、m、grid)都作为类的实例变量保存起来。

public class MatrixShortestPath {
    private int n;
    private int m;
    private int min;
    private int[][] grid;
    public int minPathSum(int[][] grid) {
        if(grid==null || grid.length==0) return 0;
        n = grid.length;
        m = grid[0].length;
        this.grid = grid;
        min = Integer.MAX_VALUE;
        f(0,0,0);
        return min;
    }
    private void f(int i,int j,int currentSum){
        //System.out.println(i+" " +j);
        if(i==n-1 && j>=m || i>=n && j==m-1){
            min = Math.min(min,currentSum);
            return;
        }
        if(i>=n || j>=m) return;
        f(i,j+1,currentSum+grid[i][j]);
        f(i+1,j,currentSum+grid[i][j]);
    }
}

我们根据上面这个特殊的例子,把回溯求解问题的递归树画出来。

递归树的每一个节点表示一个状态,用(i,j,currentPathSum),表示当前将要处理第i行第j列的数据,在处理之前已经走过的路径长度是currentPathSum。

在递归树上能看到虽然(i,j,currentPathSum)重复的不存在,但是这道题目要求的是到达(n-1,n-1)的时候最短路径长度,所以在处理的时候只需要知道到达(i,j)的时候最短的路径长度是多少,其余节点就无需向下扩散了。例如f(1,2,9),f(1,2,5),f(1,2,3)。那只需要保留f(1,2,3),f(1,2,9),f(1,2,5)无需向下扩散。因为达到(1,2)节点之后,仍然是向右、向下走,与currentPathSum等于5、9,3都无关。这样就保证了节点不会指数级增长。

对递归树剪枝——加缓存

int[][] memo,memo[i][j]=currentSum。当调用f(1,2,3)的时候,如果memo[1][2]值为0 ,那就设置memo[1][2]=3,向下计算。当遇到f(1,2,9)的时候,发现memo[1][2]=3,而9>3,则不再计算。
代码的变化就是加了判断: m e m o [ i ] [ j ] = = 0 ∣ ∣ m e m o [ i ] [ j ] > c u r r e n t S u m memo[i][j]==0 || memo[i][j]>currentSum memo[i][j]==0memo[i][j]>currentSum

public class MatrixShortestPath {
    private int n;
    private int m;
    private int min;
    private int[][] grid;
    private int[][] memo;
    public int minPathSum(int[][] grid) {
        if(grid==null || grid.length==0) return 0;
        n = grid.length;
        m = grid[0].length;
        this.grid = grid;
        min = Integer.MAX_VALUE;
        memo = new int[n][m];
        f(0,0,0);
        return min;
    }
    private void f(int i,int j,int currentSum){
        //System.out.println(i+" " +j+" "+currentSum);
        if(i==n-1 && j>=m || i>=n && j==m-1){
            min = Math.min(min,currentSum);
            return;
        }
        if(i>=n || j>=m) return;
        if(memo[i][j]==0 || memo[i][j]>currentSum){
            memo[i][j] = currentSum;
            f(i,j+1,currentSum+grid[i][j]);
            f(i+1,j,currentSum+grid[i][j]);
        }
        
    }
}

对递归树剪枝——动态规划

状态转移表

接下来我们按照这种方式,计算状态转移表。我们画出一个二维状态表,表中的行、列表示棋子所在的位置,表中的数值表示从起点到这个位置的最短路径。注意:这里状态表的值的含义与递归树中的值的含义发生了变化。我们看到在递归树中到达最后一个位置,还需要currentPathSum+matrix[i][j]。但在表中是不需要的。
如果把表定义为int[][] dp ,dp[i][j]=到达(i,j)位置的最短路径。我们想要的返回值就是dp[n-1][m-1]。
我们按照决策过程,通过不断状态递推演进,将状态表填好。为了方便代码实现,我们按行来进行依次填充。

初始化这一步是很重要的。
对dp[0][0]=grid[0][0]。
对于第0行,大于0的列,只能从左侧的位置转移到当前位置:dp[0][j-1]+grid[0][j]。
对于第0列,只能从上面的位置转移到当前位置:dp[i][0]=dp[i-1][0]+grid[i][0]。

代码:

	public int minDistDP(int[][] matrix ,int n){
        int[][] states = new int[n][n];
        //第一行
        for(int j=0;j<n;j++){
            if(j==0){
                states[0][j] = matrix[0][j];
            }else{
                states[0][j] = states[0][j-1]+matrix[0][j];
            }

        }
        //第一列
        for(int i=1;i<n;i++){
            states[i][0] = states[i-1][0]+matrix[i][0];
        }

        for(int i=1;i<n;i++){
            for(int j=1;j<n;j++){
                states[i][j] = Math.min(states[i-1][j],states[i][j-1])+matrix[i][j];
            }
        }
        return states[n-1][n-1];
    }

状态转移方程

定义int[][] dp ,dp[i][j]=到达(i,j)位置的最短路径。
我们知道可以从(i-1,j)或者(i,j-1)两个状态到达(i,j)。
从 (i-1,j)到达(i,j),路径和需要增加grid[i][j],也就是说dp[i][j]=dp[i-1][j]+grid[i][j]。
从(i,j-1)到达(i,j),路径和需要增加grid[i][j],也就是说dp[i][j]=dp[i][j-1]+grid[i][j]。
那么转移方程就是

dp[i][j] = min(dp[i-1][j]+grid[i][j],dp[i][j-1]+grid[i][j])
=min(dp[i-1][j],dp[i][j-1])+grid[i][j]

初始化:

对dp[0][0]=grid[0][0]。
对于第0行,大于0的列,只能从左侧的位置转移到当前位置:dp[0][j-1]+grid[0][j]。
对于第0列,只能从上面的位置转移到当前位置:dp[i][0]=dp[i-1][0]+grid[i][0]。

实现代码:

	public int minPathSum(int[][] grid) {
        if(grid==null || grid.length==0) return 0;
        int n = grid.length;
        int m = grid[0].length;        
        int[][] dp = new int[n][m];
        for(int j=0;j<m;j++){
            dp[0][j] = (j==0?grid[0][0]:dp[0][j-1]+grid[0][j]);
        }
        for(int i=1;i<n;i++){
            dp[i][0] = dp[i-1][0]+grid[i][0];
        }
        for(int i=1;i<n;i++){
            for(int j=1;j<m;j++){
                dp[i][j] = Math.min(dp[i-1][j],dp[i][j-1])+grid[i][j];
            }
        }
        return dp[n-1][m-1];
    }

类似题目

可以用相同的思路处理 LeetCode 322 零钱兑换

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值