leetcode
leetcode 551 ~ 600
最长递增子序列的个数

最长递增子序列的个数

难度:

标签:

题目描述

代码结果

运行时间: 54 ms, 内存: 16.6 MB


/*
 * 题目思路:
 * 1. 使用动态规划结合 Java Stream 来实现。
 * 2. 定义两个数组:lengths 和 counts。
 *    - lengths[i] 表示以 nums[i] 结尾的最长递增子序列的长度。
 *    - counts[i] 表示以 nums[i] 结尾的最长递增子序列的个数。
 * 3. 对于每个 nums[i],使用 Stream 过滤并计算能够形成递增序列的元素。
 * 4. 最后通过 Stream 找到最大长度,并计算对应的子序列个数。
 */
 
import java.util.Arrays;
import java.util.stream.IntStream;
 
public class Solution {
    public int findNumberOfLIS(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
        int n = nums.length;
        int[] lengths = new int[n];
        int[] counts = new int[n];
        Arrays.fill(counts, 1);
        int maxLength = 0;
 
        IntStream.range(0, n).forEach(i -> {
            IntStream.range(0, i).forEach(j -> {
                if (nums[j] < nums[i]) {
                    if (lengths[j] + 1 > lengths[i]) {
                        lengths[i] = lengths[j] + 1;
                        counts[i] = counts[j];
                    } else if (lengths[j] + 1 == lengths[i]) {
                        counts[i] += counts[j];
                    }
                }
            });
            maxLength = Math.max(maxLength, lengths[i]);
        });
 
        return IntStream.range(0, n)
            .filter(i -> lengths[i] == maxLength)
            .map(i -> counts[i])
            .sum();
    }
}

解释

方法:

该题解使用动态规划来解决最长递增子序列的个数问题。具体思路如下: 1. 使用两个列表dp和count来记录状态。其中dp[i]表示长度为i+1的递增子序列的结尾元素列表,count[i]表示以dp[i]中每个元素结尾的递增子序列个数累计值列表。 2. 遍历给定数组nums中的每个元素num: a. 二分查找dp中第一个大于等于num的元素位置i。 b. 如果i等于dp的长度,说明num可以接在当前最长递增子序列之后,形成一个新的最长子序列,因此将num添加到dp中,并在count中记录以num结尾的子序列个数为1。 c. 如果i小于dp的长度,说明num可以替换dp[i]中的元素,形成一个新的长度为i+1的递增子序列。此时在dp[i]中二分查找第一个大于num的元素位置k,然后将num添加到dp[i]的末尾,并在count[i]中累加count[i-1][-1] - count[i-1][k],表示以num结尾的递增子序列个数。 3. 最终返回count[-1][-1],即以dp中最后一个元素结尾的递增子序列总数。

时间复杂度:

O(n log n)

空间复杂度:

O(n)

代码细节讲解

🦆
在算法中,为何选择使用两个数组dp和count,它们各自的作用和存储的数据类型是什么?
在算法中,数组dp用于存储每个长度序列的可能结尾元素,从而追踪每种长度的递增子序列可能的结尾值。具体来说,dp[i]是一个列表,记录所有可能作为长度为i+1的递增子序列的结尾的元素。而数组count则用来记录与dp对应的递增子序列的数量。count[i]也是一个列表,与dp[i]中的每个元素对应,记录以该元素结尾的长度为i+1的递增子序列的数量。这样的设计使得算法能够在更新新的序列结尾元素时,同时更新对应的递增子序列数量,便于计算总的递增子序列个数。
🦆
在二分查找中,为什么要找到dp中第一个大于等于num的元素位置,这样做的目的是什么?
通过二分查找在dp中寻找第一个大于等于num的元素位置的目的是确定num可以插入或替换的位置,以维持每个长度的递增子序列的最小可能结尾。这种方法确保了递增子序列尽可能地小且长,因为替换成一个更小或相等的元素可以延长后续可能的递增子序列。这是因为一个小的元素更有可能被后续的更大数值接上,从而形成更长的递增子序列。
🦆
在更新count数组时,表达式`count[i-1][-1] - count[i-1][k]`是如何计算得到的,它代表了什么意义?
表达式`count[i-1][-1] - count[i-1][k]`用于计算新元素num在长度为i+1的递增子序列中的出现次数。这里,`count[i-1][-1]`表示所有长度为i的递增子序列的总数,而`count[i-1][k]`表示以小于num的元素结尾的长度为i的递增子序列的总数。因此,这个表达式的结果是以大于或等于num的元素结尾的长度为i的递增子序列的总数,即num可以在这些子序列后接续形成新的递增子序列的数量。
🦆
如果nums数组中存在重复元素,此算法如何处理?请解释算法对于示例2的处理过程。
当nums数组中存在重复元素时,算法通过在dp中寻找第一个大于等于num的元素位置来处理重复。如果找到的位置i小于dp的长度,即dp[i]中已有元素大于等于num,算法会将num添加到dp[i]中,这样做不会影响已有的递增子序列的任何其他结尾,保证了递增子序列的正确性和完整性。如果num正好等于某一位置的元素,它将替换现有的那个元素,而不会增加新的序列,仅仅是提供了一个新的可能的结尾,而不改变序列的总数。

相关问题

最长递增子序列

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

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

 

示例 1:

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

示例 2:

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

示例 3:

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

 

提示:

  • 1 <= nums.length <= 2500
  • -104 <= nums[i] <= 104

 

进阶:

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

最长连续递增序列

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。

连续递增的子序列 可以由两个下标 lrl < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。

 

示例 1:

输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。 

示例 2:

输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2], 长度为1。

 

提示:

  • 1 <= nums.length <= 104
  • -109 <= nums[i] <= 109