代码随想录之动态规划(一)

161 阅读7分钟

基础经典题目

动态规划解题思路可以参考文章[动态规划问题]

T509-斐波那契数

见LeetCode第509题[斐波那契数]

题目描述

斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n) 。

我的思路

  • 注意到公式中,当前斐波那契数只是和前两个数有关,因此可以准备3个变量fn | fn_1 | fn_2
  • 根据状态转移方程,自底向上遍历求解

我的题解

public int fib(int n) {
        if (n == 0) return 0;
        if (n == 1) return 1;

        int fn_1 = 1;
        int fn_2 = 0;
        int fn = 1;
        for (int i = 3; i <= n; i++) {
            fn_2 = fn_1;
            fn_1 = fn;

            fn = fn_1 + fn_2;
        }
        return fn;

    }

计算复杂度分析

  • 时间复杂度O(N)O(N),需要遍历到nn来更新fn
  • 空间复杂度O(1)O(1),仅需常数个变量存储fn_1fn_2

T70-爬楼梯

见LeetCode第70题[爬楼梯]

题目描述

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

我的思路

  • 这题本质上就是斐波那契数列,因此解题思路一直
  • 如何爬到第n个台阶上?
    • 可以从第 n - 1阶楼梯跨一步上去
    • 可以从第n - 2阶楼梯两步跨上去
  • 状态转移方程如何写出?
    • fn=fn1+fn2f_n = f_{n - 1} + f_{n - 2}
    • f1=2;f2=1;f_1 = 2; \quad f_2 = 1;

我的题解

public int climbStairs(int n) {
    if (n <= 3) return n;

    int f_1 = 2;
    int f_2 = 1;
    int fn = 0;
    for (int i = 3; i <= n; i++) {
        fn = f_1 + f_2;
        f_2 = f_1;
        f_1 = fn;
    }
    return fn;
}

T746-使用最小代价爬楼梯

见LeetCode第746题[使用最小代价爬楼梯]

题目描述

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第i个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。 你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。 请你计算并返回达到楼梯顶部的最低花费。

示例 2:

输入: cost = [1,100,1,1,1,100,1,1,100,1]
输出: 6
解释: 你将从下标为 0 的台阶开始。
- 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
- 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
- 支付 1 ,向上爬一个台阶,到达楼梯顶部。
总花费为 6 。

我的思路

  • 这一题有点像是贪心,上一层或者上两层是根据代价来的
  • 每一步都选择局部最优解,可以保证全局最优解
  • 状态转移方程可以为:
    • fn=min(fn1+costn1,fn2+costn2) f_n = \min(f_{n-1} + cost_{n-1}, f_{n-2} + cost_{n-2})
  • 初始边界条件为:
    • f0=0;f1=0;f_{0} = 0; f_{1} = 0;
  • 循环结束条件为:
    • i > cost.length

我的题解

public int minCostClimbingStairs(int[] cost) {

    if (cost.length == 2) {
        return Math.min(cost[0], cost[1]);
    }

    int total = 0;
    int total_1 = 0;
    int total_2 = 0;
    for (int i = 2; i <= cost.length; i++) {
        total = Math.min(total_1 + cost[i - 1], total_2 + cost[i - 2]);
        total_2 = total_1;
        total_1 = total;
    }
    return total;

}

计算复杂度分析

  • 时间复杂度O(N)O(N),需要遍历到nn来更新fn
  • 空间复杂度O(1)O(1),仅需常数个变量存储fn_1fn_2

T62-不同路径

见LeetCode第62题[不同路径]

题目描述

一个机器人位于一个 m x n **网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

示例

输入: m = 3, n = 7
输出: 28

我的思路

  • 对于当前位置,其只能从上面或者左边过来,因此状态转移方程为:dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
  • 初始化第一行dp[0][j]和第一列dp[i][0]为 1
  • 根据状态转移方程更新当前的路径数即可

我的题解

/**
 * 不同路径数量
 * @param m
 * @param n
 * @return
 */
public int uniquePaths(int m, int n) {

    if (m == 1 || n == 1) return 1;

    // 初始化 一维DP数组
    int[] dp = new int[n];
    Arrays.fill(dp, 1);

    for (int i = 1; i < m; i++) {
        // 将二维DP数组压缩到一维,当前的值只和上边的值dp[j] 和 左边的值 dp[j - 1] 相关
        for (int j = 1; j < n; j++) {
            dp[j] = dp[j] + dp[j - 1];
        }
    }
    return dp[n - 1];
}

