Java动态规划算法从入门的到熟练

算法分析 专栏收录该内容
36 篇文章 0 订阅


动态规划(DP)问题是算法刷题过程中一大类问题,下面是在刷动态规划题目时的一些总结。

一、动态规划

1. 定义

动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。

动态规划算法的基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。

2. 基本思想和策略

由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。

主要是三点:

  1. 拆分问题。就是根据问题的可能性把问题划分成一步一步这样就可以通过递推或者递归来实现。
    关键就是这个步骤:动态规划有一类问题就是从后往前推到。有时候我们很容易知道:如果只有一种情况时,最佳的选择应该怎么做。然后根据这个最佳选择往前一步推导,得到前一步的最佳选择
  2. 定义问题状态和状态之间的关系。前面拆分的步骤之间的关系,用一种量化的形式表现出来,类似于高中学的推导公式,因为这种式子很容易用程序写出来,也可以说对程序比较亲和(也就是最后所说的状态转移方程式)。
  3. 找到最优解。我们应该将最优解保存下来,为了往前推导时能够使用前一步的最优解,在这个过程中难免有一些相比于最优解差的解,此时我们应该放弃,只保存最优解,这样我们每一次都把最优解保存了下来,大大降低了时间复杂度。

3. 解题思路

  1. 将原问题分解为子问题。 (注意:1.子问题与原问题形式相同或类似,只是问题规模变小了,从而变简单了;2,子问题一旦求出就要保存下来,保证每个子问题只求解一遍。)
  2. 确定状态。(状态:在动规解题中,我们将和子问题相关的各个变量的一组取值,称之为一个"状态"。一个状态对应一个或多个子问题所谓的在某个状态的值,这个就是状态所对应的子问题的解,所有状态的集合称为"状态空间"。我的理解就是状态就是某个问题某组变量,状态空间就是该问题的所有组变量。)
    另外:整个问题的时间复杂度就是状态数目乘以每个状态所需要的时间
  3. 确定一些初始状态(边界条件)的值。 (这个视情况而定,千万别以为就是最简单的那个子问题解。)
  4. 确定状态转移方程 (这一步和第三步是最关键的 记住"人人为我"递推,由已知推未知。)

4. 使用情况

  1. 问题具有最优子结构
  2. 无后效性。(一般遇到的求最优解问题都可以用动态规划)

二、算法笔记

下面是力扣刷题过程中的

509. 斐波那契数

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

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

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

示例一:

输入:2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1

示例二:

输入:3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2

代码实现:

class Solution {
    public int fib(int n) {
        if(n <= 1 ) return n;
        int[] dp = new int[n+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];
    }
}

1137. 第 N 个泰波那契数

泰波那契序列 Tn 定义如下:

T0 = 0, T1 = 1, T2 = 1, 且在 n >= 0 的条件下 Tn+3 = Tn + Tn+1 + Tn+2

给你整数 n,请返回第 n 个泰波那契数 Tn 的值。

示例一:

输入:n = 4
输出:4
解释:
T_3 = 0 + 1 + 1 = 2
T_4 = 1 + 1 + 2 = 4

示例二:

输入:n = 25
输出:1389537

代码实现:

class Solution {
    public int tribonacci(int n) {
        // if(n <= 1) return n;
        // if(n == 2) return 1;
        // int[] dp = new int[n+1];
        // dp[0] = 0;
        // dp[1] = 1;
        // dp[2] = 1;
        // for(int i = 3; i <= n; i++){
        //     dp[i] = dp[i-1] + dp[i-2] + dp[i-3];
        // }
        // return dp[n];
        if (n == 0) return 0;
        if (n == 1 || n == 2) return 1;
        int a = 0, b = 1, c = 1;
        for (int i = 3; i <= n; i++) {
            int d = a + b + c;
            a = b;
            b = c;
            c = d;
        }
        return c;
    }
}

70. 爬楼梯

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

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

示例一:

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

示例二:

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

代码实现:

class Solution {
    public int climbStairs(int n) {
        if(n == 1) return 1;
        if(n == 2) return 2;
        int[] dp = new int[n+1];
        dp[1] = 1;
        dp[2] = 2;
        for(int i = 3; i <= n; i++){
            dp[i] = dp[i-1] + dp[i-2];
        }
        return dp[n];
    }
}

746. 使用最小花费爬楼梯

数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。

每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。

请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。

示例一:

输入:cost = [10, 15, 20]
输出:15
解释:最低花费是从 cost[1] 开始,然后走两步即可到阶梯顶,一共花费 15

示例二:

输入:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出:6
解释:最低花费方式是从 cost[0] 开始,逐个经过那些 1 ,跳过 cost[3] ,一共花费 6

代码实现:

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int n = cost.length;
        int[] dp = new int[n + 1];
        dp[0] = dp[1] = 0;
        for (int i = 2; i <= n; i++) {
            dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
        }
        return dp[n];
    }
}

198. 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例一:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4

示例二:

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12

状态转移方程
// 状态转移方程,偷第i个房间,则金额为前i-2的金额加该房间
// 不偷该房间,则最大金额为前i-1个房间的,故最大金额为两者最大的

dp[i] = Math.max(dp[i-2] + nums[i], dp[i-1]); 

只有一间房子:dp[0] = nums[0];
只有两间房子:dp[1] = Math.max(nums[0], nums[1]);

代码实现:

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        int[] dp = new int[n];
        if(n == 1) return nums[0];
        if(n == 2) return Math.max(nums[0], nums[1]);
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0], nums[1]);
        for(int i = 2; i < n; i++){
            // 状态转移方程,偷第i个房间,则金额为前i-2的金额加该房间
            // 不偷该房间,则最大金额为前i-1个房间的,故最大金额为两者最大的
            dp[i] = Math.max(dp[i-2] + nums[i], dp[i-1]);  
        } 
        return dp[n-1];
    }
}

213. 打家劫舍 II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

示例一:

输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2, 因为他们是相邻的。

示例二:

输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4

示例三:

