给你一个整数数组nums,找到其中最长严格递增子序列的长度
先说几句吧,这道题确实适合深入的研究和剖析,今天好好来研究这道题.
首先,最简单的做法就是动态规划,dp[i]表示"在数组nums中i位置的最长子序列的长度"
,因此很简单的动态规划,时间复杂度在O(n^2)
//纯用动态规划来做可以实现O(n^2),dp[i]表示数组i位置(包含i)的最长子序列长度
public static int lengthOfLIS1(int[] nums){
if (nums.length==0) return 0;
int[] dp = new int[nums.length];
int maxLen = 0;
dp[0] = 1;
for (int i=1;i<nums.length;i++){
dp[i] = 1;//初始化都是长度为1的 毕竟自己就算一个
for (int j=0;j<i;j++){
if (nums[j]<nums[i]){
dp[i] = Math.max(dp[i],dp[j]+1);
}
}
maxLen = Math.max(maxLen,dp[i]);
}
return maxLen;
}
其次,是使用动态规划+二分,可以实现时间复杂度O(nlogn).这里的dp[i]表示的是"长度为i的子序列末尾元素的最小值"
public static int lengthOfLIS2(int[] nums){
if (nums.length==0) return 0;
//维护一个数组dp[i],表示长度为i的最长上升子序列的末尾元素的最小值,用len记录目前最长上升子序列的长度,起始时len为1,dp[1]=nums[0]
int[] dp = new int[nums.length+1];
int len = 1;
dp[1] = nums[0];
for (int i=1;i<nums.length;i++){
if (nums[i]>dp[len]){//将nums[i]加入到dp[len]后面
len++;
dp[len] = nums[i];
}else{//利用二分查找找到nums[i]应该插入的位置 即dp[i−1]<nums[j]<dp[i]的下标i,并更新dp[i]=nums[j]
int left = 1,right = len,pos = 0;//如果找不到 说明所有的数都比nums[i]大,此时要更新dp[1],所以这里将 pos 设为 0
while (left<=right){
int mid = left + ((right-left)>>1);
if (dp[mid]<nums[i]){
pos = mid;
left = mid+1;
}else{
right = mid-1;
}
}
//找到了应该放的位置
dp[pos+1] = nums[i];
}
}
return len;
}
上面的代码中还有一个比较有用的部分,就是给你一个target,让你在有序数组nums中找到小于target的最大的那个数,也就是target应该出入的位置
public int findnum(int[] nums,int target){
int left = 0,right = nums.length-1,pos = 0;
while (left<=right){
int mid = left+(right-left)>>1;
if (nums[mid]<target){
pos = mid;//更新位置 然后一轮一轮的循环 最终逼近到最佳的那个位置
left = mid+1;
}else{
right=mid-1;
}
}
return pos;
}
现在来看最难的部分了,让你返回这个最长的子序列!!!
方法一:利用,时间复杂度在O(n^2)的纯动态规划进行修改.整一个"List<List< Integer >> ans"用于存放每个位置的最长子序列
//进阶版 要求出那个最长的子序列(用纯动态规划改编)
public static int lengthOfLIS3(int[] nums){
if (nums.length==0) return 0;
int[] dp = new int[nums.length];
List<List<Integer>> ans = new ArrayList<>();//用于记录每个位置的最长子序列
int maxLen = 0,index = -1;
for (int i=0;i<nums.length;i++){
dp[i] = 1;//初始化都是长度为1的 毕竟自己就算一个
List<Integer> res = new ArrayList<>();
res.add(nums[i]);//子序列的初始化都是自己一个元素
ans.add(res);
for (int j=0;j<i;j++){
if (nums[j]<nums[i]){
if (dp[i]<dp[j]+1){
List<Integer> newres = new ArrayList<>(ans.get(j));//j的路径拿过来
newres.add(nums[i]);
ans.remove(i);//原来i这个地方的路径就是一个初始化的自己
ans.add(newres);//现在换成了newres
dp[i] = dp[j]+1;
}
}
}
if (maxLen<dp[i]){
maxLen = dp[i];
index = i;
}
}
System.out.println(Arrays.toString(ans.get(index).toArray()));
return maxLen;
}
方法二:用时间复杂度O(nlogn)的动态规划+二分改编,这里确实是有点绕,但是捋清楚了就不难了.
- 首先dp[i]表示以nums[i]结尾的最长递增子序列长度
- tail[i]表示长度为i的最长子序列的最小结尾数字
这一步你仔细看,其实就是把纯动态规划时候的dp[i]和动态规划+二分时的dp[i]两个全拿过来了.就这是最绕的,只要这两个数组搞明白就简单了.
//进阶版 要求出那个最长的子序列(用动态规划+二分改编)
//这题是真的逆天,注意注意注意!!!dp[]数组代表的是每个位置的最长子序列长度
//tail[]数组则表示的是长度为i的子数组的最小尾元素
public static int lengthOfLIS4(int[] nums){
if (nums.length==0) return 0;
int[] dp = new int[nums.length+1];//dp[i]表示以arr[i]结尾的最长递增子序列长度
int[] tail = new int[nums.length+1];//tail[i]表示长度为i的最长子序列的最小结尾数字
int len = 0;
tail[0] = Integer.MIN_VALUE;
for (int i=0;i<nums.length;i++){
if (nums[i]>tail[len]){//将nums[i]加入
len++;
tail[len] = nums[i];//长度为len的子序列的末尾最小数为nums[i]
dp[i] = len;//以arr[i]结尾的最长递增子序列长度为i
}else{//利用二分查找找到nums[i]应该插入的位置 即dp[i−1]<nums[j]<dp[i]的下标i,并更新dp[i] = nums[j]
int left = 1,right = len,pos = 0;//如果找不到 说明所有的数都比nums[i]大,此时要更新dp[1],所以这里将pos设为0
while (left<=right){
int mid = left + ((right-left)>>1);
if (dp[mid]<nums[i]){
pos = mid;
left = mid+1;
}else{
right = mid-1;
}
}
//找到了应该放的位置
tail[pos+1] = nums[i];//长度为pos+1的子序列的末尾最小数为nums[i]
dp[i] = pos+1;//以arr[i]结尾的最长递增子序列长度为pos+1
}
}
//dp[]数组记录了每个位置的子序列长度
//tail[]数组记录了每个长度的最小结尾数字
int[] ans = new int[len];
int index = len;
for (int i=nums.length-1;i>=0;i--){
if (dp[i] == index){//寻找长度等于index的序列
ans[index - 1] = tail[index];
index--;
}
}
System.out.println(Arrays.toString(ans));
return len;
}