题目:给定数组arr,设长度为n,输出arr的最长递增子序列。(如果有多个答案,请输出其中字典序最小的)
eg: 输入:[2,1,5,3,6,4,8,9,7]
输出:[1,3,4,8,9]
这题好不容易写出了动归,结果超时了。。。。难过
说一下另一种解法,还是看别人的代码理解了很久。贪心+二分。
首先在动归中有一个操作,是查找当前元素位置之前,第一个小于当前元素的值,正是这个查询操作导致动归解法的时间复杂度变成了,所以这里用二分查找来实现以减少时间复杂度。(动归+二分应该也是可以?我没有尝试,觉得这种贪心的解法挺有趣的就记录一下)
然后说一下具体的解法,首先使用一个数组 lens 来存储当前元素所能构成的最长上升子序列,然后使用一个数组 flags 来存储上升子序列的临时元素,但是要注意这个数组中的元素并不一定是最终结果,其作用主要是辅助更新 lens 数组,最终的最长上升子序列的构成也是由 lens 和原始数组 arr 生成的。
首先原始数组为 arr = {2,1,5,3,6,4,8,9,7},我们定义一个最大长度max来记录所能达到的最大上升子序列的长度,初始化 flags = {2,0,0,0,0,0,0,0,0},lens = {1,0,0,0,0,0,0,0,0},使用循环从第1个元素开始遍历每个元素(数组元素从第0个开始计算),每当到达一个元素i,判断当前元素 arr[i] 和 flags[max-1] 的大小(这里的 flags[max-1] 是flags的最后一个元素有效元素),
1. 如果二者相等的话,直接将 lens[i] 置为max,因为此时所能构成的最大长度是不变的,大小相等也不需要对元素进行替换;
2. 如果当前元素 arr[i] 大于 flags[max-1],那么所能构成的最大长度 max++,flags[max-1]置为当前元素,并且更新 lens[i] 为 max;
3. 如果当前元素 arr[i] 是小于 flags[max-1]的,那么说明最大长度不变,但是所构成的子序列中,arr[i] 作为候选元素,可能可以去替换序列中的某个元素,因此需要在 flags 中使用二分进行查找,找到第一个小于 arr[i] 的元素下标 index 进行替换,替换之后,更新 lens[i] 为 index+1(按照之前提到的 flags 中的元素位置是和最长上升子序列绑定的,flags中的位置实际就是最长上升子序列的位置,因此 index 表示了从0到index的子序列的长度-1,所以 index+1 表示的是到元素 flags[i] 所能构成的子序列的长度),这一块当时理解费了点力气。。
经过上述步骤,可以计算出 lens = {1,1,2,2,3,3,4,5,4},然后再次使用贪心算法获得最终结果。
由于题目要求的是最小字典,我们在计算 flags 和 lens 的时候已经按照这个逻辑进行了,并且我们得到了最大长度是5,我们在 lens 中可以看到,存在多个元素所能构成的最大长度相等的情况,那么应该怎么去判断哪个是小字典的呢。
首先明确 lens 值相等有两种情况,1. 元素值相等,那么能够构成的最长序列的长度自然相等,这种情况对于字典大小是没有影响的;2.后面元素的值小于前面元素的值,更新 lens 后二者的值相等。
从上面的分析就可以得到,第一种情况可以忽略不计,那相等的来源就是第二种情况,以及在对第二种情况的分析中我们也知道了,对于 lens 相等的元素,越靠后的值是越小的,那么我们就可以从后向前遍历,从 lens 值等于max 的位置开始,判断当前值是不是等于 max,是的话直接拿到当前元素,max--,这样也就直接跳过了前面的等于max的值,进入了下一个位置的挑选。
即选出的 lens 中的元素对应的位置为 1,3,5,6,7,对应 arr 中的元素为 1,3,4,8,9.
完整代码如下:
public static int[] LIS (int[] arr) {
// write code here
int max = 1; //记录最大长度
int[] lens = new int[arr.length]; //记录i能构成的最大长度
int[] flags = new int[arr.length]; //记录最长序列
flags[0] = arr[0]; //初始化最长序列
lens[0] = 1;
for(int i=1;i<arr.length;i++) {
if(flags[max-1]<arr[i]) { //当前元素大于最长序列,可以构成,直接放在后面
flags[max++] = arr[i];
lens[i] = max;
}else if(flags[max-1]>arr[i]){//当前元素小于最长序列,往前查找进行替换
int index = findFirst(flags,max,arr[i]); //更新的是最长序列的元素,不是原始数组
flags[index] = arr[i];
lens[i] = index+1; //index表示的位置,+1后表示的是长度
}else {
lens[i] = max;
}
}
int[] res = new int[max];
for(int i = arr.length-1;i>=0;i--) {
if(lens[i]==max) {
res[--max] = arr[i];
}
}
return res;
}
public static int findFirst(int[] arr,int i, int index) { //查找lens前i-1个元素中小于index的最长序列
int left = 0;
int right = i-1;
while(left<=right) {
int mid = (left+right)/2;
if(arr[mid]<index) {
left = mid+1;
}else{
right = mid-1;
}
}
return left;
}