动态规划DP

动态规划 DP

3. 动态规划 DP

什么是动态规划

动态规划,英文:Dynamic Programming,简称DP,将问题分解为互相重叠的子问题,通过反复求解子问题来解决原问题就是动态规划,如果某一问题有很多重叠子问题,使用动态规划来解是比较有效的。

Q:什么是重叠子问题

A:比如这里要求的f(4) f(3) … f(1)就是重叠子问题

image-20221219015225017

求解动态规划的核心问题是穷举,但是这类问题穷举有点特别,因为这类问题存在「重叠子问题,如果暴力穷举的话效率会极其低下。动态规划问题一定会具备「最优子结构」,才能通过子问题的最值得到原问题的最值。另外,虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,只有列出**正确的「状态转移方程」**才能正确地穷举。重叠子问题、最优子结构、状态转移方程就是动态规划三要素

Q:什么是最优子结构?

A:问题寻找从顶部到底部相加和最小的路径,现在已经在5这个位置,可以向1和8走,显然5+1比5+8小,那么5往1走就是最优的子结构

image-20221219015537608

动态规划和其他算法的区别

  1. 动态规划和分治的区别:动态规划和分治都有最优子结构 ,但是分治的子问题不重叠
    分治没有重复的子问题
    image-20221219020103735
  2. 动态规划和贪心的区别:动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优解,所以它永远是局部最优,但是全局的解不一定是最优的。
    比如
    image-20221219020250267
  3. 动态规划和递归的区别:递归和回溯可能存在非常多的重复计算,动态规划可以用递归加记忆化的方式减少不必要的重复计算

解题方法

  • 递归+记忆化(自顶向下)
  • 动态规划(自底向上)

ds_135

解题步骤

  1. 根据重叠子问题定义状态
  2. 寻找最优子结构推导状态转移方程
  3. 确定dp初始状态
  4. 确定输出值

509. 斐波那契数 (easy)

  1. dp(n) 表示第n个位置斐波那契数为多少
  2. 状态转移方程 dp(n) = dp(n-1) + dp(n-2)
  3. dp初始状态 dp(0) = 0 dp(1) = 1
  4. 确定输出值

image-20221219020720124

暴力递归

class Solution {
    public int fib(int n) {
        if (n == 0) {
            return 0;
        } else if (n == 1) {
            return 1;
        }

        return fib(n - 1) + fib(n - 2);
    }
}

image-20221219021847649

递归 + 记忆体

也就是自顶向下

class Solution {
    //用于存储已经计算过的值
    static HashMap<Integer,Integer> hashMap = new HashMap<>();
    public int fib(int n) {
        //如果已经计算过,直接返回结果
        if(hashMap.containsKey(n)){
            return hashMap.get(n);
        }


        if (n == 0) return 0;
        if (n == 1) return 1;
		
        //计算完后放入HashMap
        hashMap.put(n,fib(n - 1) + fib(n - 2));
        return hashMap.get(n);
    }
}

image-20221219022309266

动态规划

沿着一条路往上走,不会去走其他路了

image-20221219023335258

class Solution {
    static int[] dp = new int[50];
    public int fib(int n) {
        if (n == 0) return 0;
        if (n == 1) return 1;
        dp[0] = 0;
        dp[1] = 1;
        
        for (int i = 2; i <= n; i++) {
            //自底向上计算每个状态
            dp[i] = dp[i-1] + dp[i-2];
        }

        return dp[n];
    }
}

image-20221219023434418

滚动数组优化

因为每个斐波那契数只与前两个斐波那契数有关,所以其实可以把数组的长度设为3或2,优化空间复杂度

class Solution {
    static int[] dp = new int[3];
    public int fib(int n) {
        if (n == 0) return 0;
        if (n == 1) return 1;
        dp[0] = 0;
        dp[1] = 1;
        for (int i = 2; i <= n; i++) {
            //自底向上计算每个状态
            dp[2] = dp[0] + dp[1];

            //数组滚动
            dp[0] = dp[1];
            dp[1] = dp[2];

        }

        return dp[2];
    }
}

//再次优化,把数组变为2

class Solution {
    static int[] dp = new int[2];
    public int fib(int n) {
        if (n == 0) return 0;
        if (n == 1) return 1;
        dp[0] = 0;
        dp[1] = 1;
        int sum = 0;

        for (int i = 2; i <= n; i++) {
            //自底向上计算每个状态
            sum = dp[0] + dp[1];

            //数组滚动
            dp[0] = dp[1];
            dp[1] = sum;
        }

        return sum;
    }
}

image-20221219024331099

动态规划 + 降维

降维能减少空间复杂度,但不利于程序的扩展

比如如果我们要获取过程中的每一个斐波那契数,这种方法就不好扩展了

class Solution {
    public int fib(int n) {
        if (n == 0) return 0;
        if (n == 1) return 1;
        int prev0 = 0;
        int prev1 = 1;
        int sum = 0;
        for (int i = 2; i <= n; i++) {
            //自底向上计算每个状态
            sum = prev0 + prev1;
            prev0 = prev1;
            prev1 = sum;

        }

        return sum;
    }
}

image-20221219024833175

  • 思路:自底而上的动态规划
  • 复杂度分析:时间复杂度O(n),空间复杂度O(1)

62. 不同路径(midium)

难度中等

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

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

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

示例 1:

img

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

示例 2:

输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下

示例 3:

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

示例 4:

输入:m = 3, n = 3
输出:6

提示:

  • 1 <= m, n <= 100
  • 题目数据保证答案小于等于 2 * 109

1. 动态规划

123133
  1. 根据重叠子问题定义状态
    • dp[i][j]表示到达i j的位置有多少种路径
  2. 寻找最优子结构推导状态转移方程
    • 由于在每个位置只能向下或者向右, 所以每个坐标的路径和等于上一行相同位置和上一列相同位置不同路径的总和,状态转移方程:dp[i][j] = dp[i-1][j] + dp[i][j-1];
  3. 确定dp初始状态
    • dp[0][0...n] = 1
    • dp[0...m][0] = 1
  4. 确定输出值
  • 复杂度:时间复杂度O(mn)。空间复杂度O(mn),优化后空间复杂度O(n)
class Solution {
    public int uniquePaths(int m, int n) {
        //定义状态
        int[][] dp = new int[m][n];
        //初始化
        for (int i = 0; i < m; i++) {
            dp[i][0] = 1;
        }
        for (int i = 0; i < n; i++) {
            dp[0][i] = 1;
        }

        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                //状态转移方程
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }

