120. Triangle

......[2],

.....[3,4],

....[6,5,7],

.. [4,1,8,3]

经典例题,求从上到下最短。

解决方法:

  • DFS: Traverse

  • DFS: Divide Conquer

  • Divide Conquer + Memorization

  • Traditional Dynamic Programming


DFS: Traverse:

min = MIN_VALUE; traverse(triangle, 0, 0, 0);

一个list套list模拟树状结构,对于该树状结构的一个节点(0, 0)来说,它的左右子节点的表达方式:

  • 左下:行 + 1,列不变: (1, 0)
  • 右下:行 + 1,列 + 1: (1, 1)

min 不能做参数,受java底部引用的限制,这一点前面已经有很多例子了。但是全局变量的写法一直都不是我喜欢的,我觉得应当避免的写法。

public class Solution {
    int min;
    public int minimumTotal(List<List<Integer>> triangle) {
        if (triangle == null || triangle.size() == 0) return 0;

        min = Integer.MAX_VALUE;
        traverse(triangle, 0, 0, 0);
        return min;
    }
    private void traverse(List<List<Integer>> triangle, int x, int y, int sum) {
        if (x == triangle.size()) {
            min = Math.min(min, sum);
            return;
        }
        traverse(triangle, x + 1, y, sum + triangle.get(x).get(y));
        traverse(triangle, x + 1, y + 1, sum + triangle.get(x).get(y));
    }
}

显然会超时

时间复杂度是多少呢?无奖竞猜:

  • A - O(n)
  • B - O(n^2)
  • C - O(2^n)
  • D - O(n!)

提示:一个树状结构有 n 层,递归要 n 层,每一层有两个选择。

DFS: Divide Conquer

这个和刚才差不多,还是递归.

  • 左下:行 + 1,列不变: (x + 1, y)
  • 右下:行 + 1,列 + 1: (x + 1, y + 1)

这次很不错,很棒,没有用全局变量,写着就跟普通二叉树遍历没什么太大区别。

public class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        if (triangle == null || triangle.size() == 0) return 0;

        return dfs(triangle, 0, 0);
    }
    private int dfs(List<List<Integer>> triangle, int x, int y) {
        if (x == triangle.size()) {
            return 0; //just like root == null
        }
        int left = dfs(triangle, x + 1, y);
        int right = dfs(triangle, x + 1, y + 1);

        return Math.min(left, right) + triangle.get(x).get(y);
    }
}

显然还是会超时

这次的时间复杂度是多少呢?无奖竞猜:

  • A - O(n)
  • B - O(n^2)
  • C - O(2^n)
  • D - O(n!)

Divide Conquer + Memorization

我们来思考一下,前面的方法之所以会超时,是因为我们干了一些没用的事,因为有的点进入了两次。本来已经在别的点的下面算过了右子树,我们跑到另一个点的下面又去了算了一遍左子树,但实际上是同一个节点,我们算了两遍。这显然会超时。

委座又出来了:以空间换时间。

记忆化搜索!记住那些已经计算过的点,加一个hash表。

先要初始化,都变成MAX_VALUE,如果我们到了最后一行,那就返回最后一行的值就行了。因为下面没有了。

public class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        if (triangle == null || triangle.size() == 0) return 0;

        int n = triangle.size();
        int[][] hash = new int[n][n];
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                hash[i][j] = Integer.MAX_VALUE;
            }
        }
        return dfs(triangle, hash, 0, 0);
    }
    private int dfs(List<List<Integer>> triangle, int[][] hash, int x, int y) {
        if (x == triangle.size() - 1) {
            return triangle.get(x).get(y); // root is leaf
        }
        if (hash[x][y] != Integer.MAX_VALUE) {
            return hash[x][y];
        }
        int left = dfs(triangle, hash, x + 1, y);
        int right = dfs(triangle, hash, x + 1, y + 1);

        hash[x][y] = Math.min(left, right) + triangle.get(x).get(y);
        return hash[x][y];
    }
}

这次不超时了,因为我们优化过了啊。

这次的时间复杂度是多少呢?这次好猜:

  • A - O(n)
  • B - O(n^2)
  • C - O(2^n)
  • D - O(n!)

Traditional Dynamic Programming