输入:nums = [0]
输出:0

分为两种情况
// 如果偷窃第一间房子,则偷窃范围为0, n - 2;如果偷窃最后一间房子,则偷窃范围为1, n - 1

代码实现:

class Solution {
    public int rob(int[] nums) {
        // 因为是一个圆,所以不能偷完第一个偷最后一个
        int n = nums.length;
        if(n == 1) return nums[0];
        if(n == 2) return Math.max(nums[0], nums[1]);
        // 如果偷窃第一间房子,则偷窃范围为0, n - 2;如果偷窃最后一间房子,则偷窃范围为1, n - 1
        return Math.max(robRange(nums, 0, n - 2), robRange(nums, 1, n - 1));
    }
    public int robRange(int[] nums, int start, int end){
        int first = nums[start], second = Math.max(nums[start], nums[start + 1]);
        for (int i = start + 2; i <= end; i++) {
            int temp = second;
            second = Math.max(first + nums[i], second);
            first = temp;
        }
        return second;
    }
}

740. 删除并获得点数

给你一个整数数组 nums ,你可以对它进行一些操作。

每次操作中,选择任意一个 nums[i] ,删除它并获得 nums[i] 的点数。之后,你必须删除 所有 等于 nums[i] - 1 和 nums[i] + 1 的元素。

开始你拥有 0 个点数。返回你能通过这些操作获得的最大点数。

示例一:

输入:nums = [3,4,2]
输出:6
解释:
删除 4 获得 4 个点数,因此 3 也被删除。
之后,删除 2 获得 2 个点数。总共获得 6 个点数。

示例二:

输入:nums = [2,2,3,3,3,4]
输出:9
解释:
删除 3 获得 3 个点数,接着要删除两个 24 。
之后,再次删除 3 获得 3 个点数,再次删除 3 获得 3 个点数。
总共获得 9 个点数。

思路:
根据题意,在选择了元素 x 后,该元素以及所有等于 x−1 或 x+1 的元素会从数组中删去。若还有多个值为 x 的元素,由于所有等于 x−1 或 x+1 的元素已经被删除,我们可以直接删除 x 并获得其点数。因此若选择了 x,所有等于 x 的元素也应一同被选择,以尽可能多地获得点数。

记元素 x 在数组中出现的次数为 cx,我们可以用一个数组 sum 记录数组 num 中所有相同元素之和,即 sum[x]=x⋅cx。若选择了 x,则可以获取 sum[x]] 的点数,且无法再选择 x−1 和 x+1。

代码实现:

class Solution {
    public int deleteAndEarn(int[] nums) {
        int maxVal = 0;
        for (int val : nums) {
            maxVal = Math.max(maxVal, val);
        }
        int[] sum = new int[maxVal + 1];
        for (int val : nums) {
            sum[val] += val;
        }
        return rob(sum);
    }

    public int rob(int[] nums) {
        int size = nums.length;
        int first = nums[0], second = Math.max(nums[0], nums[1]);
        for (int i = 2; i < size; i++) {
            int temp = second;
            second = Math.max(first + nums[i], second);
            first = temp;
        }
        return second;

    }
}

55. 跳跃游戏

给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标。

示例一:

输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 13 步到达最后一个下标。

示例二:

输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。

思路:
贪心算法:

for循环存储每个位置最远所能达到的位置

right = Math.max(right, i + nums[i]);

如果能达到最后一个元素,则表明可以跳到。

代码实现:

class Solution {
    public boolean canJump(int[] nums) {
        int right = nums[0]; // 存储最远可达到位置
        for(int i = 0; i < nums.length; i++){
            if(i <= right){
                right = Math.max(right, i + nums[i]);
                if(right >= nums.length-1){
                    return true;
                }
            }
        }
        return false;
    }
}

45. 跳跃游戏 II

给你一个非负整数数组 nums ,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

你的目标是使用最少的跳跃次数到达数组的最后一个位置。

假设你总是可以到达数组的最后一个位置。

示例一:

输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
     从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。

示例二:

输入: nums = [2,3,0,1,4]
输出: 2

贪心算法,每次都跳最远距离

求最少的跳跃次数,和Ⅰ相同,首先计算每个位置最远能到达的位置
记录该位置,步数加1.

代码实现:

class Solution {
    public int jump(int[] nums) {
        int length = nums.length;
        int end = 0;
        int maxPosition = 0; 
        int steps = 0;
        for (int i = 0; i < length - 1; i++) {
            maxPosition = Math.max(maxPosition, i + nums[i]);  // 找到所能达到的最远位置
            if (i == end) { // end为最远位置,到达则步数加1
                end = maxPosition;
                steps++;
            }
        }
        return steps;
    }
}

53. 最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例一:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6

示例二:

输入:nums = [1]
输出:1

示例三:

输入:nums = [-1]
输出:-1

dp[i] 表示以第 i 个数结尾的连续子数组的最大和,找最大的dp[i]

nums[i] += Math.max(nums[i-1], 0) 

以 i 结尾的最大子序和等于以 i 结尾的元素加上上一个元素与0的最大值
最大子序和等于更新的res与nums[i]的最大值

代码实现:

class Solution {
    public int maxSubArray(int[] nums) {
        int res = nums[0]; //存储和的最大值
        for(int i = 1; i < nums.length; i++){
            nums[i] += Math.max(nums[i-1], 0);
            res = Math.max(res, nums[i]);
        }
        return res;
    }
}

918. 环形子数组的最大和

给定一个由整数数组 A 表示的环形数组 C,求 C 的非空子数组的最大可能和。

在此处,环形数组意味着数组的末端将会与开头相连呈环状。(形式上,当0 <= i < A.length 时 C[i] = A[i],且当 i >= 0 时 C[i+A.length] = C[i])

此外,子数组最多只能包含固定缓冲区 A 中的每个元素一次。(形式上,对于子数组 C[i], C[i+1], …, C[j],不存在 i <= k1, k2 <= j 其中 k1 % A.length = k2 % A.length)

