LeetCode精选题之动态规划01

文章目录

  • LeetCode精选题之动态规划01
    • 1 爬楼梯--LeetCode70
    • 2 打家劫舍--LeetCode198
    • 3 打家劫舍II--LeetCode213(Medium)
    • 4 最小路径和--LeetCode64(Medium)
    • 5 不同路径--LeetCode62(Medium)
    • 6 区域和检索-数组不可变--LeetCode303
    • 7 等差数列划分--LeetCode413(Medium)
    • 8 整数拆分--LeetCode343(Medium)
    • 9 完全平方数--LeetCode279(Medium)
    • 10 解码方法--LeetCode91(Medium)
    • 11 最长上升子序列--LeetCode300(Medium)
    • 12 最长数对链--LeetCode646(Medium)
    • 13 摆动序列--LeetCode376(Medium)
    • 14 最长公共子序列--LeetCode1143

LeetCode精选题之动态规划01

参考资料:CyC2018的LeetCode题解

1 爬楼梯–LeetCode70

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

示例 1:

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

示例 2:

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

动态规划的思路:
dp[i]数组的含义:走到第 i个台阶的方法数目。
转移方程:第i个楼梯可以从第 i-1i-2个楼梯再走一步到达,走到第 i个楼梯的方法数为走到第 i-1和第i-2个楼梯的方法数之和,即:dp[i]=dp[i-1]+dp[i-2]

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];
    }
}

空间的优化:考虑到 dp[i]只与 dp[i - 1]dp[i - 2]有关,因此可以只用两个变量来存储 dp[i - 1]dp[i - 2],使得原来的 O(N) 空间复杂度优化为 O(1) 复杂度。

class Solution {
    public int climbStairs(int n) {
        if (n <= 2) {
            return n;
        }
        int prePre = 1, pre = 2;
        for (int i = 3; i <= n; i++) {
            int cur = prePre + pre;
            prePre = pre;
            pre = cur;
        }
        return pre;
    }
}

2 打家劫舍–LeetCode198

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

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

示例 1:

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

示例 2:

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12 。
class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        int prePre = 0, pre = 0;
        for (int i = 0; i < n; i++) {
            int cur = Math.max(pre, prePre+nums[i]);
            prePre = pre;
            pre = cur;
        }
        return pre;
    }
}

3 打家劫舍II–LeetCode213(Medium)

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

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

示例 1:

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

示例 2:

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

思路: 环状排列意味着第一个房子和最后一个房子中只能选择一个偷窃,因此可以把此环状排列房间问题转换成两个单排排列房间的子问题

  1. 在不偷窃第一个房子的情况下(即 nums[1, n-1]),最大金额是 p1
  2. 在不偷窃最后一个房子的情况下(即 nums[0, n−2]),最大金额是 p2

所以偷窃最大金额为以上两种情况的较大值,即 max(p1,p2)

参考题解:jyd的题解

class Solution {
    public int rob(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        int n = nums.length;
        // 一定要考虑特殊情况
        if (n == 1) return nums[0];
        return Math.max(robCore(nums, 0, n-2), robCore(nums, 1, n-1));
    }

    private int robCore(int[] nums, int start, int end) {
        int prePre = 0, pre = 0;
        for (int i = start; i <= end; i++) {
            int cur = Math.max(prePre+nums[i], pre);
            prePre = pre;
            pre = cur;
        }
        return pre;
    }
}

4 最小路径和–LeetCode64(Medium)

给定一个包含非负整数的 m x n网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。说明:每次只能向下或者向右移动一步。

示例:

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

思路:
dp数组的含义:dp[i][j]表示在坐标(i,j)位置上的最小路径和。所以最后返回的结果就是dp[m-1][n-1],其中m和n分别表示的是行数和列数。

转移方程:题目中的说明"每次只能向下或者向右移动一步",这就是状态转移方程。对于左边第一列,和上面第一行需要特殊考虑。

代码:

class Solution {
    public int minPathSum(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int[][] dp = new int[m][n];
        // 初始条件
        dp[0][0] = grid[0][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++) {
            dp[i][0] = dp[i-1][0] + grid[i][0];
        }
        
        // 状态转移
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = grid[i][j] + Math.min(dp[i][j-1], dp[i-1][j]);
            }
        }

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

空间复杂度的优化:只需要一维数组即可。