好,终于到重点了,这题用动规,怎么做?

多重循环方式。上一层依赖于下一层,所以应该 bottom-up。

熟悉的配方:

  • 左下:行 + 1,列不变: (x + 1, y)
  • 右下:行 + 1,列 + 1: (x + 1, y + 1)
  • state: f[i][j] 表示从 i,j 出发走到最后一层的最小路径长度。
  • function: f[i][j] = MIN(f[i + 1][j], f[i + 1][j + 1]) + triangle[i][j];
  • initialization: f[n - 1][i] = triangle[n - 1][i] 赋最后一层的值,因为我们要 bottom-up
  • answer: f[0][0] again, bottom-up
public class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        if (triangle == null || triangle.size() == 0) return 0;

        int n = triangle.size();
        int[][] f = new int[n][n];

        for (int i = 0; i < n; ++i) {
            f[n - 1][i] = triangle.get(n - 1).get(i);
        }
        for (int i = n - 2; i >= 0; --i) { // from n - 2 level to 0 level
            for (int j = 0; j <= i; ++j) { // from left to right
                f[i][j] = Math.min(f[i + 1][j], f[i + 1][j + 1]) + triangle.get(i).get(j);
            }
        }
        return f[0][0];
    }
}

时间空间复杂度?

  • A - O(n)
  • B - O(n^2)
  • C - O(2^n)
  • D - O(n!)

写了两百行废话终于说完了,我们下面正式进入坐标型动态规划:

坐标型动态规划

  • state:

    • f[x] 表示我从起点走到坐标x......

    • f[x][y] 表示我从起点走到坐标x,y......

  • function: 研究走到 x, y 这个点之前的一步

  • initialize: 起点

  • answer: 终点


64. Minimum Path Sum

  • state: f[x][y]从起点走到 x, y 的最短路径
  • function: f[x][y] = min(f[x - 1][y], f[x][y - 1]) + A[x][y]
  • intialize:
    • f[i][0] = sum(0,0 ~ i,0)
    • f[0][i] = sum(0,0 ~ 0,i)
  • answer: f[m - 1][n - 1]

只要是初始化二维的动态规划,就要初始化第 0 行,第 0 列。

这么简单一个题,我tm检查一个typo看了20分钟。

public class Solution {
    public int minPathSum(int[][] grid) {
        if (grid == null || grid.length == 0 || grid[0].length == 0) return 0;

        int m = grid.length;
        int n = grid[0].length;
        int[][] f = new int[m][n];

        f[0][0] = grid[0][0];

        for (int i = 1; i < m; ++i) {
            f[i][0] = f[i - 1][0] + grid[i][0];
        }
        for (int i = 1; i < n; ++i) {
            f[0][i] = f[0][i - 1] + grid[0][i];
        }
        for (int i = 1; i < m; ++i) {
            for (int j = 1; j < n; ++j) {
                f[i][j] = Math.min(f[i - 1][j], f[i][j - 1]) + grid[i][j];
            }
        }
        return f[m - 1][n - 1];
    }
}

节省空间:

public class Solution {
    public int minPathSum(int[][] grid) {
        int rows = grid.length;
        int cols = grid[0].length;

        for(int i = 1; i < cols; i++){
            grid[0][i] += grid[0][i - 1];
        }
        for(int i = 1; i < rows; i++){
            grid[i][0] += grid[i - 1][0];
        }

        for(int i = 1; i < rows; i ++){
            for(int j = 1; j < cols; j++){
                grid[i][j] = Math.min(grid[i - 1][j], grid[i][j - 1]) + grid[i][j];
            }
        }

        return grid[rows - 1][cols - 1];
    }
}

62. Unique Paths

  • state: f[x][y]从起点到 x, y 的路径数
  • function: (研究倒数第一步) f[x][y] = f[x - 1][y] + f[x][y - 1]
  • initialize:
    • f[0][i] = 1
    • f[i][0] = 1
  • answer: f[m - 1][n - 1]