示例一:

输入:[1,-2,3,-2]
输出:3
解释:从子数组 [3] 得到最大和 3

示例二:

输入:[5,-3,5]
输出:10
解释:从子数组 [5,5] 得到最大和 5 + 5 = 10

示例三:

输入:[3,-1,2,-1]
输出:4
解释:从子数组 [2,-1,3] 得到最大和 2 + (-1) + 3 = 4

分两种情况:
1.最大数组在中间,与最大子序和相同
2.最大数组跨越头尾,两边大中间小,找最小

代码实现:

class Solution {
    public int maxSubarraySumCircular(int[] nums) {
        //两种情况,第一种最大数组在中间
        int dp = nums[0], max = dp, sum = dp, min = 0;
        for(int i = 1; i < nums.length; i++){
            sum += nums[i];  // 统计数组元素总和
            dp = nums[i] + Math.max(dp, 0);
            max = Math.max(dp, max);
        }
        // 第二种情况,最大数组和跨越头尾,两边大中间最小,找最小
        dp = nums[0];
        for(int i = 1; i < nums.length-1; i++){
            dp = nums[i] + Math.min(dp, 0);
            min = Math.min(dp, min);
        }

        return Math.max(sum-min, max);
    }
}

152. 乘积最大子数组

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

示例一:

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

示例二:

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

动态规划,但要考虑遇到正负两种情况。

代码实现:

class Solution {
    public int maxProduct(int[] nums) {
        // 与最大连续子数组和不同,乘积需要考虑正负
        int max = Integer.MIN_VALUE, imax = 1, imin = 1;
        for(int i = 0; i < nums.length; i++){
            // 如果是负数,会导致最大的变最小,最小的变最大,交换两个值
            if(nums[i] < 0){
                int tmp = imax;
                imax = imin;
                imin = tmp;
            }
            imax = Math.max(imax * nums[i], nums[i]);  // 存储正的最大值
            imin = Math.min(imin * nums[i], nums[i]);  // 存储负的最小值
            max = Math.max(max, imax);
        }
        return max;
    }
}

1567. 乘积为正数的最长子数组长度

给你一个整数数组 nums ,请你求出乘积为正数的最长子数组的长度。

一个数组的子数组是由原数组中零个或者更多个连续数字组成的数组。

请你返回乘积为正数的最长子数组长度。

示例一:

输入:nums = [1,-2,-3,4]
输出:4
解释:数组本身乘积就是正数,值为 24

示例二:

输入:nums = [0,1,-2,-3,-4]
输出:3
解释:最长乘积为正数的子数组为 [1,-2,-3] ,乘积为 6 。
注意,我们不能把 0 也包括到子数组中,因为这样乘积为 0 ,不是正数。

示例三:

输入:nums = [-1,-2,-3,0,1]
输出:2
解释:乘积为正数的最长子数组是 [-1,-2] 或者 [-2,-3]

示例四:

输入:nums = [-1,2]
输出:1

代码实现:

class Solution {
    public int getMaxLen(int[] nums) {
        int len= nums.length;
        int positive =nums[0]>0?1:0;
        int negative =nums[0]<0?1:0;
        int max = positive;
        for(int i = 1 ;i<len;i++){
            if(nums[i]>0){
                positive++;//接下的数是 正数,直接加1就完事
                negative=negative>0?negative+1:0; //负数×正数=负数  
                // 所以1.只要negetive存在 长度就加一 2.不存在 只有一个正数   负数在i位置长度为0
            }
            else if(nums[i]<0){
                //nums<0  正数×负数 变号->负连续 不管正数是多少 ,往后加1
                int newNegative = positive+1;
                //nums<0 negative>0  负数×负数 变号->正连续 因为要求正连续 所以把这个值给positive,如果前面没有负数,那么就为0 
                int newPositive = negative>0?negative+1:0;                
                positive=newPositive;
                negative=newNegative;        
            }
            else{//因为nums=0直接砍断了连续,长度直接变成0
                positive=0;
                negative=0;
            }
            max=Math.max(max,positive);//每个地方 都可能最长 所以都要比较一下
        }
    return max;
    }
}

1014. 最佳观光组合

给你一个正整数数组 values,其中 values[i] 表示第 i 个观光景点的评分,并且两个景点 i 和 j 之间的 距离 为 j - i。

一对景点(i < j)组成的观光组合的得分为 values[i] + values[j] + i - j ,也就是景点的评分之和 减去 它们两者之间的距离。

返回一对观光景点能取得的最高分。

示例一:

输入:values = [8,1,5,2,6]
输出:11
解释:i = 0, j = 2, values[i] + values[j] + i - j = 8 + 5 + 0 - 2 = 11

示例二:

输入:values = [1,2]
输出:2

代码实现:

class Solution {
    public int maxScoreSightseeingPair(int[] values) {
        // 变形为 values[i]+i 和 values[j]-j 两部分,分别求最大值
        int left = values[0], res= Integer.MIN_VALUE;
        for(int i = 1; i < values.length; i++){
            res = Math.max(res, left + values[i] - i);  // 更新
            left = Math.max(left, values[i] + i);  // 更新values[i]+i的最大值
        }
        return res;
    }
}

121. 买卖股票的最佳时机

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

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

示例一:

输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例二:

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

代码实现:

class Solution {
    public int maxProfit(int[] prices) {
        // dp[i]表示前i天获得的最大利润,等于前i-1的最大利润,或第i天卖出的最大利润
        // dp[i] = max(dp[i-1], prices[i]- min(prices[0,i-1]))
        int profit = 0, cost = Integer.MAX_VALUE;
        // profit 更新最大利润, cost更新前i-1最低价格
        for(int price : prices){
            cost = Math.min(cost, price);
            profit = Math.max(profit, price - cost);
        }
        return profit;
    }
}

122. 买卖股票的最佳时机 II

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

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

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

示例一:

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

示例二:

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

示例三:

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

代码实现:

class Solution {
    public int maxProfit(int[] prices) {
        // 1.贪心算法,只要后一天比前一天大,就把差值加一下
        // int res = 0;
        // for(int i = 1; i < prices.length; i++){
        //     if(prices[i] > prices[i-1]){
        //         res += prices[i] - prices[i-1];
        //     }
        // }
        // return res;

        // 2.动态规划
        // dp[i][0] 表示第i天交易后手里没有股票的最大利润,dp[i][1] 表示第i天交易后手里持有股票的最大利润
        // dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]) 可能是第i-1天没有股票的利润,也可能是第i-1天持有,加上卖出的利润
        // dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i]) 可能是第i-1天持有股票的利润,也可能是第i-1天没有,加上买入的利润

        int[][] dp = new int[prices.length][2];
        dp[0][0] = 0;
        dp[0][1] = - prices[0];
        for(int i = 1; i < prices.length; i++){
            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]);
        }
        return dp[prices.length-1][0];
    }
}

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

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。​

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

你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

示例一:

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

代码实现:

class Solution {
    public int maxProfit(int[] prices) {
        // 动态规划,相比2多了一个冷冻期,冷冻期什么也不能干,冷冻期结束转为不持股状态
        if(prices == null || prices.length == 0) return 0;
        int[][] dp = new int[prices.length][3]; // 0 不持股,1 持股,2 冷冻期
        dp[0][0] = 0;  // 不持股
        dp[0][1] = - prices[0]; // 持股
        dp[0][2] = 0; // 冷冻期
        for(int i = 1; i < prices.length; i++){
            // 第i天不持股可以从两种状态转移, 1. 第i-1天不持股,第i天仍不买  2.冷冻期结束了,但不买
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][2]);
            // 第i天持股可以从两种状态转移, 1. 第i-1天不持股,分为i-1天是冷冻期,结束后转为不持股状态和第i-1天本身就不持股,然后买
            // 2.第i-1天持股,第i天不卖出
            dp[i][1] = Math.max(dp[i-1][0] - prices[i], dp[i-1][1]);
            // 只有第i-1天卖出,第i天才处于冷冻期
            dp[i][2] = dp[i-1][1] + prices[i];
        }
        // 只有最后一天不持股或者前一天已经卖掉(今天为冷冻期),最大值在二者之间产生
        return Math.max(dp[prices.length-1][0], dp[prices.length-1][2]);
    }
}

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

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

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

返回获得利润的最大值。

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

示例一:

输入: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

示例二:

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

代码实现:

class Solution {
    public int maxProfit(int[] prices, int fee) {
        // 动态规划,含手续费的股票交易
        // int[][] dp = new int[prices.length][2];
        // dp[0][0] = 0;  // 不持股
        // dp[0][1] = - prices[0] - fee; // 持股
        // for(int i = 1; i < prices.length; i++){
        //     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] - fee);
        // }
        // return dp[prices.length-1][0];

        // // 动态规划,减少状态变量
        // int a = 0;  // 表示今日手里   没有股票的最大收益。
        // int b = Integer.MIN_VALUE;  // 表示今日手里   有股票的的最大收益。
        // for (int price : prices) {
        //     // 没有股票,可能昨天就没有,或者昨天有,今天出手了。
        //     a = Math.max(a, b + price);
        //     // 有股票,可能昨天就有,或者今天刚买的。
        //     b = Math.max(b, a - price - fee);
        // }
        // return a;

        // 贪心算法,
        int n = prices.length;
        int buy = prices[0] + fee;
        int profit = 0;
        for (int i = 1; i < n; ++i) {
            if (prices[i] + fee < buy) {
                buy = prices[i] + fee;
            } else if (prices[i] > buy) {
                profit += prices[i] - buy;
                buy = prices[i];
            }
        }
        return profit;
    }
}

139. 单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典,判定 s 是否可以由空格拆分为一个或多个在字典中出现的单词。

说明:拆分时可以重复使用字典中的单词。

示例一:

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"

示例二:

输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。
     注意你可以重复使用字典中的单词。

示例三:

输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false

