题目描述
最长递增子序列
给你一个整数数组 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
各种解法
动态规划
子序列不要求连续,子数组才要求连续
class Solution {
public int lengthOfLIS(int[] nums) {
// dp[i]的定义为:包含第i个元素的最长递增子序列的长度
int[] dp = new int[nums.length];
//初始化默认每个数字结尾的长度都有 1,自己
dp[0] = 1;
int maxans = 1; // 最终结果,最终是所有dp中的最大值
for(int i=1; i<nums.length; i++){
dp[i]=1; // 一定要赋值,因为可能第2个数进不了if,导致取值为0
for(int j=0; j<i; j++){
if(nums[j]<nums[i]){
dp[i] = Math.max(dp[i], dp[j]+1);
}
}
maxans = Math.max(maxans, dp[i]);
}
return maxans;
}
}
解题思路
- dp[i]表示考虑前 i 个元素,以第 i 个数字结尾的最长子序列的长度,nums[i]必须被选中
- 状态转移方程:dp[i] = max(dp[j]) + 1, 其中0≤j<i且num[j]<num[i],以之前比自己小的元素结尾的最大的结果+1
- 最终的结果是dp数组中的最大值
时间复杂度:O(n^2)
动态规划+二分查找
需要把时间复杂度从O(n^2)提升到O(nlogn),自然地往二分查找的方向想。
动态规划O(n^2)来源于求dp数组是o(n),dp数组的每个值是O(n)
现在可优化的点在dp数组的每个值的求取,降到O(logn)。需要重新设计,原来的dp变成现在的tails数组
public static int longestSubArr(int[] nums){
// 因为要求O(NlogN),所以dp的思路就达不到了,需要用二分查找的思路
int[] tails = new int[nums.length]; // tails[k]记录长度为k+1的子序列的尾部元素值
int res = 0;
for(int num : nums){
int left = 0, right = res;
// 前闭后开,找到第一个num大的数,更新他
while(left<right){
int mid = left + (right-left)/2;
if(tails[mid] < num){
left = mid+1;
}else{ // 等于的还是right往左走
right = mid;
}
}
tails[left] = num;
if(right == res) res++;
}
return res;
}
解题思路
- 状态变为:tails[k]表示长度为k+1的递增子序列他的结尾元素的最小值,列举了各个长度的上升子序列可以拥有的最小的结尾。(想到这种状态,应该是如果你的递增子序列要尽可能长,那么前面的子序列的结尾要尽可能小)
- 转移方程:res为tails的当前长度,直到当前的最长上升子序列长度。也就是说如果现在出现的数大于tails记录的所有长度的递增子序列的最小值,那么他应该是一个更长递增子序列的结尾。res应该+1
- 如果tails区间中存在比当前num大的结尾,从左往右第1个,更新为当前值。(因为这个位置,前面的结尾数比自己小,后面的结尾数比自己大,当前num可以作为前面结尾数的后一个数,也就是子序列长度+1,刚好应该是在tails数组后一个结尾数的位置,刚好又比原先此位置的结尾数小,所以可以更新该位置。)
- 如果tails区间中不存在比当前num大的结尾,直接把当前num放到tails数组后面一格,因为他可以作为前面所有的结尾。res++;
- 最终返回是res的长度,也就是最长上升子序列长度。
时间复杂度:O(nlogn)
变形题
1. 要求输出最长递增子序列路径,具体序列,字典序最小的那个
字典序最小的那个,就是对tails数组的每个位置做最后更新的那几个原始数字组成。
public class Solution {
/**
* retrun the longest increasing subsequence
* @param arr int整型一维数组 the array
* @return int整型一维数组
*/
public int[] LIS (int[] nums) {
// write code here
int[] tails = new int[nums.length]; // tails[k]记录长度为k+1的子序列的尾部元素值
int res = 0;
int[] dp = new int[nums.length]; // 再记录一下每个位置的最长序列长度
for(int i=0; i<nums.length; i++){
int num = nums[i];
int left = 0, right = res;
// 前闭后开
while(left<right){
int mid = left + (right-left)/2;
if(tails[mid] < num){
left = mid+1;
}else{
right = mid;
}
}
tails[left] = num;
dp[i] = left+1; // 记录该位置的最长序列长度
if(right == res) res++;
}
// 对了后续的这些步骤和 dp数组
// 完成求上面的最长递增子序列的长度后,开始确定该递增子序列的具体序列
int[] subarr = new int[res];
int len = res;
for(int i=nums.length-1; i>=0; i--){
if(dp[i] == len){
subarr[len-1] = nums[i];
len--;
}
}
return subarr;
}
}
解题思路
- 按照原先的思路求出最大递增子序列的长度,然后在求每个数对tails数组更新情况的过程中,记录每个数对应的最大递增子序列的长度记在dp里面。
- 按照上面的一套流程,求完之后,根据填充好的dp数组和最大的递增子序列长度,然后从后往前找各个长度下的对应的数字
- 从后往前找的原因是,如果后面的数和前面的数所形成的最长递增子序列的长度相等, 那么肯定是后面的数小于前面的数的情况,不然后面这个数的最长递增子序列会比前面的数对应的要长。所以字典序最小,要选后面的数。
2. 阿里变形体
题目描述
小强现在有 n 个物品,每个物品有两种属性 x i x_{i} xi和 y i y_{i} yi.他想要从中挑出尽可能多的物品满足以下条件:对于任意两个物品 i 和 j ,满足 x i < x j x_{i}<x_{j} xi<xj且 y i < y j y_{i}<y_{j} yi<yj或者 x i > x j x_{i}>x_{j} xi>xj且 y i > y j y_{i}>y_{j} yi>yj.问最多能挑出多少物品。时间复杂度要求O(nlogn).
输入描述:
第一行输入一个正整数.表示有组数据.
对于每组数据,第一行输入一个正整数.表示物品个数.
接下来两行,每行有个整数.
第一行表示个节点的属性.
第二行表示个节点的属性.
输出描述:
输出行,每一行对应每组数据的输出.
输入例子1:
2
3
1 3 2
0 2 3
4
1 5 4 2
10 32 19 21
输出例子1:
2
3
解法
二分查找
import java.util.*;
public class Main{
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
int T = sc.nextInt();
for(int p=0; p<T; p++){
int n = sc.nextInt();
int[][] arr = new int[n][2];
for(int i=0; i<n; i++){
arr[i][0] = sc.nextInt();
}
for(int i=0; i<n; i++){
arr[i][1] = sc.nextInt();
}
// 先按照x进行升序排列,如果x相同,就按照y降序排列(x相等也是不符合,所以直接y降序强行不符合)
Arrays.sort(arr, (o1,o2)->{
if(o1[0] > o2[0])
return 1;
else if(o1[0] < o2[0])
return -1;
else
{
if(o1[1] > o2[1])
return -1;
else if(o1[1] < o2[1])
return 1;
else
return -1;
}
});
int[] nums = new int[n];
for(int i=0; i<n; i++){
nums[i] = arr[i][1];
}
System.out.println(longestSubArr(nums));
}
}
public static int longestSubArr(int[] nums){
// 求y的严格递增子序列的长度
// 因为要求O(NlogN),所以dp的思路就达不到了,需要用二分查找的思路
int[] tails = new int[nums.length]; // tails[k]记录长度为k+1的子序列的尾部元素值
int res = 0;
for(int num : nums){
int left = 0, right = res;
// 前闭后开
while(left<right){
int mid = left + (right-left)/2;
if(tails[mid] < num){
left = mid+1;
}else{
right = mid;
}
}
tails[left] = num;
if(right == res) res++;
}
return res;
}
}
解题思路
- 总体的思路,是对给的二维数组排序,在保证x升序的情况下,求出y最大递增子序列。
- 其中的一个小细节是,x相等的情况,是不包含在结果中的,所以索性让这种情况下的y降序,从而肯定不会满足条件。
- 排好序后,对y求最大递增子序列。