        return dp[m-1][n-1];
    }
}

image-20221219032424862

2. 状态压缩

123133
  1. 可以先建一个都是1的一维数组表示第一行
  2. 我们注意到2这个位置是左边加上上边的和
    • 左边就是数组中的前一位
    • 上边其实就是自己的值
  3. 自己等于自己加上左边
class Solution {
    public int uniquePaths(int m, int n) {
        //定义状态
        int[] dp = new int[n];
        //初始化
        Arrays.fill(dp, 1);

        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                //状态转移方程
                dp[j] = dp[j] + dp[j-1];
            }
        }

        return dp[n-1];
    }
}

image-20221219032353292

63. 不同路径 II (midium)

难度中等

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

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

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

网格中的障碍物和空位置分别用 10 来表示。

示例 1:

img

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

示例 2:

img

输入:obstacleGrid = [[0,1],[0,0]]
输出:1

提示:

  • m == obstacleGrid.length

  • n == obstacleGrid[i].length

  • 1 <= m, n <= 100

  • obstacleGrid[i][j]01

  • 思路:和62题一样,区别就是遇到障碍直接返回0

  • 复杂度:时间复杂度O(mn),空间复杂度O(mn),状态压缩之后是o(n)

1. 动态规划

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        //1. 根据重叠子问题定义状态
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        int dp[][] = new int[m][n];
        //2. 确定dp初始状态
        //上边和左边都是1,遇到障碍物开始后面都是0
        for (int i = 0; i < m; i++) {
            //不是障碍物填1
            if(obstacleGrid[i][0] != 1){
                dp[i][0] = 1;
            }else {
                //遇到障碍物返回
                break;
            }
        }
        for (int i = 0; i < n; i++) {
            //不是障碍物填1
            if(obstacleGrid[0][i] != 1){
                dp[0][i] = 1;
            }else {
                //遇到障碍物返回
                break;
            }
        }


        //3. 寻找最优子结构推导状态转移方程
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                //如果是障碍物
                if(obstacleGrid[i][j] == 1){
                    dp[i][j] = 0;
                    continue;
                }
                //不是障碍物 dp[i][j] = dp[i-1][j] + dp[i][j-1]
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        return dp[m-1][n-1];
    }
}

image-20221220022609385

2. 状态压缩

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        //1. 根据重叠子问题定义状态
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        int dp[] = new int[n];
        //2. 确定dp初始状态
        dp[0] = obstacleGrid[0][0] == 1 ? 0 : 1; 
		//dp[0] = 1; //也可以直接先用1填充,因为后面会遍历到
        
        
        //3. 寻找最优子结构推导状态转移方程
        //注意这里从0开始
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                //遇到障碍物
                if(obstacleGrid[i][j] == 1){
                    dp[j] = 0;
                    continue;
                }
                //最左边的时候.左边没有元素,就等于上边(自己)
                if(j >= 1){
                    dp[j] = dp[j] + dp[j-1];
                }
                //等于上边(自己)和左边的和
            }
        }

        return dp[n-1];
    }
}

image-20221220023721317

70. 爬楼梯(easy)

难度简单

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例 1:

输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶

示例 2:

输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶

提示:

  • 1 <= n <= 45

思路

  • 思路:因为每次可以爬 1 或 2 个台阶,所以到第n阶台阶可以从第n-2或n-1上来,其实就是斐波那契的dp方程
  • 复杂度分析:时间复杂度O(n),空间复杂度O(1)

1.动态规划

当前台阶可以由前一个台阶走过来,也可以由前两个台阶走过来

所以走法数 = 前一个台阶走法数 + 前两个台阶走法数

ds_71

class Solution {
    public int climbStairs(int n) {

        if(n == 1){
            return 1;
        }

        //1.定义状态
        int[] dp = new int[n];

        //2.初始化
        dp[0] = 1;
        dp[1] = 2;

        //3.状态转移方程
        for (int i = 2; i < n; i++) {
            dp[i] = dp[i-1] + dp[i-2];
        }

        return dp[n-1];
    }
}

image-20221220025746581

2.状态压缩

class Solution {
    public int climbStairs(int n) {

        if (n == 1) {
            return 1;
        } else if (n == 2) {
            return 2;
        }

        //1.定义状态
        int[] dp = new int[2];

        //2.初始化
        dp[0] = 1;
        dp[1] = 2;
        int result = 0;

        //3.状态转移方程
        for (int i = 2; i < n; i++) {
            result = dp[0] + dp[1];
            dp[0] = dp[1];
            dp[1] = result;
        }

        return result;
    }
}

image-20221220030243281

3.降维

class Solution {
    public int climbStairs(int n) {

        if (n == 1) {
            return 1;
        } else if (n == 2) {
            return 2;
        }

        //1.定义状态
        //2.初始化
        int prev1 = 1;
        int prev2 = 2;
        int result = 0;

        //3.状态转移方程
        for (int i = 2; i < n; i++) {
            result = prev1 + prev2;
            prev1 = prev2;
            prev2 = result;
        }

        return result;
    }
}

279. 完全平方数(midium)

难度中等

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,14916 都是完全平方数,而 311 不是。

示例 1:

输入:n = 12
输出:3 
解释:12 = 4 + 4 + 4

示例 2:

输入:n = 13
输出:2
解释:13 = 4 + 9

提示:

  • 1 <= n <= 104

思路

ds_204

dp[i] 的i减去所有比i小的完全平方数的平方 dp[i- j * j]

然后再+1,比较dp[i]dp[i- j * j] + 1得到小的那个

方法1:动态规划

  • 思路:dp[i] 表示i的完全平方和的最少数量,dp[i - j * j] + 1表示减去一个完全平方数j的完全平方之后的数量加1就等于dp[i],只要在dp[i], dp[i - j * j] + 1中寻找一个较少的就是最后dp[i]的值。

  • 复杂度:时间复杂度O(n* sqrt(n)),n是输入的整数,需要循环n次,每次计算dp方程的复杂度sqrt(n),空间复杂度O(n)
class Solution {
    public int numSquares(int n) {
        //1.定义状态
        int[] dp = new int[n + 1];

        for (int i = 1; i <= n; i++) {
            //2.初始化 表示都由1这个完全平方数组成
            dp[i] = i;
            for (int j = 1; i - j * j >= 0; j++) {
                //3.状态转移方程
                dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
            }
        }

        return dp[n];
    }
}

