day 19-20 算法:动态规划,爬楼梯,三角形最小路径和,乘积最大子序列,最长上升子序列

1. 题目

  1. 爬楼梯:假设需要n阶能够爬到顶楼,每一次只能爬1阶或者2阶,求问,有多少种不同的方法爬上楼顶? https://leetcode-cn.com/problems/climbing-stairs/description/
  2. 三角形最小路径和:https://leetcode-cn.com/problems/triangle/description/
  3. 乘积最大子序列:https://leetcode-cn.com/problems/maximum-product-subarray/description/
  4. 最长上升子序列:https://leetcode-cn.com/problems/longest-increasing-subsequence/

2. 基本知识

2.1 动态规划

动态规划和分治类似,但他们有异同点:

  1. 都是通过组合子问题的方式来解决原始问题
  2. 动态规划的子问题是会重合的,而分治的子问题则是不相交的
  3. 不同的子问题中会有一些公共的子问题,动态规划不会重复计算子问题,而分治则会重复计算,做更多的不必要工作

2.2 动态规划的核心

使用动态规划都可以使用递归(分治)来完成,在递归的基础上加上记忆化即可去除重复的计算,推导出最优解的方程(当前解和之前已经算出来的解之间的关系)来得出结果。

  1. 递归 + 记忆化 -> 递推(从小到大计算)
  2. 状态的定义,用数组辅助存储已计算的值,避免多次计算
  3. 状态转移方程:opt[n] = best_of(opt[n-1], opt[n-2],…)
  4. 最优子结构

2.3 简单的例子

2.3.1 斐波那契数列

计算斐波那契数列的和: 0,1,2,3,5,8…

  1. 递归算法

    时间复杂度 O(2n),空间复杂度O(1)

     private int fib(int n) {
         return (n <= 1) ? n : fib(n - 1) + fib(n - 2);
     }
    
  2. 动态规划

    时间复杂度O(n),空间复杂度O(n)

     private int dpFib(int n) {
         int[] memo = new int[n];
         memo[0] = 0; memo[1] = 1;
         for (int i = 2; i < n; i++) {
             // 从小到大递推,如果从大到小,又是一堆重复计算
             memo[i] = memo[i - 1] + memo[i - 2];
         }
         return memo[n-1];
     }
    

2.3.2 障碍棋盘路径

给定一个棋盘,从最左上角到最右下角有多少条路径,棋盘中有些格子中有石头无法通过。

  1. 递归解法

     private int paths(boolean[][] isStone, int m, int n) {
         if (isStone[m][n]) return 0;
         if (m == 0 || n == 0) return 1;
         return paths(isStone, m - 1, n) + paths(isStone, m, n - 1);
     }
    
  2. 动态规划

     private int dpPaths(boolean[][] isStone, int m, int n) {
         int[][] opt = new int[m + 1][n + 1];
    
         for (int i = 0; i < m; i++) {
             for (int j = 0; j < n; j++) {
                 // opt[i,j] = opt[i-1,j] + opt[i,j-1]
                 if (i == 0 || j == 0) {
                     opt[i][j] = 1;
                     continue;
                 }
                 if (isStone[i][j]) {
                     //碰到石头
                     opt[i][j] = 0;
                 } else {
                     //可以通过
                     opt[i][j] = opt[i - 1][j] + opt[i][j - 1];
                 }
             }
         }
    
         return opt[m][n];
     }
    

3. 算法题解

3.1 爬楼梯

假设需要n阶能够爬到顶楼,每一次只能爬1阶或者2阶,求问,有多少种不同的方法爬上楼顶?

分析:由于每次只能爬1阶或者2阶,所以,从站在楼顶那一刻倒推,倒推1阶,假设有f(n-1)种办法,倒推2阶,假设有f(n-2)种办法,则,f(n) = f(n-1) + f(n-2),这是一个标准的斐波那契数列的和解法。见 2.3.1 斐波那契数列

3.2 三角形最小路径

给定一个三角形,找出自顶向下的最小路径和,每一步只能移动到下一行的相邻节点上(角标:(i+1,j+1);(i+1,j))
例如:

[
    [2],
    [3,4],
    [6,5,7],
    [4,1,8,3]
]

得到的结果为:2+3+5+1 = 11

解法1:递归
当前节点值加上下面两个分支的最小值,加起来总和为最小路径和。
该解法时间复杂度:O(2n),空间复杂度为O(1)

private int getResult(int[][] num) {
    return dfsSum(num, 0, 0);
}

 private int dfsSum(int[][] num, int m, int n) {
    if (m >= num.length || n >= num[0].length) return 0;
    int min = Math.min(dfsSum(num, m + 1, n), dfsSum(num, m + 1, n + 1));
    return num[m][n] + min;
}

解法2:动态规划
最后一行分别赋值给初始最小路径节点值,然后根据公式,sum[i][j] = Math.min(sum[i+1][j], sum[i+1][j+1]) + num[i][j];层层倒推,得到sum[0][0]即为当前的解。