class Solution {
    public int minPathSum(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int[] dp = new int[n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                // 首先对于上面第一行和左边第一列的考虑
                if (j == 0) {
                    dp[j] = dp[j] + grid[i][j];
                }else if (i == 0) {
                    dp[j] = dp[j-1] + grid[i][j];
                }else {
                    dp[j] = Math.min(dp[j-1], dp[j]) + grid[i][j];
                }
            }
        }
        return dp[n-1];
    }
}

5 不同路径–LeetCode62(Medium)

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。问总共有多少条不同的路径?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7P27teTq-1593150692899)(.\images\LeetCode62-示意图.png)]
例如:上图是一个7 x 3 的网格。有多少可能的路径?

示例 1:

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

示例 2:

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

提示:

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

思路:和上一题类似,上一题是求路径长,这一题是求路径数量。

class Solution {
    public int uniquePaths(int m, int n) {
        int[] dp = new int[m];// 这道题目里面的n表示行数, m表示列数
        Arrays.fill(dp, 1);

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

        return dp[m-1];
    }
}

6 区域和检索-数组不可变–LeetCode303

给定一个整数数组 nums,求出数组从索引 ij( ij) 范围内元素的总和,包含 i, j两点。

示例:

给定 nums = [-2, 0, 3, -5, 2, -1],求和函数为 sumRange()

sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3

说明:

  • 你可以假设数组不可变。
  • 会多次调用 sumRange 方法。

思路:保存数组各个位置上的累加和,那么区间求和问题就变成了两个累加和相减的过程。
sum数组的含义:sum[i]表示nums[0, i]的累加和,也就是说这里的sum[i]是包含nums[i]的。说明:sum数组的含义根据自己的习惯进行定义,但一定要自己清楚它的含义即可。

那么,nums[i, j]区间求和问题就等于sum[j] - sum[i-1](i>0的情况下)

class NumArray {
    private int[] sum;
    public NumArray(int[] nums) {
        int n = nums.length;
        sum = new int[n];
        int tempSum = 0;
        for (int i = 0; i < n; i++) {
            tempSum += nums[i];
            sum[i] = tempSum;
        }
    }
    
    public int sumRange(int i, int j) {
        return i == 0 ? sum[j] : sum[j] - sum[i-1];
    }
}

7 等差数列划分–LeetCode413(Medium)

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

例如,以下数列为等差数列:

1, 3, 5, 7, 9
7, 7, 7, 7
3, -1, -5, -9

以下数列不是等差数列。

1, 1, 2, 5, 7

数组 A 包含 N 个数,且索引从0开始。数组 A 的一个子数组划分为数组 (P, Q),P 与 Q 是整数且满足 0<=P

函数要返回数组 A 中所有为等差数组的子数组个数。

示例:

A = [1, 2, 3, 4]
返回:3, A 中有三个子等差数组: [1, 2, 3], [2, 3, 4] 以及自身 [1, 2, 3, 4]。  

递归的代码:

class Solution {
    private int sum = 0;
    public int numberOfArithmeticSlices(int[] A) {
        getSlices(A, A.length-1);
        return sum;
    }
    
    // 一定要理清楚这个函数的含义:(0,index)相比(0,index-1)多出来的等差数列的个数
    // 这道题就需要把(2, A.length-1)每一位上的个数都加起来就是最后的结果。  
    private int getSlices(int[] A, int index) {
        if (index < 2) return 0;
        int temp = 0;
        if (A[index]-A[index-1] == A[index-1]-A[index-2]) {
            temp = 1 + getSlices(A, index-1);
            sum += temp;
        }else {
            getSlices(A, index-1);
        }
        return temp;
    }
}

动态规划:
dp数组的含义:dp[i]表示数组A[0, i]相比于A[0, i-1]多出来的等差数列的个数。【理解dp数组的含义很重要,官方题解中的符号比较难懂】

例如:A=[1,2,3,4]dp[0] = 0dp[1] = 0dp[2]=1相比于dp[1]多出来一个等差数列1,2,3dp[3] = dp[2]+1=2,即相比dp[2]多出来两个等差数列2,3,41,2,3,4。所以最终sum=dp[0]+dp[1]+dp[2]+dp[3]=3个。

转移方程的思路:对于第 i个元素,判断这个元素跟前一个元素的差值是否和等差数列中的差值相等。如果相等,那么新区间多出来的等差数列的个数:1+dp[i−1]。sum 同时也要加上这个值来更新全局的等差数列总数。