image-20221220032253897

120. 三角形最小路径和

难度中等

给定一个三角形 triangle ,找出自顶向下的最小路径和。

每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 ii + 1

示例 1:

输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
   2
  3 4
 6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。

示例 2:

输入:triangle = [[-10]]
输出:-10

提示:

  • 1 <= triangle.length <= 200
  • triangle[0].length == 1
  • triangle[i].length == triangle[i - 1].length + 1
  • -104 <= triangle[i][j] <= 104

进阶:

  • 你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题吗?

ds_72

  • 思路:从三角形最后一层开始向上遍历,每个数字的最小路径和是它下面两个数字中的较小者加上它本身
  • 复杂度分析:时间复杂度O(n^2),空间复杂O(n)

1.动态规划

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        //1. 定义状态
        int n = triangle.size();
        //表示从底部到nn位置的路径长度
        int dp[][] = new int[n][n];

        //2. 初始化
        for (int i = 0; i < n; i++) {
            //获取三角形最后一行
            dp[n-1][i] = triangle.get(n-1).get(i);
        }

        //3. 状态转移方程
        //每个数字的最小路径和是它下面两个数字中的较小者加上它本身
        //从倒数第2层开始
        for (int i = n-2; i >= 0; i--) {
            //第几层就有有多少个元素
            for (int j = 0; j <= i; j++) {
                dp[i][j] = triangle.get(i).get(j) + Math.min(dp[i+1][j] , dp[i+1][j+1]);
            }
        }

        return dp[0][0];
    }
}

image-20221220035808817

2.状态压缩

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        //1. 定义状态
        int n = triangle.size();
        int dp[] = new int[n];

        //2. 初始化
        for (int i = 0; i < n; i++) {
            //获取三角形最后一行
            dp[i] = triangle.get(n-1).get(i);
        }

        //3. 状态转移方程
        //每个数字的最小路径和是它下面两个数字中的较小者加上它本身
        //从倒数第2层开始
        for (int i = n-2; i >= 0; i--) {
            //第几层就有有多少个元素
            for (int j = 0; j <= i; j++) {
                dp[j] = triangle.get(i).get(j) + Math.min(dp[j] , dp[j+1]);
            }
        }

        return dp[0];
    }
}

image-20221220035039633

152. 乘积最大子数组(midium)

难度中等

给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

测试用例的答案是一个 32-位 整数。

子数组 是数组的连续子序列。

示例 1:

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

示例 2:

输入: nums = [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。

提示:

  • 1 <= nums.length <= 2 * 104
  • -10 <= nums[i] <= 10
  • nums 的任何前缀或后缀的乘积都 保证 是一个 32-位 整数

思路

ds_73

  1. 我们需要同时维护,目前子数列的最大值和最小值,因为负负可以得正
    我们建立一个二维数组dp[0...i][0/1]

    • dp[i][0]表示从第0项到第i项的子数组的最小乘积
    • dp[i][1]表示从第0项到第i项的子数组的最大乘积
  2. 初始状态:dp[0][0]=nums[0], dp[0][1]=nums[0]

  3. 分情况讨论:

    • 因为nums[i]的值不确定是否为正数,所以i这个位置的最小乘积有三种情况,我们取其中最小的

      1. 最小值*当前值 max = dp[i-1][0] * nums[i]
      2. 最大值*当前值 max = dp[i-1][1] * nums[i]
      3. 当前值 min = nums[i]
    • 因为nums[i]的值不确定是否为正数,所以i这个位置的最大乘积有三种情况,我们取其中最大的

      1. 最小值*当前值 max = dp[i-1][0] * nums[i]

      2. 最大值*当前值 max = dp[i-1][1] * nums[i]

      3. 当前值 max = nums[i]

  4. 状态转移方程:

    • dp[i] [0]=min(dp[i−1] [0]∗num[i] , dp[i−1] [1] ∗ num[i], num[i])

    • dp[i] [1]=max(dp[i−1] [0]∗num[i] , dp[i−1] [1] ∗ num[i], num[i])

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

1. 动态规划

class Solution {
    public int maxProduct(int[] nums) {
        // 1. 定义状态
        int length = nums.length;
        int dp[][] =new int[length][2];
        int res = nums[0];

        // 2. 初始化
        dp[0][0] = nums[0];
        dp[0][1] = nums[0];

        // 3. 递推
        /**
         * dp[i] [0]=min(dp[i−1] [0]∗num[i] , dp[i−1] [1] ∗ num[i], num[i])
         * dp[i] [1]=max(dp[i−1] [0]∗num[i] , dp[i−1] [1] ∗ num[i], num[i])
         */
        for (int i = 1; i < length; i++) {
            dp[i][0] = Math.min(Math.min(dp[i - 1][0] * nums[i], dp[i - 1][1] * nums[i]), nums[i]);
            dp[i][1] = Math.max(Math.max(dp[i - 1][0] * nums[i], dp[i - 1][1] * nums[i]), nums[i]);
            res = Math.max(res, dp[i][1]);
        }

        return res;

    }
}

image-20221222024756630

2.状态压缩

  1. 状态压缩:dp[i][x]只与dp[i][x]-1,所以只需定义两个变量,prevMin = nums[0]prevMax = nums[0]
  2. 状态压缩之后的方程:
    • prevMin = Math.min(prevMin * num[i], prevMax * num[i], nums[i])
    • prevMax = Math.max(prevMin * num[i], prevMax * num[i], nums[i])
class Solution {
    public int maxProduct(int[] nums) {
        // 1. 定义状态
        // 2. 初始化
        int length = nums.length;
        int prevMin = nums[0];
        int prevMax = nums[0];
        int res = nums[0];
        int temp1 = 0, temp2 = 0;
        // 3. 递推
        for (int i = 1; i < length; i++) {
            //这里不使用temp,直接放进去结果是错的,不知道为什么
            temp1 = prevMin * nums[i];
            temp2 = prevMax * nums[i];
            prevMin = Math.min(Math.min(temp1, temp2), nums[i]);
            prevMax = Math.max(Math.max(temp1, temp2), nums[i]);
            res = Math.max(res, prevMax);
        }

        return res;

    }
}

image-20221222025844789

买卖股票问题

ds_74
[121. 买卖股票的最佳时机](easy)限定交易次数 k=1
[122. 买卖股票的最佳时机 II](medium)交易次数无限制 k = +infinity
[123. 买卖股票的最佳时机 III] (hrad) 限定交易次数 k=2
[188. 买卖股票的最佳时机 IV](hard) 限定交易次数 最多次数为 k
[309. 最佳买卖股票时机含冷冻期](medium) 含有交易冷冻期
[714. 买卖股票的最佳时机含手续费] (medium) 每次交易含手续费

第5,6道题相当于在第2道题的基础上加了冷冻期和手续费的条件。

限制条件

  • 先买入才能卖出
  • 不能同时参加多笔交易,再次买入时,需要先卖出
  • k >= 0才能进行交易,否则没有交易次数

定义操作

  • 买入
  • 卖出
  • 不操作

定义状态

  • i: 天数
  • k: 交易次数,每次交易包含买入和卖出,这里我们只在买入的时候需要将 k - 1
  • 0: 不持有股票
  • 1: 持有股票

举例

dp[i][k][0]//第i天 还可以交易k次 手中没有股票
dp[i][k][1]//第i天 还可以交易k次 手中有股票

最终的最大收益是dp[n - 1][k][0]而不是dp[n - 1][k][1],因为最后一天卖出肯定比持有收益更高

状态转移方程

// 今天没有持有股票,分为两种情况:
// 1. dp[i - 1][k][0],昨天没有持有,今天不操作。 
// 2. dp[i - 1][k][1] + prices[i] 昨天持有,今天卖出,今天手中就没有股票了。
dp[i][k][0] = Math.max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i])