public class Solution {
    public int uniquePaths(int m, int n) {
        int[][] f = new int[m][n];

        for (int i = 0; i < m; ++i) {
            f[i][0] = 1;
        }
        for (int i = 0; i < n; ++i) {
            f[0][i] = 1;
        }

        for (int i = 1; i < m; ++i) {
            for (int j = 1; j < n; ++j) {
                f[i][j] = f[i - 1][j] + f[i][j - 1];
            }
        }
        return f[m - 1][n - 1];
    }
}

63. Unique Paths II

有障碍不能走。注意初始化。

public class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        if (obstacleGrid == null || obstacleGrid.length == 0 || obstacleGrid[0].length == 0) return 0;

        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;

        int[][] f = new int[m][n];

        for (int i = 0; i < m; ++i) {
            if (obstacleGrid[i][0] != 1) {
                f[i][0] = 1;
            } else break;
        }
        for (int i = 0; i < n; ++i) {
            if (obstacleGrid[0][i] != 1) {
                f[0][i] = 1;
            } else break;
        }
        for (int i = 1; i < m; ++i) {
            for (int j = 1; j < n; ++j) {
                if (obstacleGrid[i][j] == 1) {
                    f[i][j] = 0;
                } else f[i][j] = f[i - 1][j] + f[i][j - 1];
            }
        }
        return f[m - 1][n - 1];
    }
}

70. Climbing Stairs

我拿去当背包问题的intro了,实际上是所有动规的intro。同时也是最高频动规题,同时还是最简单的动规题吧。

这个要是面试时候写错了就太尬了。

public class Solution {
    public int climbStairs(int n) {
        if (n == 0) return 1;
        if (n == 1) return 1;
        if (n == 2) return 2;

        int[] f = new int[n + 1];
        f[0] = 1;
        f[1] = 1;

        for (int i = 2; i <= n; ++i) {
            f[i] = f[i - 1] + f[i - 2];
        }
        return f[n];
    }
}

55. Jump Game

用动规会超时,时间复杂度n^2。小细节容易出错,重点复习。

这道题有点特殊是因为我们返回的就是一个True False,也就是说你跳到最后一步前的起点是不确定的。不一定非要从 i - 1 的位置跳到 i ,所以说你必须每个位置都要考虑,也就是说一个一维数组需要 nested for loop。需要一个 j 来确定起点,看看每一个点是不是都有可能跳到最后,所以导致了n^2的复杂度导致了超时。

public class Solution {
    public boolean canJump(int[] nums) {
        int n = nums.length;

        boolean[] f = new boolean[n];
        f[0] = true;

        for (int i = 1; i < n; ++i) {
            for (int j = 0; j < i; ++j) {
                if (f[j] && j + nums[j] >= i) {
                    f[i] = true;
                    break;
                }
            }
        }
        return f[n - 1];
    }
}

这道题最优的解法是贪心法,这也是贪心法为数不多的应用场景,贪心法应用的太少了所以我们都没有一个专门的专题来做。

但是偶尔也能碰见这样的题必须用贪心。

public class Solution {
    public boolean canJump(int[] nums) {
        int max = nums[0];

        for (int i = 0; i < nums.length; ++i) {
            if (i <= max && nums[i] + i >= max) {
                max = nums[i] + i;
            }
        }
        return max >= nums.length - 1;
    }
}

45. Jump Game II

依旧是用动规会超时,时间复杂度n^2。

public class Solution {
    public int jump(int[] nums) {
        int n = nums.length;

        int[] f = new int[n];
        f[0] = 0;
        for (int i = 1; i < nums.length; i++) {
            f[i] = Integer.MAX_VALUE;
        }

        for (int i = 1; i < n; ++i) {
            for (int j = 0; j < i; ++j) {
                if (f[j] != Integer.MAX_VALUE && j + nums[j] >= i) {
                    f[i] = Math.min(f[i], f[j] + 1);
                }
            }
        }
        return f[n - 1];
    }
}

贪心:

public class Solution {
    public int jump(int[] nums) {
        int start = 0;
        int end = 0;
        int count = 0;

        while (end < nums.length - 1) {
            int max = end;
            count++;
            for (int i = start; i <= end; ++i) {
                if (nums[i] + i > max) {
                    max = nums[i] + i;
                }
            }
            start = end + 1;
            end = max;
        }
        return count;
    }
}

Jump Game是两道很特别的题,需要多加注意。

results matching ""

    No results matching ""