第一题
给你一个整数数组 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 。
分析
第一种方法:动态规划,设dp[i]表示以数组下标i结尾的最长序列,那么可以得到下列递推公式:
d
p
[
j
]
=
d
p
[
i
]
+
1
(
0
≤
i
<
j
且
a
r
r
[
j
]
>
a
r
r
[
i
]
,
d
p
[
i
]
是
满
足
条
件
的
最
大
值
)
dp[j] = dp[i]+1(0 \le i <j且arr[j] > arr[i],dp[i]是满足条件的最大值)
dp[j]=dp[i]+1(0≤i<j且arr[j]>arr[i],dp[i]是满足条件的最大值)
public int lengthOfLIS(int[] nums) {
if(nums.length == 0) {
return 0;
}
int[] dp = new int[nums.length];
dp[0] = 1;
int res = Integer.MIN_VALUE;
for(int i = 1;i<nums.length;i++) {
int mav = 0;
for(int j = 0;j<i;j++) {
if(nums[i]>nums[j]) {
mav = Math.max(mav, dp[j]);
}
}
dp[i] = mav+1;
res = Math.max(res, dp[i]);
}
return res>dp[0]?res:dp[0];
}
时间复杂度为o(n2)
第二种方法:动态规划+二分查找,这里我们设dp[i]表示最长升序序列长度为i的最小尾数,这里可以证明dp是非递减的,下面给出证明:
当k >i时,若dp[i]>dp[k],因为dp[k]代表长度为k的升序序列最小尾数,那么在这个序列中第i个数小于第k个数,即dp[i] < dp[k],这与假设矛盾,谷dp[i] <= dp[k],dp非递减
状态转移方程:
设当前遍历数组中的第i位,此时dp数组的有序部分最后一位为dp[k]
- 如果nums[i] > dp[k],那么dp[k+1] = nums[i]
- 如果nums[i]<=dp[k],那么就利用二分查找找出其插入的位置,然后更新该位置上的数值。
public int lengthOfLIS(int[] nums) {
int[] dp = new int[nums.length+1];
//初始化dp
dp[1] = nums[0];
//指向dp数组中最后一个有序数
int size = 1;
for(int i = 1; i < nums.length; i++) {
int position = findPosition(dp,1,size,nums[i]);
dp[position] = nums[i];
if(position > size) {
size++;
}
}
return size;
}
//二分查找
public int findPosition(int[] arr,int begin,int end,int target) {
int i = begin;
int j = end;
while(i <= j) {
int mid = (i + j) / 2;
if(target > arr[mid]) {
i = mid+1;
} else {
j = mid-1;
}
}
return i;
}
时间复杂度为o(nlogn)
第二题
给定数组arr,设长度为n,输出arr的最长递增子序列。(如果有多个答案,请输出其中字典序最小的)
示例1
输入:
[2,1,5,3,6,4,8,9,7]
返回值:
[1,3,4,8,9]
示例2
输入:
[1,2,8,6,4]
返回值:
[1,2,4]
说明:
其最长递增子序列有3个,(1,2,8)、(1,2,6)、(1,2,4)其中第三个字典序最小,故答案为(1,2,4)
分析
假设dplen[i]是以nums[i]结尾的序列的长度,那么在同一长度下,最右边的那个元素一定是字典序最小的,举个例子:序列“215364897”的每个元素对应的长度如下
2 | 1 | 5 | 3 | 6 | 4 | 8 | 9 | 7 |
---|---|---|---|---|---|---|---|---|
1 | 1 | 2 | 2 | 3 | 3 | 4 | 5 | 4 |
可以看出每个长度对应的序列是降序,如长度为1时,序列为“21”,长度为2时,序列为“53”,因此要找出最长序列的最小字典序,从后往前遍历找到遇到的第一个对应长度的字符,如找到长度为5的最小字典序:
- 从后往前遍历找到第一个对应长度为5的字符,这里是9
- 继续从后往前遍历找到对应长度为4的字符,这里是8,
- 继续从后往前遍历找到对应长度为3的字符,这里是4,
- 继续从后往前遍历找到对应长度为2的字符,这里是3,
- 继续从后往前遍历找到对应长度为1的字符,这里是1,
长度为5对应的最小字典序列就是13489
public class Solution {
public int[] LIS (int[] nums) {
// write code here
//dp[i]数组保存的是最长上升子序列长度为i的最下尾数
int[] dp = new int[nums.length+1];
//dplen[i]保存的是以nums[i]结尾的序列长度
int[] dplen = new int[nums.length+1];
dp[1] = nums[0];
int size = 1;
dplen[0] = 1;
for(int i = 1; i < nums.length; i++) {
int position = findPosition(dp,1,size,nums[i]);
dp[position] = nums[i];
if(position > size) {
size++;
}
dplen[i] = position;
}
int [] res = new int[size];
//从后往前遍历
for(int i = nums.length-1,j = size; i >= 0; i--) {
if(dplen[i] == j) {
res[--j] = nums[i];
}
}
return res;
}
public int findPosition(int[] arr,int begin,int end,int target) {
int i = begin;
int j = end;
while(i <= j) {
int mid = (i + j) / 2;
if(target > arr[mid]) {
i = mid+1;
} else {
j = mid-1;
}
}
return i;
}
}