// 今天持有股票,分为两种情况:
// 1.dp[i - 1][k][1] 昨天持有,今天不操作
// 2.dp[i - 1][k - 1][0] - prices[i] 昨天没有持有,今天买入。
dp[i][k][1] = Math.max(dp[i - 1][k][1], dp[i - 1][k - 1][0] - prices[i])

//最大利润就是这俩种情况的最大值

121. 买卖股票的最佳时机(easy)限定交易次数 k=1

k是固定值1,不影响结果,所以可以不用管,简化之后如下

dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i])
dp[i][1] = Math.max(dp[i - 1][1], -prices[i])
1.动态规划

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

class Solution {
    public int maxProfit(int[] prices) {
        // 1.定义状态  只能一次交易,可以省去一个维度k
        //  dp[i][1] 第i天持有股票时候最大利润
        //  dp[i][0] 第i天不持有股票时候最大利润
        int[][] dp = new int[prices.length][2];

        //2.初始化
        // 第1天持有股票的话 利润为-prices[0]
        // 第1天不持有股票得话 利润为 0
        dp[0][1] = -prices[0];
        dp[0][0] = 0;

        //3.状态转移方程
        for (int i = 1; i < prices.length; i++) {
            // 第i天持有股票: i-1天持股票  i-1天不持股票(买入)====> 0 - prices[i]
            dp[i][1] = Math.max(dp[i-1][1], -prices[i]); //这里注意,因为就一次,直接0-prices[i]就行了
            // 第i天不持有股票: i-1天不持股票  i-1天持股票(卖出)+prices[i]
            dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1] + prices[i]);
        }

        //最后一天不持股肯定比持股利润高
        return dp[prices.length-1][0];

    }
}

image-20221222032555928

2.状态压缩

状态压缩,dp[i] 只和 dp[i - 1] 有关,去掉一维

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

class Solution {
    public int maxProfit(int[] prices) {
        // 1.定义状态  只能一次交易,可以省去一个维度k
        //  dp[1] 持有股票时候最大利润
        //  dp[0] 不持有股票时候最大利润
        int[] dp = new int[2];

        //2.初始化
        // 第1天持有股票的话 利润为-prices[0]
        // 第1天不持有股票得话 利润为 0
        dp[1] = -prices[0];
        dp[0] = 0;

        //3.状态转移方程
        for (int i = 1; i < prices.length; i++) {
            // 第i天持有股票: i-1天持股票  i-1天不持股票(买入)====> 0 - prices[i]
            dp[1] = Math.max(dp[1], -prices[i]); //这里注意,因为就一次,直接0-prices[i]就行了
            // 第i天不持有股票: i-1天不持股票  i-1天持股票(卖出)+prices[i]
            dp[0] = Math.max(dp[0],dp[1] + prices[i]);
        }

        //最后一天不持股肯定比持股利润高
        return dp[0];

    }
}

image-20221222033001705

3.语意化

分为have 和 no 更加好理解 ,数组换为变量了

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

class Solution {
    public int maxProfit(int[] prices) {
        // 1.定义状态  只能一次交易,可以省去一个维度k
        //  dp[1] 持有股票时候最大利润
        //  dp[0] 不持有股票时候最大利润
  
        //2.初始化
        // 第1天持有股票的话 利润为-prices[0]
        // 第1天不持有股票得话 利润为 0
        int have = -prices[0];
        int no = 0;

        //3.状态转移方程
        for (int i = 1; i < prices.length; i++) {
            // 第i天持有股票: i-1天持股票  i-1天不持股票(买入)====> 0 - prices[i]
            have = Math.max(have, -prices[i]); //这里注意,因为就一次,直接0-prices[i]就行了
            // 第i天不持有股票: i-1天不持股票  i-1天持股票(卖出)+prices[i]
            no = Math.max(no,have + prices[i]);
        }

        //最后一天不持股肯定比持股利润高
        return no;

    }
}

image-20221222033853976

122. 买卖股票的最佳时机 II(medium)交易次数无限制 k = +infinity

状态转移方程

//第i天不持有 由 第i-1天不持有然后不操作 和 第i-1天持有然后卖出 两种情况的最大值转移过来
dp[i][k][0] = Math.max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i])
//第i天持有 由 第i-1天持有然后不操作 和 第i-1天不持有然后买入 两种情况的最大值转移过来
dp[i][k][1] = Math.max(dp[i - 1][k][1], dp[i - 1][k - 1][0] - prices[i])

k同样不影响结果,简化之后如下

dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i])
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i])
1.动态规划
class Solution {
    public int maxProfit(int[] prices) {
        //1.定义状态
        int n = prices.length;
        //  dp[i][1] 第i天持有股票时候最大利润
        //  dp[i][0] 第i天不持有股票时候最大利润
        int[][] dp = new int[n][2];

        //2.初始化
        // 第1天持有股票的话 利润为-prices[0]
        // 第1天不持有股票得话 利润为 0
        dp[0][1] = -prices[0];
        dp[0][0] = 0;

        //3.状态转移方程
        for (int i = 1; i < prices.length; i++) {
            // 第i天持有股票: i-1天持股票  i-1天不持股票(买入) - prices[i]
            dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0] -prices[i]); 
            // 第i天不持有股票: i-1天不持股票  i-1天持股票(卖出)+prices[i]
            dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1] + prices[i]);
        }

        return dp[n-1][0];

    }
}
2.状态压缩

