leetcode
leetcode 251 ~ 300
最长递增子序列

最长递增子序列

难度:

标签:

题目描述

给你一个整数数组 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)) 吗?

代码结果

运行时间: 3744 ms, 内存: 14.8 MB


/*
 * 思路:
 * 使用Java Stream API和二分查找实现。
 * 维护一个列表 tails,表示当前递增子序列的最小可能结尾。
 * 遍历数组 nums,对于每个元素,通过二分查找找到其在 tails 中的位置,
 * 如果能找到位置,更新相应位置的值,否则将其添加到 tails 中。
 * tails 的大小即为最长递增子序列的长度。
 */
import java.util.ArrayList;
import java.util.List;
 
public int lengthOfLIS(int[] nums) {
    if (nums == null || nums.length == 0) return 0;
    List<Integer> tails = new ArrayList<>();
    for (int num : nums) {
        int idx = binarySearch(tails, num);
        if (idx < 0) idx = -(idx + 1); // 找到插入位置
        if (idx == tails.size()) {
            tails.add(num); // 添加到尾部
        } else {
            tails.set(idx, num); // 替换当前值
        }
    }
    return tails.size();
}
 
private int binarySearch(List<Integer> list, int target) {
    int left = 0, right = list.size() - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (list.get(mid) == target) return mid;
        if (list.get(mid) < target) left = mid + 1;
        else right = mid - 1;
    }
    return -left - 1;
}

解释

方法:

该题解使用动态规划的思路来解决最长递增子序列问题。定义一个数组 dp,其中 dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度。通过两层循环,外层循环遍历数组中的每个元素,内层循环遍历当前元素之前的所有元素,如果当前元素大于之前的元素,则更新 dp[i] 为 dp[i] 和 dp[j]+1 中的较大值。最后返回 dp 数组中的最大值,即为最长递增子序列的长度。

时间复杂度:

O(n^2)

空间复杂度:

O(n)

代码细节讲解

🦆
在动态规划解决方案中,初始化dp数组的每个元素为1的原因是什么?
在动态规划中初始化 dp 数组的每个元素为1的原因是每个元素至少可以是其自身的一个递增子序列,即最短的递增子序列的长度是1。这样的初始化确保了在计算过程中,每个元素至少代表了一个以该元素结尾的序列。
🦆
您如何理解内层循环中的条件`if nums[i] > nums[j]`,这个条件是用来判断什么情况?
内层循环中的条件 `if nums[i] > nums[j]` 用来判断 nums 中的当前元素 nums[i] 是否大于之前的某个元素 nums[j]。如果是,这意味着 nums[i] 可以接在 nums[j] 形成的递增子序列之后,从而可能形成一个更长的递增子序列。这个条件是动态规划解决最长递增子序列问题的关键,确保了只有在递增的情况下才考虑将 nums[i] 添加到子序列中。
🦆
如果数组`nums`完全逆序会对算法的性能或输出有何影响?
如果数组 `nums` 完全逆序,算法的输出将会是 1,因为在逆序的数组中,没有任何两个连续的元素满足递增的条件,因此每个元素都只能形成长度为1的递增子序列。性能方面,算法仍然需要执行两层循环来检查所有可能的元素对,但每次内层循环的条件检查 `if nums[i] > nums[j]` 都将为假,不会执行任何更新操作。因此,尽管输出为最小可能值,算法的时间复杂度仍然是 O(n^2)。

相关问题

递增的三元子序列

给你一个整数数组 nums ,判断这个数组中是否存在长度为 3 的递增子序列。

如果存在这样的三元组下标 (i, j, k) 且满足 i < j < k ,使得 nums[i] < nums[j] < nums[k] ,返回 true ;否则,返回 false

 

示例 1:

输入:nums = [1,2,3,4,5]
输出:true
解释:任何 i < j < k 的三元组都满足题意

示例 2:

输入:nums = [5,4,3,2,1]
输出:false
解释:不存在满足题意的三元组

示例 3:

输入:nums = [2,1,5,0,4,6]
输出:true
解释:三元组 (3, 4, 5) 满足题意,因为 nums[3] == 0 < nums[4] == 4 < nums[5] == 6

 

提示:

  • 1 <= nums.length <= 5 * 105
  • -231 <= nums[i] <= 231 - 1

 

进阶:你能实现时间复杂度为 O(n) ,空间复杂度为 O(1) 的解决方案吗?

俄罗斯套娃信封问题

给你一个二维整数数组 envelopes ,其中 envelopes[i] = [wi, hi] ,表示第 i 个信封的宽度和高度。

当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。

请计算 最多能有多少个 信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。

注意:不允许旋转信封。

 

示例 1:

输入:envelopes = [[5,4],[6,4],[6,7],[2,3]]
输出:3
解释:最多信封的个数为 3, 组合为: [2,3] => [5,4] => [6,7]。

示例 2:

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

 

提示:

  • 1 <= envelopes.length <= 105
  • envelopes[i].length == 2
  • 1 <= wi, hi <= 105

最长数对链

给你一个由 n 个数对组成的数对数组 pairs ,其中 pairs[i] = [lefti, righti] 且 lefti < righti

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

找出并返回能够形成的 最长数对链的长度

你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。

 

示例 1:

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

示例 2:

输入:pairs = [[1,2],[7,8],[4,5]]
输出:3
解释:最长的数对链是 [1,2] -> [4,5] -> [7,8] 。

 

提示:

  • n == pairs.length
  • 1 <= n <= 1000
  • -1000 <= lefti < righti <= 1000

最长递增子序列的个数

给定一个未排序的整数数组 nums , 返回最长递增子序列的个数 。

注意 这个数列必须是 严格 递增的。

 

示例 1:

输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。

示例 2:

输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。

 

提示: 

  • 1 <= nums.length <= 2000
  • -106 <= nums[i] <= 106

两个字符串的最小ASCII删除和

给定两个字符串s1 和 s2,返回 使两个字符串相等所需删除字符的 ASCII 值的最小和 

 

示例 1:

输入: s1 = "sea", s2 = "eat"
输出: 231
解释: 在 "sea" 中删除 "s" 并将 "s" 的值(115)加入总和。
在 "eat" 中删除 "t" 并将 116 加入总和。
结束时,两个字符串相等,115 + 116 = 231 就是符合条件的最小和。

示例 2:

输入: s1 = "delete", s2 = "leet"
输出: 403
解释: 在 "delete" 中删除 "dee" 字符串变成 "let",
将 100[d]+101[e]+101[e] 加入总和。在 "leet" 中删除 "e" 将 101[e] 加入总和。
结束时,两个字符串都等于 "let",结果即为 100+101+101+101 = 403 。
如果改为将两个字符串转换为 "lee" 或 "eet",我们会得到 433 或 417 的结果,比答案更大。

 

提示:

  • 0 <= s1.length, s2.length <= 1000
  • s1 和 s2 由小写英文字母组成