class Solution {
    public int numberOfArithmeticSlices(int[] A) {
        int n = A.length;
        int sum = 0;
        int[] dp = new int[n];
        for (int i = 2; i < n; i++) {
            if (A[i]-A[i-1] == A[i-1]-A[i-2]) {
                dp[i] = dp[i-1] + 1;
                sum += dp[i];
            }else {
                // 构不成等差数列,dp[i] = 0
            }
        }
        return sum;
    }
}

8 整数拆分–LeetCode343(Medium)

给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。

示例 1:

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

示例 2:

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

说明: 你可以假设 n 不小于 2 且不大于 58。

思路:
dp数组的含义:dp[i]表示拆分i得到的最大乘积。
初始条件:1没法拆分,所以得不到最大乘积,故dp[1]为零。这里的初始条件我是根据上面的dp数组的含义来得到的,含义不一样,可能初始条件也不一样。注意:这里把dp[1]理解为1也是可以,表示不拆分1,1就是最大乘积,所以不同理解会得到不同的初始条件。

转移方程:

dp[i] = Math.max(dp[i], j*dp[i-j]);
dp[i] = Math.max(dp[i], j*(i-j));

其中j*dp[i-j]表示对i-j进行拆分。j*(i-j)表示对i-j不拆分。

举例:对于求解dp[5],需要比较1*dp[4]、1*4、2*dp[3]、2*3、3*dp[2]、3*2、4*dp[1]、4*1,取这里面的最大值。

class Solution {
    public int integerBreak(int n) {
        // dp[i]表示拆分i得到的最大乘积
        int[] dp = new int[n+1];
        dp[1] = 0;// 1没法拆分,所以得不到最大乘积,为零
        for (int i = 2; i <= n; i++) {
            for (int j = 1; j < i; j++) {
                dp[i] = Math.max(dp[i], j*dp[i-j]);
                dp[i] = Math.max(dp[i], j*(i-j));
            }
        }
        return dp[n];
    }
}

9 完全平方数–LeetCode279(Medium)

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

示例 1:

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

示例 2:

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

动态规划:
dp数组的含义:dp[i]表示正整数i组成和的完全平方数的最少个数。
初始条件:1是完全平方数,且一个数n肯定可以由n个1组成,故正整数n组成和的完全平方数的最大个数为n。每个位置的dp[i]初始化为自身,即dp[i] = i
转移方程:比较所有可能取其中的最小值,这里的所有可能值得是代码中的j,保证j*j不超过i。举例:当i等于12时,j可以1,2,3,4的平方是16已经大于12了。比较i, 1+dp[12-1*1], 1+dp[12-2*2], 1+dp[12-3*3]取其中的最小值就是dp[i]的值。

class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n+1];// dp[i]表示正整数i组成和的完全平方数的最少个数

        // 初始条件,一个数n肯定可以由n个1组成
        for (int i = 1; i <= n; i++) {
            dp[i] = i;
        }

        // 根据转移方程进行更新
        for (int i = 2; i <= n; i++) {
            for (int j = 1; j*j <= i; j++) {
                dp[i] = Math.min(dp[i], 1+dp[i-j*j]);
            }
        }

        return dp[n];
    }
}

10 解码方法–LeetCode91(Medium)

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

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

‘Z’ -> 26

给定一个只包含数字的非空字符串,请计算解码方法的总数。

示例 1:

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

示例 2:

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

思路:
dp数组的含义:dp[i]表示字符串s的前i位子串(包括i)解码方法的总数。注意:这里的i指的是长度。

初始条件:

  • dp[0] = 1表示空串没法进行编码,可以理解为是一种编码方式。
  • dp[1] = 1表示第一位不是0的话只能有一种编码方式。

转移方程:

  • 字符串si位置上的字符不等于字符0的话,dp[i] += dp[i-1],表示i位置上的字符可以和前面的子串进行编码。
  • 字符串si-1i两个位置构成的子串对应的整数如果在[10, 26]之间的话,dp[i] += dp[i-2],表示i位置上的字符可以和i-1位置上的字符组合进行编码,然后再和字符串si-2位置前面的子串进行编码。
  • 举例:“2123”,指针在3这个位置上,则3可以和前面子串"212"进行编码,还可以先和前面的字符‘2’进行编码成“23”对应的字母,然后再和前面的子串“21”进行编码。