状态压缩,同样dp[i] 只和 dp[i - 1] 有关,去掉一维

class Solution {
    public int maxProfit(int[] prices) {
        //1.定义状态
        //  dp[1] 持有股票时候最大利润
        //  dp[0] 不持有股票时候最大利润
        int[] dp = new int[2];

        //2.初始化
        // 第1天持有股票的话 利润为-prices[0]
        // 第1天不持有股票得话 利润为 0
        dp[1] = -prices[0];
        dp[0] = 0;

        //3.状态转移方程
        for (int i = 1; i < prices.length; i++) {
            // 第i天持有股票: i-1天持股票  i-1天不持股票(买入) - prices[i]
            dp[1] = Math.max(dp[1],dp[0] -prices[i]); 
            // 第i天不持有股票: i-1天不持股票  i-1天持股票(卖出)+prices[i]
            dp[0] = Math.max(dp[0],dp[1] + prices[i]);
        }

        return dp[0];

    }
}
3. 语意化
class Solution {
    public int maxProfit(int[] prices) {
        //1.定义状态
        //  dp[1] 持有股票时候最大利润
        //  dp[0] 不持有股票时候最大利润

        //2.初始化
        // 第1天持有股票的话 利润为-prices[0]
        // 第1天不持有股票得话 利润为 0
        int have = -prices[0];
        int no = 0;

        //3.状态转移方程
        for (int i = 1; i < prices.length; i++) {
            // 第i天持有股票: i-1天持股票  i-1天不持股票(买入) - prices[i]
            have = Math.max(have,no -prices[i]);
            // 第i天不持有股票: i-1天不持股票  i-1天持股票(卖出)+prices[i]
            no = Math.max(no,have + prices[i]);
        }

        return no;

    }
}

123. 买卖股票的最佳时机 III

难度困难

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
     随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。

示例 2:

输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。   
     注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。   
     因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例 3:

输入:prices = [7,6,4,3,1] 
输出:0 
解释:在这个情况下, 没有交易完成, 所以最大利润为 0。

示例 4:

输入:prices = [1]
输出:0

提示:

  • 1 <= prices.length <= 105
  • 0 <= prices[i] <= 105
思路

状态转移方程

dp[i][k][0] = Math.max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i])
dp[i][k][1] = Math.max(dp[i - 1][k][1], dp[i - 1][k - 1][0] - prices[i])

k对结果有影响 不能舍去,只能对k进行循环

1.动态规划
class Solution {
    public int maxProfit(int[] prices) {
        //1.定义状态
        //dp[i][k][1] 表示第i天进行了k笔交易并且持股时得最大利润
        //dp[i][k][0] 表示第i天进行了k笔交易并且不持股时得最大利润
        int n = prices.length;
        int[][][] dp = new int[n][3][2];

        //2.初始化
        for (int k = 0; k <= 2; k++) {
            dp[0][k][0] = 0;
            dp[0][k][1] = -prices[0];
        }



        //3.递推公式
        for (int i = 1; i < n; i++) {
            //k=0时
            // 第i天不持股 = 0  //不需写
            // 第i天持股 max(第i-1天持股,第i-1天不持股-prices[i])
            dp[i][0][1] = Math.max(dp[i-1][0][1], dp[i-1][0][0] - prices[i]);
            //限定k笔交易,所以对其进行循环
            for (int k = 1; k <=2 ; k++) {
                //第i天不持股 = max(第i-1天不持股,第i-1天持股+prices[i])
                dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k-1][1]+prices[i]);
                //第i天持股 = max(第i-1天持股,第i-1天不持股-prices[i])
                dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k][0]-prices[i]);
            }
        }

        return dp[n-1][2][0];


    }
}

image-20221222175107842

2.状态压缩

状态压缩,同样dp[i] 只和 dp[i - 1] 有关,去掉一维

class Solution {
    public int maxProfit(int[] prices) {
        //1.定义状态
        //dp[k][1] 表示进行了k笔交易并且持股时得最大利润
        //dp[k][0] 表示进行了k笔交易并且不持股时得最大利润
        int n = prices.length;
        int[][] dp = new int[3][2];

        //2.初始化
        for (int k = 0; k <= 2; k++) {
            dp[k][0] = 0;
            dp[k][1] = -prices[0];
        }

        //3.递推公式
        for (int i = 1; i < n; i++) {
            //k=0时
            // 第i天不持股 = 0  //不需写
            // 第i天持股 max(第i-1天持股,第i-1天不持股-prices[i])
            dp[0][1] = Math.max(dp[0][1], dp[0][0] - prices[i]);
            //限定k笔交易,所以对其进行循环
            for (int k = 1; k <=2 ; k++) {
                //第i天不持股 = max(第i-1天不持股,第i-1天持股+prices[i])
                dp[k][0] = Math.max(dp[k][0], dp[k-1][1]+prices[i]);
                //第i天持股 = max(第i-1天持股,第i-1天不持股-prices[i])
                dp[k][1] = Math.max(dp[k][1], dp[k][0]-prices[i]);
            }
        }

        return dp[2][0];
    }
}

image-20221222180722838

3.降维
class Solution {
    public int maxProfit(int[] prices) {
        //1.定义状态

        int n = prices.length;
        //have[k] 表示进行了k笔交易并且持股时得最大利润
        //no[k] 表示进行了k笔交易并且不持股时得最大利润
        int[] have = new int[3];
        int[] no = new int[3];

        //2.初始化
        for (int k = 0; k <= 2; k++) {
            no[k] = 0;
            have[k] = -prices[0];
        }

        //3.递推公式
        for (int i = 1; i < n; i++) {
            //k=0时
            // 第i天不持股 = 0  //不需写
            // 第i天持股 max(第i-1天持股,第i-1天不持股-prices[i])
            have[0] = Math.max(have[0], no[0]-prices[i]);
            //限定k笔交易,所以对其进行循环
            for (int k = 1; k <=2 ; k++) {
                //第i天不持股 = max(第i-1天不持股,第i-1天持股+prices[i])
                no[k] = Math.max(no[k], have[k-1] + prices[i]);
                //第i天持股 = max(第i-1天持股,第i-1天不持股-prices[i])
                have[k] = Math.max(have[k], no[k]-prices[i]);
            }
        }

        return no[2];
    }
}