代码实现:

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        // 动态规划
        // dp[i] 表示以i-1结尾的字符串能否拆分成字典中得单词
        boolean[] dp = new boolean[s.length()+1];
        dp[0] = true;
        for(int i = 1; i <= s.length(); i++){
            for(int j = 0; j < i; j++){
                if(dp[j] && wordDict.contains(s.substring(j, i))){
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[s.length()];
    }
}

42. 接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例一:
在这里插入图片描述

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。

示例二:

输入:height = [4,2,0,3,2,5]
输出:9

这是一道困难题,可以采用动态规划和单调栈两种做法。

代码实现:

class Solution {
    public int trap(int[] height) {
        // 动态规划
        int n = height.length;
        if(n <= 2) return 0;
        // 状态定义 dp[i][0..1]表示i位置的左右最大高度
        // 状态转移 正向遍历得到左最大高度 dp[i][0] = max(h[i], dp[i-1][0])
        //          反向遍历得到右最大高度 dp[i][1] = max(h[i], dp[i+1][1])
        int[][] dp = new int[n][2];
        dp[0][0] = height[0];
        dp[n-1][1] = height[n-1];
        for(int i = 1; i < n-1; i++){
            dp[i][0] = Math.max(height[i], dp[i-1][0]);  // 左最大高度
            dp[n-1-i][1] = Math.max(height[n-1-i], dp[n-i][1]);   // 右最大高度
        }
        int sum = 0;
        for(int i = 1; i < n-1; i++){
            sum += Math.min(dp[i][0], dp[i][1]) - height[i];  // 每处可接雨水等于左右高度最小值减去自身的高度
        }
        return sum;
    }
}

单调栈实现:

使用单调栈存储,使用单调递减处找低洼处

1.单调栈分为单调递增栈和单调递减栈
11. 单调递增栈即栈内元素保持单调递增的栈
12. 同理单调递减栈即栈内元素保持单调递减的栈

2.操作规则(下面都以单调递增栈为例)
21. 如果新的元素比栈顶元素大,就入栈
22. 如果新的元素较小,那就一直把栈内元素弹出来,直到栈顶比新元素小

3.加入这样一个规则之后,会有什么效果
31. 栈内的元素是递增的
32. 当元素出栈时,说明这个新元素是出栈元素向后找第一个比其小的元素
33. 当元素出栈后,说明新栈顶元素是出栈元素向前找第一个比其小的元素

代码模板:

stack<int> st;
for(int i = 0; i < nums.size(); i++)
{
	while(!st.empty() && st.top() > nums[i])
	{
		st.pop();
	}
	st.push(nums[i]);
}

代码实现:

> class Solution {
    public int trap(int[] height) { // 使用单调递减栈来计算
        int sum = 0;
        Deque<Integer> stack = new LinkedList<>(); // 用Deque实现栈
        for(int i = 0; i < height.length; i++){ // 遍历高度数组
            //如果栈不空,且右边柱子(当前元素)大于左边柱子(栈顶元素),说明形成低洼处了
            while(!stack.isEmpty() && height[i] > height[stack.peek()]){
                // 栈顶的小元素(低洼处索引)出栈,低洼处弹出,尝试此低洼处能积攒雨水
                int top = stack.pop();
                // 看栈里是否有东西即左墙是否存在,有右墙+低洼,没有左墙没用
                if(stack.isEmpty()){
                    break;
                }
                // 栈顶索引值就是左墙位置
                int left = stack.peek();
                // 能积攒的水=(右墙位置-左墙位置-1)*(min(右墙高度,左墙高度)-低洼处高度)
                sum += (i - left - 1) * (Math.min(height[i], height[left]) - height[top]);
            }
            // 当前索引的高度小于栈顶的会直接入栈,大于栈顶就一直出栈,直到小于栈顶或栈空
            stack.push(i);
        }
        return sum;
    }
}

413. 等差数列划分

如果一个数列 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该数列为等差数列。

例如,[1,3,5,7,9]、[7,7,7,7] 和 [3,-1,-5,-9] 都是等差数列。

给你一个整数数组 nums ,返回数组 nums 中所有为等差数组的 子数组 个数。

子数组 是数组中的一个连续序列。

示例一:

输入:nums = [1,2,3,4]
输出:3
解释:nums 中有三个子等差数组:[1, 2, 3][2, 3, 4][1,2,3,4] 自身。

示例二:

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

代码实现:

class Solution {
    public int numberOfArithmeticSlices(int[] nums) {
        // 找规律
        //定义状态:dp[i]表示从nums[0]到nums[i]且以nums[i]为结尾的等差数列子数组的数量。
        //状态转移方程: if nums[i]-nums[i-1]==nums[i-1]-nums[i-2] dp[i] = dp[i-1]+1 else 0
        //如果nums[i]能和nums[i-1]nums[i-2]组成等差数列,则以nums[i-1]结尾的等差数列均可以nums[i]结尾,且多了一个新等差数列[nums[i],       
        //nums[i-1],nums[i-2]]
        if(nums == null || nums.length < 3) return 0;
        int res = 0;
        int count = 0;
        for(int i = 2; i < nums.length; i++){
            if(nums[i] - nums[i-1] == nums[i-1] - nums[i-2]){  // 
                count++;
                res += count;
            }
            else{
                count = 0;
            }
        }
        return res;
    }
}

91. 解码方法

一条包含字母 A-Z 的消息通过以下映射进行了 编码 :

‘A’ -> 1
‘B’ -> 2

‘Z’ -> 26

要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,“11106” 可以映射为:

"AAJF" ,将消息分组为 (1 1 10 6)
"KJF" ,将消息分组为 (11 10 6)

注意,消息不能分组为 (1 11 06) ,因为 “06” 不能映射为 “F” ,这是由于 “6” 和 “06” 在映射中并不等价。

给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。

题目数据保证答案肯定是一个 32 位 的整数。

示例一:

输入:s = "12"
输出:2
解释:它可以解码为 "AB"1 2)或者 "L"12)。

示例二:

输入:s = "226"
输出:3
解释:它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6)

示例三:

输入:s = "0"
输出:0
解释:没有字符映射到以 0 开头的数字。
含有 0 的有效映射是 'J' -> "10"'T'-> "20" 。
由于没有字符,因此没有有效的方法对此进行解码,因为所有数字都需要映射。

示例四:

输入:s = "06"
输出:0
解释:"06" 不能映射到 "F" ,因为字符串含有前导 0"6""06" 在映射中并不等价)。

代码实现:

class Solution {
    public int numDecodings(String s) {
        // 动态规划, s[i]表示第i个字符
        // dp[i]表示前i个字符的解码方法数
        // 1.使用一个字符解码,s[i]!= 0, dp[i]=dp[i-1]
        // 2.使用两个字符解码,s[i-1]!=0 且s[i-1]和s[i]组成的数字小于26,dp[i]=dp[i-2]
        int[] dp = new int[s.length()+1];
        dp[0] = 1; //空字符串
        for(int i = 1; i <= s.length(); i++){
            if(s.charAt(i-1) != '0'){
                dp[i] += dp[i-1];
            }
            if(i > 1 && s.charAt(i-2) != '0' && ((s.charAt(i-2) - '0') * 10 + (s.charAt(i-1) - '0')) <= 26){
                dp[i] += dp[i-2];
            }
        }
        return dp[s.length()];
    }
}

优化后的动态规划:

class Solution {
    public int numDecodings(String s) {
        // 状态转移方程只与三个变量有关
        int[] dp = new int[s.length()+1];
        int a=0, b=1, c=0;
        for(int i = 1; i <= s.length(); i++){
            c = 0;
            if(s.charAt(i-1) != '0'){
                c += b;
            }
            if(i > 1 && s.charAt(i-2) != '0' && ((s.charAt(i-2) - '0') * 10 + (s.charAt(i-1) - '0')) <= 26){
                c += a;
            }
            a = b;
            b = c;
        }
        return c;
    }
}

264. 丑数 II