class Solution {
    public int numDecodings(String s) {
        int n = s.length();
        int[] dp = new int[n+1];
        // 如果第一位为字符'0',肯定没法进行编码
        if (s.charAt(0) == '0') {
            return 0;
        }
        dp[0] = 1;// 空串,没法进行编码,可以理解为一种编码方式
        dp[1] = 1;// 第一位不是0的话只能有一种编码方式。
        for (int i = 2; i <= n; i++) {
            if (s.charAt(i-1) != '0') {
                dp[i] += dp[i-1];
            }
            int temp = Integer.parseInt(s.substring(i-2, i));
            if (temp >= 10 && temp <= 26) {
                dp[i] += dp[i-2];
            }
            if (dp[i] == 0) {
                return 0;// 表示没法进行编码
            }
        }
        return dp[n];
    }
}

11 最长上升子序列–LeetCode300(Medium)

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

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4 
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

说明:

  • 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
  • 你算法的时间复杂度应该为 O(n2) 。

进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?

思路:
dp数组的含义:dp[i]为考虑前 i 个元素,以第 i 个数字结尾的最长上升子序列的长度,注意 nums[i] 必须被选取

初始状态:每个数字本身构成一个子序列,所以dp数组中每一位上都初始化为1.

从左往右计算dp数组中各个位置的值,状态转移方程为:

dp[i] = max(dp[j]) + 1, 其中0≤j

注意:对于一个长度为 n 的序列,最长递增子序列并不一定会以 Sn-1 为结尾,因此 dp[n-1]不是序列的最长递增子序列的长度,需要遍历 dp 数组找出最大值才是所要的结果。

代码:

class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        //边界条件
        if (n == 0) return 0;
        int[] dp = new int[n];
        // 初始化,即每个数字本身构成一个子序列
        Arrays.fill(dp, 1);
        // 转移方程
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    dp[i] = Math.max(dp[i], dp[j]+1);
                }
            }
        }
        int res = dp[0];
        for (int i = 1; i < n; i++) {
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

12 最长数对链–LeetCode646(Medium)

给出 n个数对。 在每一个数对中,第一个数字总是比第二个数字小。

现在,我们定义一种跟随关系,当且仅当 b < c时,数对(c, d)才可以跟在 (a, b)后面。我们用这种形式来构造一个数对链。

给定一个对数集合,找出能够形成的最长数对链的长度。你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。

示例 :

输入: [[1,2], [2,3], [3,4]]
输出: 2
解释: 最长的数对链是 [1,2] -> [3,4]

注意:给出数对的个数在 [1, 1000] 范围内。

思路:这道题目和【俄罗斯套娃信封问题–LeetCode354】的思路是相似的。先对数组排序,排序规则是按照数对中第一个数升序排序。

为什么要排序?
我自己的理解是先让数对看起来是有序的,然后再用动态规划找出最长的数对链。举例:[[3,4], [2,3], [1,2]],这个例子是上面的实例的变形,如果直接对这个例子用动态规划是没法做的,按照规则每一个数对都没法和它前面的数对构成数对链,得到的结果是1,但是实际上,[1,2]和[3,4]可以构成数对链。所以需要先对所有的数对进行排序,然后再用动态规划求解。

class Solution {
    public int findLongestChain(int[][] pairs) {
        if (pairs == null || pairs.length == 0) {
            return 0;
        }
        
        // 首先对数组排序,排序规则是按照数对中第一个数升序排序
        Arrays.sort(pairs, (a, b) -> (a[0] - b[0]));
        int n = pairs.length;
        int[] dp = new int[n];
        Arrays.fill(dp, 1);// 初始条件,每一个数对可以单独构成一个子链
        
        int res = dp[0];
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (pairs[i][0] > pairs[j][1]) {
                    dp[i] = Math.max(dp[i], 1+dp[j]);
                }
            }
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

13 摆动序列–LeetCode376(Medium)

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

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

给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

示例 1:

输入: [1,7,4,9,2,5]
输出: 6 
解释: 整个序列均为摆动序列。

示例 2:

输入: [1,17,5,10,13,15,10,5,16,8]
输出: 7
解释: 这个序列包含几个长度为 7 摆动序列,其中一个可为[1,17,10,13,10,16,8]。

示例 3:

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

进阶:你能否用 O(n) 时间复杂度完成此题?

思路:动态规划。

用动态规划就要考虑当前位置的元素能否接在前面元素的后面,从而构成摆动序列。这里的判断规则我用的是一个choice数组,包含三个数:-1 0 1。

  • -1表示当前位置的最长摆动序列,当前元素是比序列中前一个元素小,注意这里是摆动序列,而不是原数组。
  • 0表示还不存在大小关系,适用于数组中第一个元素,第一个元素后面既可以是大于它的树,也可以是小于它的数。
  • 1表示当前位置的最长摆动序列,当前元素是比序列中前一个元素大。

记录了大小关系,就可以用动态规划了。思路和最长上升子序列的思路是一致的。

class Solution {
    public int wiggleMaxLength(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        int n = nums.length;
        int[] dp = new int[n];
        int[] choice = new int[n];
        Arrays.fill(dp, 1);// 所有位置初始化为1,因为一个元素也可以构成摆动序列
        int res = 1;
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[j] == nums[i]) {
                    continue;
                }
                if (choice[j] == 0) {
                    dp[i] = dp[j] + 1;
                    choice[i] = nums[i] > nums[j] ? 1 : -1;
                }
                if (choice[j] == 1 && nums[j] > nums[i] && dp[i] < dp[j]+1) {
                    choice[i] = -1;
                    dp[i] = dp[j]+1;
                }
                if (choice[j] == -1 && nums[j] < nums[i] && dp[i] < dp[j]+1) {
                    choice[i] = 1;
                    dp[i] = dp[j] + 1;
                }
            }
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

参考了官方题解,类似的状态转移方程,但是含义不一样,使用up数组和down数组更好理解。思路大致如下:
每当我们选择一个元素作为摆动序列的一部分时,这个元素要么是上升的,要么是下降的,这取决于前一个元素的大小。

  • up[i]表示目前为止最长的以第 i个元素结尾的上升摆动序列的长度。
  • down[i]表示目前为止最长的以第 i个元素结尾的下降摆动序列的长度。

我们每当找到将第 i个元素作为上升摆动序列的尾部的时候就更新 up[i]。现在我们考虑如何更新 up[i],我们需要考虑前面所有的降序结尾摆动序列,也就是找到 down[j],满足 jnums[i]>nums[j]。类似的, down[i]也会被更新。

class Solution {
    public int wiggleMaxLength(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        int n = nums.length;
        int[] up = new int[n];
        int[] down = new int[n];
        Arrays.fill(up, 1);
        Arrays.fill(down, 1);
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    up[i] = Math.max(up[i], down[j]+1);
                }else if (nums[i] < nums[j]) {
                    down[i] = Math.max(down[i], up[j]+1);
                }
            }
        }
        return Math.max(up[n-1], down[n-1]);
    }
}