image-20221222180707940

188. 买卖股票的最佳时机 IV

难度困难

给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。

**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。

示例 2:

输入:k = 2, prices = [3,2,6,5,0,3]
输出:7
解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
     随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。

提示:

  • 0 <= k <= 100
  • 0 <= prices.length <= 1000
  • 0 <= prices[i] <= 1000
限制

限定交易次数 最多次数为 k

1.动态规划
class Solution {
    public int maxProfit(int k, int[] prices) {
        //1.定义状态
        //dp[i][k][1] 表示第i天进行了k笔交易并且持股时得最大利润
        //dp[i][k][0] 表示第i天进行了k笔交易并且不持股时得最大利润
        int n = prices.length;
        int[][][] dp =  new int[n][k+1][2];


        //初始化
        for (int temp_k = 0; temp_k <= k; temp_k++) {
            dp[0][temp_k][0] = 0; //第一天不持股利润为0
            dp[0][temp_k][1] = -prices[0]; //第一天持股利润为-prices[0]
        }

        for (int i = 1; i < n ; i++) {
            //k=0  不持股为0
            dp[i][0][0] = 0;
            dp[i][0][1] = Math.max(dp[i-1][0][1], dp[i-1][0][0] - prices[i]);
            //k!=0
            for (int temp_k = 1; temp_k <= k; temp_k++) {
                // 第i天持有股票: i-1天持股票  i-1天不持股票(买入) - prices[i]
                dp[i][temp_k][1] = Math.max(dp[i-1][temp_k][1] , dp[i-1][temp_k][0] - prices[i]);
                // 第i天不持有股票: i-1天不持股票  i-1天持股票(卖出)+prices[i]
                dp[i][temp_k][0] = Math.max(dp[i-1][temp_k][0] , dp[i-1][temp_k-1][1] + prices[i]);
            }
        }

        return dp[n-1][k][0];
    }
}

image-20221222185228570

2.状态压缩

状态压缩,同样dp[i] 只和 dp[i - 1] 有关,去掉一维

class Solution {
    public int maxProfit(int k, int[] prices) {
        //1.定义状态
        //dp[k][1] 表示进行了k笔交易并且持股时得最大利润
        //dp[k][0] 表示进行了k笔交易并且不持股时得最大利润
        int n = prices.length;
        int[][] dp =  new int[k+1][2];


        //初始化
        for (int temp_k = 0; temp_k <= k; temp_k++) {
            dp[temp_k][0] = 0; //第一天不持股利润为0
            dp[temp_k][1] = -prices[0]; //第一天持股利润为-prices[0]
        }

        for (int i = 1; i < n ; i++) {
            //k=0  不持股为0
            dp[0][0] = 0;
            dp[0][1] = Math.max(dp[0][1], dp[0][0] - prices[i]);
            //k!=0
            for (int temp_k = 1; temp_k <= k; temp_k++) {
                // 第i天持有股票: i-1天持股票  i-1天不持股票(买入) - prices[i]
                dp[temp_k][1] = Math.max(dp[temp_k][1] , dp[temp_k][0] - prices[i]);
                // 第i天不持有股票: i-1天不持股票  i-1天持股票(卖出)+prices[i]
                dp[temp_k][0] = Math.max(dp[temp_k][0] , dp[temp_k-1][1] + prices[i]);
            }
        }

        return dp[k][0];
    }
}

image-20221222185634367

3.降维
class Solution {
    public int maxProfit(int k, int[] prices) {
        //1.定义状态
        //have[k] 表示进行了k笔交易并且持股时得最大利润
        //no[k] 表示进行了k笔交易并且不持股时得最大利润
        int n = prices.length;
        int[] have = new int[k+1];
        int[] no = new int[k+1];


        //初始化
        for (int temp_k = 0; temp_k <= k; temp_k++) {
            no[temp_k] = 0; //第一天不持股利润为0
            have[temp_k]= -prices[0]; //第一天持股利润为-prices[0]
        }

        for (int i = 1; i < n ; i++) {
            //k=0  不持股为0
            no[0] = 0;
            have[0] = Math.max(have[0], no[0] - prices[i]);
            //k!=0
            for (int temp_k = 1; temp_k <= k; temp_k++) {
                // 第i天持有股票: i-1天持股票  i-1天不持股票(买入) - prices[i]
                have[temp_k] = Math.max(have[temp_k] , no[temp_k] - prices[i]);
                // 第i天不持有股票: i-1天不持股票  i-1天持股票(卖出)+prices[i]
                no[temp_k] = Math.max(no[temp_k] , have[temp_k-1] + prices[i]);
            }
        }

        return no[k];
    }
}

image-20221222190353320

309. 最佳买卖股票时机含冷冻期

难度中等

给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

  • 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入: prices = [1,2,3,0,2]
输出: 3 
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]

示例 2:

输入: prices = [1]
输出: 0

提示:

  • 1 <= prices.length <= 5000
  • 0 <= prices[i] <= 1000
思路

状态转移方程

dp[i][k][0] = Math.max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i])
//冷却时间1天,所以要从 i - 2 天转移状态
//买入,卖出 ---- 冷冻期 ----  买入,卖出
//注意这里使用了i-2 所以会左溢出,需要把i=1时单独拉出来
dp[i][k][1] = Math.max(dp[i - 1][k][1], dp[i - 2][k - 1][0] - prices[i])

题目不限制k的大小,可以舍去

1.动态规划
class Solution {
    public int maxProfit(int[] prices) {
        //1.定义状态
        //dp[i][1] 表示第i天持股时得最大利润
        //dp[i][0] 表示第i天不持股时得最大利润
        int n = prices.length;
        int[][] dp = new int[n][2];

        //2.初始化
        dp[0][0] = 0;
        dp[0][1] = -prices[0];

        //3.递推公式
        for (int i = 1; i < n; i++) {


            //第i天不持股 = max(第i-1天不持股,第i-1天持股+prices[i])
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]);


            //如果是第一天,不存在冷冻期的问题
            if(i == 1){
                //第i天持股 = max(第i-1天持股,第i-1天不持股-prices[i])
                dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0]-prices[i]);
            }else{
                //冷却时间1天,第i天持股 = max(第i-1天持股,第i-2天不持股-prices[i])
                //买入,卖出 ---- 冷冻期 ----  买入,卖出
                dp[i][1] = Math.max(dp[i-1][1], dp[i-2][0]-prices[i]);
            }

        }

        return dp[n-1][0];

    }
}