给你一个整数 n ,请你找出并返回第 n 个 丑数 。

丑数 就是只包含质因数 2、3 和/或 5 的正整数。

示例一:

输入:n = 10
输出:12
解释:[1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是由前 10 个丑数组成的序列。

示例二:

输入:n = 1
输出:1
解释:1 通常被视为丑数。

代码实现:

class Solution {
    public int nthUglyNumber(int n) {
        // 后面的丑数一定是有前面的丑数乘以2,乘以3或乘以5得到的
        // dp[i] 表示第i个丑数,定义三个指针,下一个丑数是当前指针指向的丑数乘以对应的质因数
        // 每次判断,指针所指向的丑数乘以对应的因数中最小的
        int n2 = 0, n3 = 0, n5 = 0;
        int[] dp = new int[n];
        dp[0] = 1;
        for(int i = 1; i < n; i++){
            dp[i] = Math.min(2 * dp[n2], Math.min(3 * dp[n3], 5 * dp[n5]));
            if(dp[i] == 2 * dp[n2]) n2++;
            if(dp[i] == 3 * dp[n3]) n3++;
            if(dp[i] == 5 * dp[n5]) n5++;
        }
        return dp[n-1];
    }
}

96. 不同的二叉搜索树

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

示例一:
在这里插入图片描述

输入:n = 3
输出:5

示例二:

输入:n = 1
输出:1

代码实现:

class Solution {
    public int numTrees(int n) {
        // n个节点存在二叉排序树的个数是G(n),当1为根节点,左子树节点个数为0,右子树节点个数为n-1
        // G(n) = G(0)*G(n-1)+G(1)*(n-2)+...+G(n-1)*G(0)
        // dp[i] 表示i个节点的二叉排序树个数,则以不同的数为根节点时,计算左右两边的个数
        if(n <= 2) return n;
        int[] dp = new int[n+1];
        dp[0] = 1;
        dp[1] = 1;
        dp[2] = 2;
        // 外层循环是节点个数,填充数组
        for(int i = 3; i <= n; i++){
            // 内层循环遍历各个元素用作根节点的情况
            for(int j = 1; j <= i; j++){
                dp[i] += dp[j-1] * dp[i-j];
            }
        }
        return dp[n];
    }
}

118. 杨辉三角

给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。

在「杨辉三角」中,每个数是它左上方和右上方的数的和。
在这里插入图片描述
示例一:

输入: numRows = 5
输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]

示例二:

输入: numRows = 1
输出: [[1]]

代码实现:

class Solution {
    public List<List<Integer>> generate(int numRows) {
        List<List<Integer>> res = new ArrayList<>();
        int[][] arr = new int[numRows][numRows];
        for(int i = 0; i < numRows; i++){
            List<Integer> list = new ArrayList<>();
            for(int j = 0; j <= i; j++){
                if(j == 0 || j==i){
                    arr[i][j] = 1;
                }
                else{
                    arr[i][j] = arr[i-1][j-1] + arr[i-1][j];
                }
                list.add(arr[i][j]);
            }
            res.add(list);
        }
        return res;
    }
}

119. 杨辉三角 II

给定一个非负索引 rowIndex,返回「杨辉三角」的第 rowIndex 行。

在「杨辉三角」中,每个数是它左上方和右上方的数的和。

示例一:

输入: rowIndex = 3
输出: [1,3,3,1]

示例二:

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

示例三:

输入: rowIndex = 1
输出: [1,1]

代码实现:

杨辉三角Ⅰ的基础上,直接计算所求索引的一行值。需要注意的是起始行不同。

class Solution {
    public List<Integer> getRow(int rowIndex) {
        List<List<Integer>> res = new ArrayList<>();
        int[][] arr = new int[rowIndex+1][rowIndex+1];
        for(int i = 0; i <= rowIndex; i++){
            List<Integer> list = new ArrayList<>();
            for(int j = 0; j <= i; j++){
                if(j == 0 || j==i){
                    arr[i][j] = 1;
                }
                else{
                    arr[i][j] = arr[i-1][j-1] + arr[i-1][j];
                }
                list.add(arr[i][j]);
            }
            res.add(list);
        }
        return res.get(rowIndex);
    }
}

方法二:倒着计算当前行

class Solution {
    public List<Integer> getRow(int rowIndex) {
        // 倒着计算当前行,当前行第 i 项的计算只与上一行第 i−1 项及第 i 项有关
        List<Integer> row = new ArrayList<Integer>();
        row.add(1);
        for (int i = 1; i <= rowIndex; ++i) {
            row.add(0);
            for (int j = i; j > 0; --j) {
                row.set(j, row.get(j) + row.get(j - 1));
            }
        }
        return row;
    }
}

931. 下降路径最小和

给你一个 n x n 的 方形 整数数组 matrix ,请你找出并返回通过 matrix 的下降路径 的 最小和 。

下降路径 可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列(即位于正下方或者沿对角线向左或者向右的第一个元素)。具体来说,位置 (row, col) 的下一个元素应当是 (row + 1, col - 1)、(row + 1, col) 或者 (row + 1, col + 1) 。

示例一:

输入:matrix = [[2,1,3],[6,5,4],[7,8,9]]
输出:13
解释:下面是两条和最小的下降路径,用* *标注:
[[2,*1*,3],      [[2,*1*,3],
 [6,*5*,4],       [6,5,*4*],
 [*7*,8,9]]       [7,*8*,9]]

示例二:

输入:matrix = [[-19,57],[-40,-5]]
输出:-59
解释:下面是一条和最小的下降路径,用* *标注:
[[*-19*,57],
 [*-40*,-5]]

示例三:

输入:matrix = [[-48]]
输出:-48

代码实现:

class Solution {
    public int minFallingPathSum(int[][] matrix) {
        // 先把每一行的最小值累加到最后一行,最后比较最后一行的最小值
        // 根据后一行找前一行的最小值,左边一行,正上面或右边一行的最小值
        // 先判断是否是边界
        int n = matrix.length, res = Integer.MAX_VALUE;
        for(int i = 1; i < n; i++){
            for(int j = 0; j < n; j++){
                if(j == 0){
                    matrix[i][j] += Math.min(matrix[i-1][j], matrix[i-1][j+1]);
                }else if(j == n-1){
                    matrix[i][j] += Math.min(matrix[i-1][j], matrix[i-1][j-1]);
                }else{
                    matrix[i][j] += Math.min(Math.min(matrix[i-1][j-1], matrix[i-1][j]), matrix[i-1][j+1]);
                }
            }
        }
        for(int j = 0; j < n; j++){ // 比较最后一行的最小值
            res = Math.min(res, matrix[n-1][j]);
        }
        return res;
    }
}

120. 三角形最小路径和

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

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

示例二:

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

代码实现:

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        int n = triangle.size(), res = Integer.MAX_VALUE;
        int[][] dp = new int[n][n];  // 存储最小和
        dp[0][0] = triangle.get(0).get(0);
        for(int i = 1; i < n; i++){
            // 第一列,只能有对应的上一列得到
            dp[i][0] = dp[i-1][0] + triangle.get(i).get(0);
            for(int j = 1; j < i; j++){
                dp[i][j] = Math.min(dp[i-1][j-1], dp[i-1][j]) + triangle.get(i).get(j);
            }
            // 第i列,只能有第i-1列的i-1行得到
            dp[i][i] = dp[i-1][i-1] + triangle.get(i).get(i);
        }
        // 遍历最后一行找最小值
        for(int i = 0; i < n; i++){
            res = Math.min(res, dp[n-1][i]);
        }
        return res;
    }
}

1314. 矩阵区域和

给你一个 m x n 的矩阵 mat 和一个整数 k ,请你返回一个矩阵 answer ,其中每个 answer[i][j] 是所有满足下述条件的元素 mat[r][c] 的和:

i - k <= r <= i + k,
j - k <= c <= j + k 且
(r, c) 在矩阵内。

示例一:

输入:mat = [[1,2,3],[4,5,6],[7,8,9]], k = 1
输出:[[12,21,16],[27,45,33],[24,39,28]]

示例二:

输入:mat = [[1,2,3],[4,5,6],[7,8,9]], k = 2
输出:[[45,45,45],[45,45,45],[45,45,45]]

思路:矩阵问题,使用到二维前缀和

存储二维前缀和,P[i][j] 表示数组 mat 中以 (0, 0) 为左上角,(i - 1, j - 1) 为右下角的矩形子数组的元素之和

代码实现:

class Solution {
    public int[][] matrixBlockSum(int[][] mat, int k) {
        int m = mat.length, n = mat[0].length;
        int[][] answer = new int[m][n];
        int[][] dp = new int[m+1][n+1];  
        // 存储二维前缀和,P[i][j] 表示数组 mat 中以 (0, 0) 为左上角,(i - 1, j - 1) 为右下角的矩形子数组的元素之和
        for(int i = 0; i < m; i++){
            for(int j = 0; j < n; j++){
                dp[i+1][j+1] = dp[i][j+1] + dp[i+1][j] - dp[i][j] + mat[i][j];
            }
        }

        // 分析边界特殊情况后,计算answer
        for(int i = 0; i < m; i++){
            for(int j = 0; j < n; j++){
                // 左上角坐标
                int r1 = Math.max(i-k, 0);
                int c1 = Math.max(j-k, 0);
                // 右下角坐标
                int r2 = Math.min(i+k, m-1);
                int c2 = Math.min(j+k, n-1);
                // 计算(r1, c1)到(r2, c2)的元素和
                answer[i][j] = dp[r2+1][c2+1] - dp[r1][c2+1] - dp[r2+1][c1] + dp[r1][c1];
            }
        }
        return answer;
    }
}

62. 不同路径

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

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

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

示例一:
在这里插入图片描述

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

示例二:

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

代码实现:

class Solution {
    public int uniquePaths(int m, int n) {

        // 动态规划
        // dp[i][j]表示从左上角到i,j的不同路径数
        // dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
        int[][] dp = new int[m][n];        
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (i == 0 || j == 0)
                    dp[i][j] = 1;
                else {
                    dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
                }
            }
        }
        return dp[m - 1][n - 1];  
    }
}

63. 不同路径 II

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

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

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

在这里插入图片描述
示例一:
在这里插入图片描述

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

示例二:
在这里插入图片描述

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

代码实现:

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        // 如果网格 (i,j) 上有障碍物,则 dp[i][j] 值为 0,表示走到该格子的方法数为 0;
        // 否则网格 (i,j) 可以从网格 (i−1,j) 或者 网格 (i,j−1) 走过来,因此走到该格子的方法数为走到网格 (i−1,j)和网格 (i,j−1)的方法数之和,
        // 即 dp[i,j]=dp[i−1,j]+dp[i,j−1]。

        int m = obstacleGrid.length, n = obstacleGrid[0].length;
        int[][] dp = new int[m][n];
        // 初始化 第一行和第一列
        for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) {
            dp[i][0] = 1;
        }
        for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) {
            dp[0][j] = 1;
        }

        // 根据状态转移方程 dp[i][j] = dp[i - 1][j] + dp[i][j - 1] 进行递推。
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (obstacleGrid[i][j] == 0) {
                    dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
                }
            }
        }
        return dp[m-1][n-1];
    }
}

64. 最小路径和

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。

示例一:
在这里插入图片描述

输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 13111 的总和最小。

示例二:

输入:grid = [[1,2,3],[4,5,6]]
输出:12

代码实现:

class Solution {
    public int minPathSum(int[][] grid) {
        // 动态规划
        int m = grid.length, n = grid[0].length;
        int[][] dp = new int[m][n];
        dp[0][0] = grid[0][0];
        for(int i = 1; i < m; i++){
            dp[i][0] = dp[i-1][0] + grid[i][0];
        }
        for(int j = 1; j < n; j++){
            dp[0][j] = dp[0][j-1] + grid[0][j];
        }
        for(int i = 1; i < m; i++){
            for(int j = 1; j < n; j++){
                dp[i][j] += Math.min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
            }
        }
        return dp[m-1][n-1];
    }
}