对时间复杂度和空间复杂度的优化:

class Solution {
    public int wiggleMaxLength(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        int up = 1, down = 1;
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > nums[i-1]) {
                up = down + 1;
            }else if (nums[i] < nums[i-1]) {
                down  = up + 1;
            }
        }
        return Math.max(up, down);
    }
}

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

这一题感觉方法的解题循序渐进,逐渐优化题解过程,可以直接参考官方题解。参考资料:LeetCode官方解题

14 最长公共子序列–LeetCode1143

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

若这两个字符串没有公共子序列,则返回 0。

示例 1:

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace",它的长度为 3。

示例 2:

输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc",它的长度为 3。

示例 3:

输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0。

提示:

  • 1 <= text1.length <= 1000
  • 1 <= text2.length <= 1000
  • 输入的字符串只含有小写英文字符。

思路:
dp数组的含义:dp[i][j]表示text1中的前i个字符(包括i)和text2中的前j个字符(包括j)得到的最长公共子序列(LCS)的长度。这里的ij表示的是长度,目的是让两个字符串中的第一个字符也可以使用转移方程。
转移方程:

  • 如果text1中的第i个字符和text2中的第j个字符相等,说明这个字符肯定在LCS中,只需要看ij前面的字符即可,转移方程:dp[i][j] = 1 + dp[i-1][j-1]
  • 如果不相等,说明至少有一个字符不在,需要舍弃一个,即dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1])
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int m = text1.length();
        int n = text2.length();
        // dp[i][j]表示text1中的前i个字符(包括i)和text2中的前j个字符(包括j)得到的最长公共子序列的长度
        // 这里的i j表示的是长度,目的是让两个字符串中的第一个字符也可以使用下面的转移方程
        // 有点类似于链表中加个虚拟头节点
        int[][] dp = new int[m+1][n+1];
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (text1.charAt(i-1) == text2.charAt(j-1)) {
                    dp[i][j] = 1 + dp[i-1][j-1];
                }else {
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        return dp[m][n];
    }
}

你可能感兴趣的:(#,LeetCode刷题)