image-20221222195553764

2.降维
class Solution {
    public int maxProfit(int[] prices) {
        //1.定义状态
        //have[n] 表示第i天持股时得最大利润
        //no[n] 表示第i天不持股时得最大利润
        int n = prices.length;
        int[] have = new int[n];
        int[] no = new int[n];

        //2.初始化
        no[0] = 0;
        have[0] = -prices[0];

        //3.递推公式
        for (int i = 1; i < n; i++) {
            //第i天不持股 = max(第i-1天不持股,第i-1天持股+prices[i])
            no[i] = Math.max(no[i-1], have[i-1]+prices[i]);

            //如果是第一天,不存在冷冻期的问题
            if(i == 1){
                //第i天持股 = max(第i-1天持股,第i-1天不持股-prices[i])
                have[i] = Math.max(have[i-1], no[i-1]-prices[i]);
            }else{
                //冷却时间1天,第i天持股 = max(第i-1天持股,第i-2天不持股-prices[i])
                //买入,卖出 ---- 冷冻期 ----  买入,卖出
                have[i] = Math.max(have[i-1], no[i-2]-prices[i]);
            }

        }

        return no[n-1];
    }
}

image-20221222200141929

714. 买卖股票的最佳时机含手续费

难度中等

给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。

返回获得利润的最大值。

**注意:**这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

示例 1:

输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
输出:8
解释:能够达到的最大利润:  
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8

示例 2:

输入:prices = [1,3,7,5,10,3], fee = 3
输出:6

提示:

  • 1 <= prices.length <= 5 * 104
  • 1 <= prices[i] < 5 * 104
  • 0 <= fee < 5 * 104
1.动态规划
class Solution {
    public int maxProfit(int[] prices, int fee) {
        //1.定义状态
        //dp[i][1] 表示第i天持股时得最大利润
        //dp[i][0] 表示第i天不持股时得最大利润
        int n = prices.length;
        int[][] dp = new int[n][2];

        //2.初始化
        dp[0][0] = 0;
        dp[0][1] = -prices[0];

        //3.递推公式
        for (int i = 1; i < n; i++) {
            //第i天不持股 = max(第i-1天不持股,第i-1天持股+prices[i]-fee)
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]-fee);
            //第i天持股 = max(第i-1天持股,第i-1天不持股-prices[i])
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0]-prices[i]);
        }

        return dp[n-1][0];
    }
}

image-20221222201112488

2.降维
class Solution {
    public int maxProfit(int[] prices, int fee) {
        //1.定义状态
        //have[i] 表示第i天持股时得最大利润
        //no[i] 表示第i天不持股时得最大利润
        int n = prices.length;
        int[] have = new int[n];
        int[] no = new int[n];

        //2.初始化
        no[0] = 0;
        have[0] = -prices[0];

        //3.递推公式
        for (int i = 1; i < n; i++) {
            //第i天不持股 = max(第i-1天不持股,第i-1天持股+prices[i]-fee)
            no[i] = Math.max(no[i-1], have[i-1]+prices[i]-fee);
            //第i天持股 = max(第i-1天持股,第i-1天不持股-prices[i])
            have[i] = Math.max(have[i-1], no[i-1]-prices[i]);
        }

        return no[n-1];
    }
}

image-20221222201625474

3.降维 + 状态压缩
class Solution {
    public int maxProfit(int[] prices, int fee) {
        //1.定义状态
        //have 表示第i天持股时得最大利润
        //no 表示第i天不持股时得最大利润
        int n = prices.length;
        int have;
        int no;

        //2.初始化
        no = 0;
        have = -prices[0];

        //3.递推公式
        for (int i = 1; i < n; i++) {
            //第i天不持股 = max(第i-1天不持股,第i-1天持股+prices[i]-fee)
            no = Math.max(no, have +prices[i]-fee);
            //第i天持股 = max(第i-1天持股,第i-1天不持股-prices[i])
            have = Math.max(have, no-prices[i]);
        }

        return no;
    }
}

image-20221222201752208

322. 零钱兑换

难度中等2233收藏分享切换为英文接收动态反馈

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1

示例 2:

输入:coins = [2], amount = 3
输出:-1

示例 3:

输入:coins = [1], amount = 0
输出:0

提示:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 231 - 1
  • 0 <= amount <= 104
思路

这题和上面的 279题完全平方数 其实非常像

  • 思路:dp[n]表示兑换面额n所需要的最少硬币,因为硬币无限,所以可以自底向上计算dp[n],对于dp[0~i]的每个状态,循环coins数组,寻找可以兑换的组合,用i面额减去当前硬币价值,dp[n-coin]在加上一个硬币数就是dp[i],最后取最小值就是答案,状态转移方程就是dp[n] = Math.min(dp[n], dp[n - coin] + 1);
  • 复杂度分析:时间复杂度是O(sn),s是兑换金额,n是硬币数组长度,一共需要计算s个状态,每个状态需要遍历n个面额来转移状态。空间复杂度是O(s),也就是dp数组的长度

image-20221223015329119

1.动态规划
class Solution {
    public int coinChange(int[] coins, int amount) {
        //1.状态定义
        int[] dp = new int[amount+1];

        //2.初始化
        Arrays.fill(dp,Integer.MAX_VALUE/2);
        dp[0] = 0;

        //3.状态转移
        // dp[n] = min(dp[n], dp[n-coins[0...n] + 1] )
        for (int i = 1; i <= amount ; i++) {
            for (int j = 0; j < coins.length; j++) {
                if(i-coins[j] >= 0){
                    dp[i] = Math.min(dp[i], dp[i-coins[j]]+ 1 );
                }
            }
        }

        //如果结果硬币数大于总金额,说明找不到组合
        return dp[amount] > amount ? -1 : dp[amount];

    }
}

image-20221223021348402

72. 编辑距离

难度困难2710

给你两个单词 word1word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

示例 1:

输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

示例 2:

输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')

提示:

  • 0 <= word1.length, word2.length <= 500
  • word1word2 由小写英文字母组成

思路