221. 最大正方形

在一个由 ‘0’ 和 ‘1’ 组成的二维矩阵内,找到只包含 ‘1’ 的最大正方形,并返回其面积。

示例一:
在这里插入图片描述

输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
输出:4

示例二:
在这里插入图片描述

输入:matrix = [["0","1"],["1","0"]]
输出:1

示例三:

输入:matrix = [["0"]]
输出:0

代码实现:

class Solution {
    public int maximalSquare(char[][] matrix) {
        // 动态规划, dp[i][j]表示以i,j为右下角所能构成的最大正方形的边长
        // 状态转移方程:dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
        // 当前位置的边长值等于三个相邻位置的元素的最小值加1
        // 该位置的值为1
        int maxSide = 0;
        int rows = matrix.length, columns = matrix[0].length;
        int[][] dp = new int[rows][columns];
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < columns; j++) {
                if (matrix[i][j] == '1') {
                    if (i == 0 || j == 0) {
                        dp[i][j] = 1;
                    } else {
                        dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
                    }
                    maxSide = Math.max(maxSide, dp[i][j]);
                }
            }
        }
        int res = 0;
        res = maxSide * maxSide;
        return res;
    }
}

300. 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例一:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4

示例二:

输入:nums = [0,1,0,3,2,3]
输出:4

示例三:

输入:nums = [7,7,7,7,7,7,7]
输出:1

代码实现:

动态规划:

class Solution {
    public int lengthOfLIS(int[] nums) {

        // 动态规划
        int[] dp = new int[nums.length];
        Arrays.fill(dp, 1);
        for(int i = 0; i < dp.length; i++){
            for(int j = 0; j < i; j++){
                if(nums[i] - nums[j] > 0){
                    dp[i] = Math.max(dp[i], dp[j]+1);
                }
            }
        }
        int res = 0;
        for(int i = 0; i < dp.length; i++){
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

动态规划 + 二分查找:

class Solution {
    public int lengthOfLIS(int[] nums) {

        int[] dp = new int[nums.length];
        // 动态规划 + 二分查找
        Arrays.fill(dp, Integer.MAX_VALUE);
        int res = 0;
        for(int num : nums){
            int i = 0, j = nums.length-1;
            while(i <= j){
                int mid = (j - i)/2 + i;
                if(dp[mid] < num) i = mid + 1;
                else j = mid - 1;
            }
            dp[i] = num;
            if(dp[res] != Integer.MAX_VALUE) res++;
        }
        return res;

    }
}

376. 摆动序列

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。

给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。

示例一:

输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3)

示例二:

输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。
其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8)

示例三:

输入:nums = [1,2,3,4,5,6,7,8,9]
输出:2

代码实现:

动态规划:

class Solution {
    public int wiggleMaxLength(int[] nums) {
        // 动态规划,分别统计递增的元素个数和递减的元素个数,比较大小
        // up[i] 表示以前 i 个元素中的某一个为结尾的最长的「上升摆动序列」的长度。
        // down[i]表示以前 i 个元素中的某一个为结尾的最长的「下降摆动序列」的长度。
        int n = nums.length;
        if(n < 2) return n;

        // 原始动态规划
        int[] up = new int[n];
        int[] down = new int[n];
        up[0] = down[0] = 1;
        for (int i = 1; i < n; i++) {
            if (nums[i] > nums[i - 1]) {
                up[i] = Math.max(up[i - 1], down[i - 1] + 1);
                down[i] = down[i - 1];
            } else if (nums[i] < nums[i - 1]) {
                up[i] = up[i - 1];
                down[i] = Math.max(up[i - 1] + 1, down[i - 1]);
            } else {
                up[i] = up[i - 1];
                down[i] = down[i - 1];
            }
        }
        return Math.max(up[n - 1], down[n - 1]);
    }
}

优化后的动态规划:

class Solution {
    public int wiggleMaxLength(int[] nums) {
        // 动态规划,分别统计递增的元素个数和递减的元素个数,比较大小
        // up[i] 表示以前 i 个元素中的某一个为结尾的最长的「上升摆动序列」的长度。
        // down[i]表示以前 i 个元素中的某一个为结尾的最长的「下降摆动序列」的长度。
        int n = nums.length;
        if(n < 2) return n;
        
        // 优化动态规划,直接用up和down减少变量存储
        int up = 1, down = 1;
        for(int i = 1; i < n; i++){
            if(nums[i] > nums[i-1]) {
                up = down + 1;
            }
            if(nums[i] < nums[i-1]) {
                down = up + 1;
            }
        }
        return Math.max(up, down);
    }
}

贪心算法:

class Solution {
    public int wiggleMaxLength(int[] nums) {
        // 动态规划,分别统计递增的元素个数和递减的元素个数,比较大小
        // up[i] 表示以前 i 个元素中的某一个为结尾的最长的「上升摆动序列」的长度。
        // down[i]表示以前 i 个元素中的某一个为结尾的最长的「下降摆动序列」的长度。
        int n = nums.length;
        if(n < 2) return n;

        // 贪心算法,保持区间波动,只需要把单调区间上的元素移除就可以了
        // 当前差值
        int curDiff = 0;
        // 上一个插值
        int preDiff = 0;
        int count = 1;
        for(int i = 1; i < n; i++){
            // 得到当前差值
            curDiff = nums[i] - nums[i-1];
            //如果当前差值和上一个差值为一正一负
            //等于0的情况表示初始时的preDiff,将当前差值赋值给前差值,继续判断
            if ((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) {
                count++;
                preDiff = curDiff;
            }
        }
        return count;
    }
}
  • 2
    点赞
  • 0
    评论
  • 2
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:技术黑板 设计师:CSDN官方博客 返回首页

打赏作者

小朱小朱绝不服输

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值