Leetcode动态规划两题

平常很少做动态规划的题目,今天刚好在leetcode上遇到了两道。其中后一道是前一道题的拓展。题解使用Java描述。


问题一

链接

A robot is located at the top-left corner of a m x n grid (marked ‘Start’ in the diagram below).

The robot can only move either down or right at any point in time. The robot is trying to reach the bottom-right corner of the grid (marked ‘Finish’ in the diagram below).

How many possible unique paths are there?

问题:

一个机器人位于一个 m x n 网格的左上角 (起始方位如图已标出)

这个机器人每次只能向下或向右移动一格。机器人试图移动到网格右下方的角落 (结束位置如图已标出)

机器人有多少种可能的移动路径?


这个问题的题解很容易用递归描述。如图很容易看出,当 m x n 网格中 m = 1 或是 n = 1时,机器人只能有一种走法 (不断向下or向右移动)。当 m、n均大于1时,可以分解成两个子问题(m - 1) x n 和 m x (n - 1),可能的移动路径数量为两个子问题解之和。Java描述如下:

public class Solution {
    public int uniquePaths(int m, int n) {
        if (m == 1 || n == 1) return 1;
        return uniquePaths(m - 1, n) + uniquePaths(m, n - 1);
    }
}

看起来真是非常的easy。但是这个代码是无法AC的,原因是超时。分析一下这一算法的时间复杂度,会发现其复杂度居然是呈指数增长!这时候我们就可以使用 动态规划(Dynamic Programming)来优化算法。

我们的递归算法慢的主要原因在于进行了过多的重复计算。我们会发现 uniquePaths(3, 3) = uniquePaths(2, 3) + uniquePaths(3, 2) = uniquePaths(1, 3) + uniquePaths(2, 2) + uniquePaths(3, 1) + uniquePaths(2, 2)

这一过程中出现了对uniquePaths(2, 2)的重复计算。

动态规划将子问题的答案系统地记录在了一个表里,避免了这样的重复计算。

如下所示,我们用一个m x n的二维数组中记录了子问题的解,如果搜索到子问题已解决,那么直接返回二维数组中记录的值。这样就在原有算法的基础上避免了重复计算。

public class Solution {
    private int[][] map;

    public int uniquePaths(int m, int n) {
        map = new int[m][n];
        for (int i = 0; i < m; i++)
            for (int j = 0; j < n; j++)
                map[i][j] = -1;
        return upath(m, n);
    }

    public int upath(int m, int n) {
        if (m == 1 || n == 1) return 1;
        if (map[m-1][n-1] != -1) return map[m-1][n-1];
        int res = upath(m - 1, n) + upath(m, n -1);
        map[m-1][n-1] = res;
        return res;
    }
}

大部分时候我们会写成非递归的形式(教科书上好像都是像下面这样写的)

public class Solution {
   public int uniquePaths(int m, int n) {
       int[][] map = new int[m][n];
       for(int i = 0; i<m;i++)
           map[i][0] = 1;
       for(int j= 0;j<n;j++)
           map[0][j]=1;
       for(int i = 1;i<m;i++)
           for(int j = 1;j<n;j++)
               map[i][j] = map[i-1][j]+map[i][j-1];
       return map[m-1][n-1];
   }
}


问题二

链接

Follow up for “Unique Paths”:

Now consider if some obstacles are added to the grids. How many unique paths would there be?

An obstacle and empty space is marked as 1 and 0 respectively in the grid.

For example, There is one obstacle in the middle of a 3x3 grid as illustrated below.

继续上一道题。

现在考虑有的网格中存在障碍物,那么在这种情况下还有多少种走法?

有障碍物和无障碍物的方格分别被标记为1和0。如下图即为一个障碍物在正中的3 x 3网格。

[
  [0,0,0],
  [0,1,0],
  [0,0,0]
]

这道题本质上和上一道题是一样的,同样使用动态规划的方法解决。不过在此基础上需要稍作改变。

上一道题中我们将递归的基本条件,1 x m和 n x 1网格的走法置为1。但是在这里,我们将障碍物和障碍物之前的走法置为0。对于存在障碍物的网格[0, 0, 1, 1, 0],我们认为每个方格对应的走法数量为[0, 0, 0, 0, 1]。而对于其它存在障碍物的方格,我们统一认为该方格对应的走法为0。代码在第一题的基础上稍作修改后如下。

public class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        int[][] map = new int[m][n];
        for(int i = 0, k = 1; i<m;i++) {
            if (obstacleGrid[i][0] == 1) k = 0;
            map[i][0] = k;
        }
        for(int j= 0, k = 1;j<n;j++) {
            if (obstacleGrid[0][j] == 1) k = 0;
            map[0][j]=k;
        }
        for(int i = 1;i<m;i++)
            for(int j = 1;j<n;j++) {
                if (obstacleGrid[i][j] == 1) map[i][j] = 0;
                else map[i][j] = map[i-1][j]+map[i][j-1];
            }
        return map[m-1][n-1];
    }
}