加载中...
673-最长递增子序列的个数(Number of Longest Increasing Subsequence)
发表于:2021-12-03 | 分类: 中等
字数统计: 2k | 阅读时长: 8分钟 | 阅读量:

原文链接: https://leetcode-cn.com/problems/number-of-longest-increasing-subsequence

英文原文

Given an integer array nums, return the number of longest increasing subsequences.

Notice that the sequence has to be strictly increasing.

 

Example 1:

Input: nums = [1,3,5,4,7]
Output: 2
Explanation: The two longest increasing subsequences are [1, 3, 4, 7] and [1, 3, 5, 7].

Example 2:

Input: nums = [2,2,2,2,2]
Output: 5
Explanation: The length of longest continuous increasing subsequence is 1, and there are 5 subsequences' length is 1, so output 5.

 

Constraints:

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

中文题目

给定一个未排序的整数数组,找到最长递增子序列的个数。

示例 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。

注意: 给定的数组长度不超过 2000 并且结果一定是32位有符号整数。

通过代码

高赞题解

序列 DP

与朴素的 LIS 问题(问长度)相比,本题问的是最长上升子序列的个数。

我们只需要在朴素 LIS 问题的基础上通过「记录额外信息」来进行求解即可。

在朴素的 LIS 问题中,我们定义 $f[i]$ 为考虑以 $nums[i]$ 为结尾的最长上升子序列的长度。 最终答案为所有 $f[0…(n - 1)]$ 中的最大值。

不失一般性地考虑 $f[i]$ 该如何转移:

  • 由于每个数都能独自一个成为子序列,因此起始必然有 $f[i] = 1$;
  • 枚举区间 $[0, i)$ 的所有数 $nums[j]$,如果满足 $nums[j] < nums[i]$,说明 $nums[i]$ 可以接在 $nums[j]$ 后面形成上升子序列,此时使用 $f[j]$ 更新 $f[i]$,即有 $f[i] = f[j] + 1$。

回到本题,由于我们需要求解的是最长上升子序列的个数,因此需要额外定义 $g[i]$ 为考虑以 $nums[i]$ 结尾的最长上升子序列的个数。

结合 $f[i]$ 的转移过程,不失一般性地考虑 $g[i]$ 该如何转移:

  • 同理,由于每个数都能独自一个成为子序列,因此起始必然有 $g[i] = 1$;
  • 枚举区间 $[0, i)$ 的所有数 $nums[j]$,如果满足 $nums[j] < nums[i]$,说明 $nums[i]$ 可以接在 $nums[j]$ 后面形成上升子序列,这时候对 $f[i]$ 和 $f[j] + 1$ 的大小关系进行分情况讨论:
    • 满足 $f[i] < f[j] + 1$:说明 $f[i]$ 会被 $f[j] + 1$ 直接更新,此时同步直接更新 $g[i] = g[j]$ 即可;
    • 满足 $f[i] = f[j] + 1$:说明找到了一个新的符合条件的前驱,此时将值继续累加到方案数当中,即有 $g[i] += g[j]$。

在转移过程,我们可以同时记录全局最长上升子序列的最大长度 $max$,最终答案为所有满足 $f[i] = max$ 的 $g[i]$ 的累加值。

代码:

[]
class Solution { public int findNumberOfLIS(int[] nums) { int n = nums.length; int[] f = new int[n], g = new int[n]; int max = 1; for (int i = 0; i < n; i++) { f[i] = g[i] = 1; for (int j = 0; j < i; j++) { if (nums[j] < nums[i]) { if (f[i] < f[j] + 1) { f[i] = f[j] + 1; g[i] = g[j]; } else if (f[i] == f[j] + 1) { g[i] += g[j]; } } } max = Math.max(max, f[i]); } int ans = 0; for (int i = 0; i < n; i++) { if (f[i] == max) ans += g[i]; } return ans; } }
  • 时间复杂度:$O(n^2)$
  • 空间复杂度:$O(n)$

LIS 问题的贪心解 + 树状数组

我们知道,对于朴素的 LIS 问题存在贪心解法,能够在 $O(n\log{n})$ 复杂度内求解 LIS 问题。

在贪心解中,我们会多开一个贪心数组 $q$,用来记录长度为 $len$ 的最长上升子序列的「最小结尾元素」为何值:**$q[len] = x$ 代表长度为 $len$ 的最长上升子序列的最小结尾元素为 $x$。**

可以证明 $q$ 存在单调性,因此每次确定 $nums[i]$ 可以接在哪个 $nums[j]$ 后面会形成最长上升子序列时,可以通过「二分」来找到满足 $nums[j] < nums[i]$ 的最大下标来实现。

