一、题目
给定一个未排序的整数数组 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
二、代码
class Solution {
// 时间复杂度O(N*logN)
public int findNumberOfLIS(int[] nums) {
// 过滤无效参数
if (nums == null || nums.length == 0) {
return 0;
}
// 设置dp,里面存储了所有长度的递增子序列中以每一个nums[i]为结尾的子序列个数分别有多少个
// dp.get(i):存储长度为i的递增子序列信息
// dp.get(i).(key, value):表示以大于等于key结尾的长度为i的递增子序列的个数为value个
ArrayList<TreeMap<Integer, Integer>> dp = new ArrayList<>();
// 遍历nums
for (int num : nums) {
// 记录以num结尾的递增子序列的最大长度
int len = 0;
// 记录以num结尾的最大递增子序列的个数
int cnt = 0;
// 1、找到以num结尾的最长递增子序列的长度
// 用二分法找到dp中找到一个长度最小的无法和num构成递增子序列的长度,即找到一种长度的最小结尾数是大于等于num的,并且这个长度是满足这个条件中最小的长度
len = search(dp, num);
// 2、计算以num结尾的最长递增子序列的个数
// 如果len == 0,说明此时num比所有的数都小,无法和他们组成递增子序列,只能自己组成一个子序列,个数为1个
if (len == 0) {
cnt = 1;
// num可以和以前的子序列组成更长的递增子序列
} else {
// 获取这个可以和num组成递增子序列的最长递增子序列信息
TreeMap<Integer, Integer> treeMap = dp.get(len - 1);
// 以num结尾的最长递增子序列个数就是长度为len-1的递增子序列中所有结尾数小于num的个数总和
// treeMap.firstEntry().getValue():长度为len-1的递增子序列总数
// treeMap.ceilingEntry(num).getValue():长度为len-1的递增子序列中结尾值大于num的子序列总数
// 上面两个数相减得到的就是长度为len-1的递增子序列中所有结尾数小于num的个数总和
cnt = treeMap.firstEntry().getValue() - (treeMap.ceilingEntry(num) == null ? 0 : treeMap.ceilingEntry(num).getValue());
}
// 3、将计算得到的len和cnt加入到dp中
// 如果len == dp.size(),说明以num结尾的最长递增子序列已经推高了当前找到的最长递增子序列长度,需要新开一个TreeMap来存储
if (len == dp.size()) {
dp.add(new TreeMap<Integer, Integer>());
// 将以num结尾的最长递增子序列数据加入到dp中
dp.get(len).put(num, cnt);
// 服用以前已有的TreeMap来存储以num结尾的最长递增子序列数据
} else {
// 这里要注意,len长度的递增子序列中,此时num一定是最小结尾
// 因为前面通过二分法找到的len长度是长度最小的无法和num构成递增子序列的长度,即len长度的递增子序列原本的最小结尾数是大于num的,
// 所以num的加入一定会作为len长度的递增子序列中新的最小结尾数
// 所以在计算长度等于len的递增子序列中结尾数大于等于num的个数时,就直接用dp.get(len).firstEntry().getValue() + cnt
// dp.get(len).firstEntry().getValue():原本就在TreeMap中结尾数大于等于num的总数,它也是原来的长度为len的递增子序列的总个数
dp.get(len).put(num, dp.get(len).firstEntry().getValue() + cnt);
}
}
// 直接返回dp中最大长度的递增子序列总个数即可
// 最大长度:dp.size() - 1
// 递增子序列总个数:dp.get(dp.size() - 1).firstEntry().getValue()
return dp.get(dp.size() - 1).firstEntry().getValue();
}
// 二分查找,返回TreeMap.firstEntry().getKey()>=num最左的位置
public int search(ArrayList<TreeMap<Integer, Integer>> dp, int num) {
int l = 0;
int r = dp.size() - 1;
// 如果最后返回dp.size()就说明此时dp中没有比num大的
int ans = dp.size();
while (l <= r) {
int m = (l + r) >> 1;
// TreeMap.firstEntry()就是当前这个长度的递增子序列中结尾数最小的子序列
if (dp.get(m).firstEntry().getKey() >= num) {
// 记录大于等于num的答案
ans = m;
r = m - 1;
} else {
// 如果一直执行这个分支就会返回0,说明num比当前所有的数都小
l = m + 1;
}
}
return ans;
}
}
三、解题思路
假设遍历到了x这个数,先通过二分法去找x能组成的最长长度的递增子序列是多少,假设它最长能组成Y长度,这是因为Y-1长度的最小结尾数小于x,但是Y长度的最小结尾数大于x,所以x最多就是和Y-1长度的子序列组成递增子序列。
然后利用有序表找到Y-1长度中结尾数大于x的最小值是多少,假设是z,找这个位置记录的个数,假设为c个,假设Y-1长度的子序列总数为a(即这个长度记录中大于等于结尾值最小的那个个数),然后用Y-1长度递增子序列的总数量a减去c,得到的就是Y-1长度中结尾小于x的个数一共有a-c个,也就是能组成的长度为Y以x结尾的递增子序列的个数为a-c个。
然后长度为Y的记录中大于x最近的值是k,k的记录是d个,表示长度为Y结尾大于等于k的子序列有d个,那么记录在大于等于x的个数就是a-c+d个。
至此就完成了整个流程的处理,当把所有数都遍历完一遍后,记录的结构中最大长度的总子序列个数(即记录的大于等于最小结位值位置的个数)就是最后的最长递增子序列个数。