题目描述 这是 LeetCode 上的 673. 最长递增子序列的个数 ,难度为 中等 。
给定一个未排序的整数数组,找到最长递增子序列的个数。
示例 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位有符号整数。
提示:
$1 <= nums.length <= 2000$
$-10^6 <= nums[i] <= 10^6$
序列 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]$ 的累加值。
Java 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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; } }
C++ 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Solution {public : int findNumberOfLIS (vector<int >& nums) { int n = nums.size (); vector<int > f (n) , g (n) ; int maxv = 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]; } } } maxv = max (maxv, f[i]); } int ans = 0 ; for (int i = 0 ; i < n; i++) { if (f[i] == maxv) ans += g[i]; } return ans; } };
Python 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def findNumberOfLIS (self, nums: List [int ] ) -> int : n = len (nums) f, g = [1 ] * n, [1 ] * n maxv = 1 for i in range (1 , n): for j in range (i): if nums[j] < nums[i]: if f[i] < f[j] + 1 : f[i], g[i] = f[j] + 1 , g[j] elif f[i] == f[j] + 1 : g[i] += g[j] maxv = max (maxv, f[i]) ans = 0 for i in range (n): if f[i] == maxv: ans += g[i] return ans
TypeScript 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function findNumberOfLIS (nums: number [] ): number { const n = nums.length ; let f = new Array (n).fill (1 ), g = new Array (n).fill (1 ); let max = 1 ; for (let i = 1 ; i < n; i++) { for (let 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]); } let ans = 0 ; for (let 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)$ 信息:
因为数据范围较大($-10^6 <= nums[i] <= 10^6$),但数的个数为 $2000$,因此第一步先对 $nums$ 进行离散化操作;
在遍历 $nums$ 时,每次从树状数组中查询值严格小于 $nums[i]$ 离散值(利用 $nums[i]$ 离散化后的值仍为正整数,我们可以直接查询小于等于 $nums[i]$ 离散值 $-1$ 的值)的最大长度,及最大长度对应的数量;
对于流程 $2$ 中查得的 $(len, cnt)$,由于 $nums[i]$ 可以接在其后,因此首先长度加一,同时数量将 $cnt$ 累加到该离散值中。
Java 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 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++); } 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)$
最后 这是我们「刷穿 LeetCode」系列文章的第 No.673
篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。
在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。
为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。
在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。