private int dpSum(int[][] num) {
    int row = num.length;
    int col = num[num.length - 1].length;
    int[][] sum = new int[row][col];

    for (int i = row-1; i >=0; i--) {
        for (int j = 0; j < num[i].length; j++) {
            if (i == row - 1) {
                // 最后一行
                sum[i][j] = num[i][j];
                continue;
            }
            // 下一行相邻两个路径的最小值加上当前节点值
            sum[i][j] = Math.min(sum[i+1][j], sum[i+1][j+1]) + num[i][j];
        }
    }
    return sum[0][0];
}

上面使用的是二维数组来存放中间计算的值,空间复杂度为O(n2),下面优化为O(n)

private int dpSum2(List> triangle) {
    int row = triangle.size();
    // 初始化一个一维数组,用来辅助存储单次循环内的路径和
    int[] dp = new int[row];

    for (int i = 0; i < row; i++) {
        // 将最后一行的值赋值为初始值
        dp[i] = triangle.get(row - 1).get(i);
    }
    for (int i = row - 2; i >= 0; i--) {
        for (int j = 0; j <= i; j++) {
            // 将路径和更新
            dp[j] = triangle.get(i).get(j) + Math.min(dp[i], dp[i + 1]);
        }
    }
    return dp[0];
}

3.3 乘积最大子序列

给定一个数组nums,找出一个序列中乘积最大的连续子序列(该序列至少包含一位数)

示例:

输入:[2,3,-2,4]
输出:6
解释:子数组[2,3]有最大乘积

解题思路:
动态规划两个核心步骤:

  1. 定义状态:使用二维数组存储子乘积的最大和最小值

  2. 定义状态方程:如果当前数大于0:取最大数*当前数,得最大子乘积;如果当前数小于0,则取最小数,乘以当前数,得最小数。

    private int maxProduct(int[] nums) {
    //第二列为了表示是最大还是最小值,第一行为最大值,第二行为最小值
    int[][] dp = new int[2][2];
    //初始化最大最小值
    dp[0][0] = dp[0][1] = nums[0];

     int max = 0;
     for (int i = 1; i < nums.length; i++) {
         int x = i % 2;
         int y = (i - 1) % 2;
         // 最大值
         dp[x][0] = Math.max(dp[y][0] * nums[i], dp[y][1] * nums[i]);
         // 最小值
         dp[x][1] = Math.min(dp[y][0] * nums[i], dp[y][1] * nums[i]);
         max = Math.max(max, dp[x][0]);
     }
     return max;
    

    }

也可以只定义两个变量代替数组:

private int maxProduct2(int[] nums) {
    int icMax = nums[0];
    int icMin = nums[0];

    for (int i = 1; i < nums.length; i++) {
        icMax = max(icMax * nums[i], icMax * nums[i], icMax);
        icMin = min(icMax * nums[i], icMax * nums[i], icMin);
    }
    return icMax;
}

3.4 最长上升子序列

给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

输入:[10,9,2,5,3,7,101,18]
输出:4
解释:最长子序列:[2,3,7,101]

解法1: 递归
从头开始递归,如果当前节点比上一个节点值大,那么就 + 1,否则继续递归,直到遍历完成为止。
该解法时间复杂度:O(2n),空间复杂度O(1)

private int lengthOfLIS(int[] nums) {
    return lengthofLIS(nums, Integer.MIN_VALUE, 0);
}

private int lengthofLIS(int[] nums, int pre, int cur) {

    if (cur == nums.length) return 0;

    int taken = 0;
    if (pre < nums[cur]) {
        //当前节点是有效的上升子序列的一个
        taken = 1 + lengthofLIS(nums, nums[cur], cur + 1);
    }
    // 当前节点不是上升子序列中的节点
    int noTaken = lengthofLIS(nums, pre, cur + 1);
    return Math.max(taken, noTaken);
}

解法2: 动态规划
动态规划核心步骤:

  1. 定义状态:使用一个一位数组来存放当前升序的最大子序列长度

  2. 定义状态方程:如果当前值和之前的所有元素遍历对比,当前值较大,且该子序列是前面所有子序列长度的最大值,那么+1就是当前的最大子序列的长度。

     private int lengthOfLIS(int[] nums) {
         if (nums.length == 0) return 0;
    
         int[] dp = new int[nums.length];
         dp[0] = 1;
    
         int maxas = 1;
         for (int i = 1; i < nums.length; i++) {
             int maxval = 0;
             for (int j = 0; j < i; j ++) {
                 if (nums[i] > nums[j]) {
                     maxval = Math.max(maxval, dp[j]);
                 }
             }
             dp[i] = maxval + 1;
             maxas = Math.max(maxas, dp[i]);
         }
         return maxas;
     } 
    

你可能感兴趣的:(面试,算法和数据结构,算法题面试专栏)