ds_76 ds_77
  • 思路:dp[i][j]表示word1前i个字符和word2前j个字符的最少编辑距离。
    1. 如果word1[i-1] === word2[j-1],说明最后一个字符不用操作,此时dp[i][j] = dp[i-1][j-1],即此时的最小操作数和word1和word2都减少一个字符的最小编辑数相同
    2. 如果word1[i-1] !== word2[j-1],则分为三种情况
      1. word1删除最后一个字符,状态转移成dp[i-1][j],即dp[i][j] = dp[i-1][j] + 1,+1指删除操作
      2. word1在最后加上一个字符,状态转移成dp[i][j-1],即dp[i][j] = dp[i][j-1] + 1,+1指增加操作
        增加:dp[i][j] = dp[i+1][j] 此时最后两位相等所以i和j都可以-1 => dp[i][j] = dp[i+1-1][j-1]
      3. word1替换最后一个字符,状态转移成dp[i-1][j-1],即dp[i] [j] = dp[i-1] [j-1] + 1,+1指替换操作
  • 复杂度:时间复杂度是O(mn) ,m是word1的长度,n是word2的长度。空间复杂度是O(mn) ,需要用m * n大小的二维数字存储状态。

从后往前比较两个字符串的最后一位字符,这时候想把word1的最后一位换为word2的最后一位有三种办法

  1. 增加和word2最后一个字符相同的字符到word1末尾
  2. 修改word1的末尾为word2的最后一个字符
  3. 删除word1的最后一个字符

1.动态规划

class Solution {
    public int minDistance(String word1, String word2) {
        //1.定义状态
        //dp[i][j]表示word1前i个字符和word2前j个字符的最少编辑距离
        int m = word1.length();
        int n = word2.length();
        int[][] dp = new int[m+1][n+1];

        //2.初始化
        //word2 前0个字符为空时 删除字符操作就行
        for (int i = 1; i <= m; i++) {
            dp[i][0] = i;
        }
        //word1 前0个字符为空时 添加字符操作就行
        for (int j = 1; j <= n; j++) {
            dp[0][j] = j;
        }

        //3.状态转化
        for (int i = 1; i <= m ; i++) {
            for (int j = 1; j <= n; j++) {

                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    //如果word1的i处字符和word2的j处字符相同,无需操作
                    dp[i][j] = dp[i-1][j-1];
                }else{
                    //如果word1的i处字符和word2的j处字符不同,选出三种操作中操作数最少的 +1
                    // 增加:dp[i][j] = dp[i+1][j] 此时最后两位相等所以i和j都可以-1 => dp[i][j] = dp[i+1-1][j-1]
                    // 删除:dp[i][j] = dp[i-1][j]
                    // 替换:dp[i][j] = dp[i-1][j-1]
                    dp[i][j] = Math.min(dp[i][j-1],Math.min(dp[i-1][j],dp[i-1][j-1])) + 1;
                }
            }
        }

        return dp[m][n];

    }
}

10. 正则表达式匹配(难)

难度困难

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.''*' 的正则表达式匹配。

  • '.' 匹配任意单个字符
  • '*' 匹配零个或多个前面的那一个元素

所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

示例 1:

输入:s = "aa", p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。

示例 2:

输入:s = "aa", p = "a*"
输出:true
解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。

示例 3:

输入:s = "ab", p = ".*"
输出:true
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。

提示:

  • 1 <= s.length <= 20
  • 1 <= p.length <= 30
  • s 只包含从 a-z 的小写字母。
  • p 只包含从 a-z 的小写字母,以及字符 .*
  • 保证每次出现字符 * 时,前面都匹配到有效的字符

思路

ds_78

ds_79

  • 思路:dp[i][j] 表示 s 的前 i 个字符能否和p的前j个字符匹配,分为四种情况,看图
  • 复杂度:时间复杂度O(mn),m,n分别是字符串s和p的长度,需要嵌套循环s和p。空间复杂度O(mn),dp数组所占的空间

312. 戳气球

难度困难

n 个气球,编号为0n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中。

现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的 i - 1i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1i + 1 超出了数组的边界,那么就当它是一个数字为 1 的气球。

求所能获得硬币的最大数量。

示例 1:

输入:nums = [3,1,5,8]
输出:167
解释:
nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins =  3*1*5    +   3*5*8   +  1*3*8  + 1*8*1 = 167

示例 2:

输入:nums = [1,5]
输出:10

提示:

  • n == nums.length
  • 1 <= n <= 300
  • 0 <= nums[i] <= 100

思路

ds_112

  • 思路:dp[i][j] 表示开区间 (i,j) 能拿到的的金币,k是这个区间 最后一个 被戳爆的气球,枚举ij,遍历所有区间,i-j能获得的最大数量的金币等于 戳破当前的气球获得的金钱加上之前i-kk-j区间中已经获得的金币
  • 复杂度:时间复杂度O(n^3),n是气球的数量,三层遍历。空间复杂度O(n^2),dp数组的空间。

343. 整数拆分

难度中等

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

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

示例 1:

输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。

示例 2:

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

提示:

  • 2 <= n <= 58

思路

ds_136

0-1背包问题

0-1背包问题指的是有n个物品和容量为j的背包,weight数组中记录了n个物品的重量,位置i的物品重量是weight[i],value数组中记录了n个物品的价值,位置i的物品价值是vales[i],每个物品只能放一次到背包中,问将哪些物品装入背包,使背包的价值最大。

举例:

ds_214

我们用动态规划的方式来做

  • 状态定义:dp[i][j] 表示从前i个物品里任意取,放进容量为j的背包,价值总和最大是多少
  • 状态转移方程: dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]);
  • 每个物品有放入背包和不放入背包两种情况
    1. j - weight[i]<0:表示装不下i号元素了,不放入背包,此时dp[i][j] = dp[i-1][j],dp[i] [j]取决于前i-1中的物品装入容量为j的背包中的最大价值
    2. j - weight[i]>=0:可以选择放入或者不放入背包。 放入背包则:dp[i][j] = dp[i - 1][j - weight[i]] + value[i]dp[i - 1][j - weight[i]] 表示i-1中的物品装入容量为j-weight[i]的背包中的最大价值,然后在加上放入的物品的价值value[i]就可以将状态转移到dp[i][j]。 不放入背包则:dp[i][j] = dp[i - 1] [j],在这两种情况中取较大者。
  • 初始化dp数组:dp[i][0]表示背包的容积为0,则背包的价值一定是0,dp[0][j]表示第0号物品放入背包之后背包的价值

ds_137

  • 最终需要返回值:就是dp数组的最后一行的最后一列

循环完成之后的dp数组如下图

ds_138

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值