复杂度分析

  • 时间复杂度: O(MN)O(MN),双层循环更新dp[]数组
  • 空间复杂度: O(N)O(N),用来存储dp中间值

T63-不同路径II

见LeetCode第63题[不同路径II]

题目描述

给定一个 m x n 的整数数组 grid。一个机器人初始位于 左上角(即 grid[0][0])。机器人尝试移动到 右下角(即 grid[m - 1][n - 1])。机器人每次只能向下或者向右移动一步。 网格中的障碍物和空位置分别用 1 和 0 来表示。机器人的移动路径中不能包含 任何 有障碍物的方格。 返回机器人能够到达右下角的不同路径数量。

示例

输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]] 
输出:2 
解释:3x3 网格的正中间有一个障碍物。 
从左上角到右下角一共有 2 条不同的路径: 
1. 向右 -> 向右 -> 向下 -> 向下 
2. 向下 -> 向下 -> 向右 -> 向右

我的思路

  • 这题本质上和上题没什么区别,只是需要判断障碍物
    • 如果当前的grid[i][j] == 0 那么就将dp置为 0
  • 同样可以考虑压缩dp数组为 一维数组

我的题解

public int uniquePathsWithObstacles(int[][] obstacleGrid) {
    int m = obstacleGrid.length;
    int n = obstacleGrid[0].length;
    int[] dp = new int[n];
    Arrays.fill(dp, 0);

    // 初始化第一行
    for (int i = 0; i < n; i++) {
        if (obstacleGrid[0][i] == 1) {
            dp[i] = 0;
        } else {
            dp[i] = i == 0 ? 1 : dp[i - 1];
        }
    }

    for (int i = 1; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (obstacleGrid[i][j] == 1) {
                dp[j] = 0;
            } else {
                dp[j] = j == 0 ? dp[j] : dp[j] + dp[j - 1];
            }
        }
    }

    return dp[n - 1];
}

复杂度分析

  • 时间复杂度: O(MN)O(MN),双层循环更新dp[]数组
  • 空间复杂度: O(N)O(N),用来存储dp中间值

T343-整数拆分

见LeetCode第343题[整数拆分]

题目描述

给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

返回 你可以获得的最大乘积 。

示例 2:

输入: n = 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36

我的思路

  • 这题实际就是求函数f(x)=xn/xf(x) = x^{n/x}的极值问题
  • 不难看出,函数f(x)f(x)的极大值点在x=ex = e处,但是ee显然是非整数
  • 注意到f(2)<f(3)f(2) < f(3),因此可以划分足够多的 3即可

我的题解

public int integerBreak(int n) {

    if (n <= 3) return n - 1;

    int quotient = n / 3;
    int remainder = n % 3;
    switch (remainder) {
        case 0:
            return (int) Math.pow(3, quotient);
        case 1:
            return (int) Math.pow(3, quotient - 1) * 4;
        default:
            return (int) Math.pow(3, quotient) * remainder;
    }
}

T96-不同的二叉搜索树

见LeetCode第96题[不同的二叉搜索树]

题目描述

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

示例

转存失败,建议直接上传图片文件

输入:n = 3 
输出:5

我的思路

  • 对于一个有序数组:1 2 3 4
  • 1 为根节点
    • 2 3 4 为右孩子,有几种可能? 5种
    • 共有 1 * 5 种可能
  • 2 为根节点
    • 1 为左孩子,1 种可能
    • 3 4 为右孩子,2 种可能
    • 共有 1 * 2 种可能
  • 3 为根节点,同2
  • 4 为根节点,同1

我的题解

/**
 * 不同的二叉搜索树
 * @param n
 * @return
 */
public int numTrees(int n) {

    int[] dp = new int[n + 1];
    // 初始化某些情况
    dp[0] = 1;
    dp[1] = 1;

    for (int i = 2; i <= n; i++) {
        // 根节点的选取
        for (int j = 1; j <= i; j++) {
            // 左孩子的可能数量 * 右孩子的可能数量
            dp[i] += dp[j - 1] * dp[i - j];
        }
    }
    return dp[n];
}

计算复杂度分析

  • 时间复杂度O(N2)O(N^2),更新dp数组需要嵌套一层循环
  • 空间复杂度O(N)O(N),用来存储dp数组