对于本题,由于我们需要求最长上升子序列的个数,单纯使用一维的贪心数组记录最小结尾元素并不足以。

考虑对其进行扩展,期望能取到「最大长度」的同时,能够知道这个「最大长度」对应多少个子序列数量,同时期望该操作复杂度为 $O(\log{n})$。

我们可以使用「树状数组」维护二元组 $(len, cnt)$ 信息:

  1. 因为数据范围较大($-10^6 <= nums[i] <= 10^6$),但数的个数为 $2000$,因此第一步先对 $nums$ 进行离散化操作;
  2. 在遍历 $nums$ 时,每次从树状数组中查询值严格小于 $nums[i]$ 离散值(利用 $nums[i]$ 离散化后的值仍为正整数,我们可以直接查询小于等于 $nums[i]$ 离散值 $-1$ 的值)的最大长度,及最大长度对应的数量;
  3. 对于流程 $2$ 中查得的 $(len, cnt)$,由于 $nums[i]$ 可以接在其后,因此首先长度加一,同时数量将 $cnt$ 累加到该离散值中。

代码:

[]
class Solution { int n; int[][] tr = new int[2010][2]; int lowbit(int x) { return x & -x; } int[] query(int x) { int len = 0, cnt = 0; for (int i = x; i > 0; i -= lowbit(i)) { if (len == tr[i][0]) { cnt += tr[i][1]; } else if (len < tr[i][0]) { len = tr[i][0]; cnt = tr[i][1]; } } return new int[]{len, cnt}; } void add(int x, int[] info) { for (int i = x; i <= n; i += lowbit(i)) { int len = tr[i][0], cnt = tr[i][1]; if (len == info[0]) { cnt += info[1]; } else if (len < info[0]) { len = info[0]; cnt = info[1]; } tr[i][0] = len; tr[i][1] = cnt; } } public int findNumberOfLIS(int[] nums) { n = nums.length; // 离散化 int[] tmp = nums.clone(); Arrays.sort(tmp); Map<Integer, Integer> map = new HashMap<>(); for (int i = 0, idx = 1; i < n; i++) { if (!map.containsKey(tmp[i])) map.put(tmp[i], idx++); } // 树状数组维护 (len, cnt) 信息 for (int i = 0; i < n; i++) { int x = map.get(nums[i]); int[] info = query(x - 1); int len = info[0], cnt = info[1]; add(x, new int[]{len + 1, Math.max(cnt, 1)}); } int[] ans = query(n); return ans[1]; } }
  • 时间复杂度:$O(n\log{n})$
  • 空间复杂度:$O(n)$

其他「序列 DP」内容

意犹未尽?可以尝试下面的「序列 DP」问题:

题目 题解 难度 推荐指数
354. 俄罗斯套娃信封问题 LeetCode 题解链接 困难 🤩🤩🤩🤩🤩
368. 最大整除子集 LeetCode 题解链接 中等 🤩🤩🤩🤩
446. 等差数列划分 II - 子序列 LeetCode 题解链接 困难 🤩🤩🤩🤩🤩
740. 删除并获得点数 LeetCode 题解链接 中等 🤩🤩🤩🤩🤩
978. 最长湍流子数组 LeetCode 题解链接 中等 🤩🤩🤩
1035. 不相交的线 LeetCode 题解链接 中等 🤩🤩🤩🤩
1143. 最长公共子序列 LeetCode 题解链接 中等 🤩🤩🤩🤩
1473. 粉刷房子 III LeetCode 题解链接 困难 🤩🤩🤩🤩
1713. 得到子序列的最少操作次数 LeetCode 题解链接 困难 🤩🤩🤩🤩🤩

注:以上目录整理来自 wiki,任何形式的转载引用请保留出处。


最后

如果有帮助到你,请给题解点个赞和收藏,让更多的人看到 ~ (“▔□▔)/

也欢迎你 关注我(公主号后台回复「送书」即可参与长期看题解学算法送实体书活动)或 加入「组队打卡」小群 ,提供写「证明」&「思路」的高质量题解。

所有题解已经加入 刷题指南,欢迎 star 哦 ~

统计信息

通过次数 提交次数 AC比率
50487 115805 43.6%

提交历史

提交时间 提交结果 执行时间 内存消耗 语言

相似题目

题目 难度
最长递增子序列 中等
最长连续递增序列 简单
上一篇:
672-灯泡开关 Ⅱ(Bulb Switcher II)
下一篇:
675-为高尔夫比赛砍树(Cut Off Trees for Golf Event)
本文目录
本文目录