最长子序列问题(Longest Subsequence)
猴子摘桃问题
首先用一道阿里笔试题引出最长子序列问题:
小猴子下山,沿着下山的路有一排桃树,每棵树都结了一些桃子。小猴子想摘桃子,但是有一些条件需要遵守,小猴子只能沿着下山的方向走,不能回头,每颗树最多摘一个,而且一旦摘了一棵树的桃子,就不能再摘比这棵树结的桃子少的树上的桃子。那么小猴子最多能摘到几颗桃子呢?
举例说明,比如有5棵树,分别结了10,4,5,12,8颗桃子,那么小猴子最多能摘3颗桃子,来自于结了4,5,8颗桃子的桃树。
这里我们发现该问题的本质是求解给定数列的最长递增子序列,比如10,4,5,12,8中的最长递增子序列是4,5,8,也可以是4,5,12,长度是3.
解法一
记录以当前元素为最大元素的最长序列长度
首先最简单的思路:
开辟一个lis数组,记录以arr[i]作为最大元素子序列的长度。
遍历到下一个元素arr[i+1],遍历lis数组前i个元素,
- 如果比lis[k]大,说明该元素可以作为 以lis[k]为最大元素的这个递 增子序列的最后一个元素。
- 然后lis[k]+1,更新当前lis[i+1]的值,在遍历前i个元素后存储最大的值,举例
arr:12,4,5,8,13,14,9
lis:1,1,2,3,2,3,4
新增元素20,对应lis[7]
经历1+1,2+1,3+1,4+1最后等于最大的4+1=5
最后遍历所有lis,最大的元素即为最长递增子序列的长度
public class LIS_A {
public int lis(int arr[]){
assert arr!=null:"数组为空";//临界值测试 数组长度为0,1分别返回0,1,正常
int arrL=arr.length;
int[] lis=new int[arrL];//lis[i]表示以arr[i]作为最大元素 子序列的长度
for(int i=0;i<arrL;i++){
//即使没有其他更小元素,当前元素本身就是长度为1的递增子序列,所以初始长度为1
lis[i]=1;
for(int j=0;j<i;j++){
//如果arr[i]>arr[j]并且lis[j]+1>lis[i],因为之前可能有很多子序列,所以保证lis[i]是最长的
if(arr[i]>arr[j]&&lis[j]+1>lis[i])
lis[i]=lis[j]+1;
}
}
return max(lis);
}
/*返回数组最大值O(n)*/
public int max(int arr[]){
int max=0;
for (int i = 0; i < arr.length; i++) {
if(arr[i]>max)
max=arr[i];
}
return max;
}
}
该解法每次都要遍历前面所有lis[k] 所以输入n个元素 时间复杂度是O(n^2+n)
解法二
记录某一长度序列的最大元素最小值
解法一 每到一个元素,都要遍历之前的所有lis元素,更好的思路是:
记录下 不同长度的子序列的最小值:MaxV[k] (长度为k的子序列的最小值)
然后对比当前元素会有两种情况:
- 如果当前元素MaxV[k] < arr[i]< MaxV[k+1]说明arr[i]可以加在MaxV[k]后面成为新的k+1长的序列;
- 又因为小于MaxV[k+1],所以当前arr[i]应该取代MaxV[k+1]成为k+1长序列新的最小值:MaxV[j+1]=arr[i]
- 如果当前当前元素 arr[i] > MaxV[k] (MaxV[k]是MaxV数组最后的元素,k是当前最长的长度)
- 所以加入arr[i]后会构成新的最长k+1; 因而MaxV[k+1]=arr[i]
public class LIS_B {
public int lis(int arr[]){
assert arr.length>0:"输入错误";
int arrL=arr.length;
int[] lis=new int[arrL];
/*MaxV是个比较难理解的地方
MaxV[j] 记录长度为j的许多序列中 最大值的最小值,所以新值只要大于这个值就可以插在后面构成j+1长的新子数组
最长可能为arrL所以MaxV[arrL]也得有
MaxV[0]是数组最小值减1,存在的意义是为了更新MaxV[1]
比如[5,6]插入1,长度为1的子序列最大元素最小值MaxV[1]此刻是5,插入1那么这个最小值就应该是1
而插入机制是MaxV[j]<arr[i]&&arr[i]<MaxV[j+1],意味着arr[i]插入构成j+1的新数列,因而要覆盖Max[j+1]成为新的最小值
所以此时MaxV[0]<1<MaxV[1],1会写入MaxV[1]成为新的长度为1的子序列最大元素最小值*/
int[] MaxV=new int[arrL+1];
MaxV[1]=arr[0];//其实也可以不给MaxV[1]赋值(maxLength=0即可),但是比较大小时会很麻烦
MaxV[0]=min(arr)-1;//临界值,元素至少能写入MaxV[1]
int maxLength=1;
for(int i=0;i<arrL;i++){
int j;//子序列长度
for(j=maxLength;j>=0;j--) { /*从maxLength开始找,一直到0,插入对应的位置*/
if (arr[i] >MaxV[j]) { //非递增,或者递减可以在这里修改
lis[i] = j + 1;
break;
}
}
//两种情况,大于当前最长,所以构成新的最长,maxLength+1,并把当前值赋给MaxV[lis[i]]等同maxV[maxLength]
if(lis[i]>maxLength){
maxLength=lis[i];
MaxV[lis[i]]=arr[i];
}
else if(MaxV[j]<arr[i]&&arr[i]<MaxV[j+1]){ //非递增,或者递减可以在这里修改
MaxV[j+1]=arr[i];
}
}
return maxLength;
}
private int min(int arr[]){
int min=arr[0];
for (int i = 1; i < arr.length; i++) {
if(arr[i]<min)
min=arr[i];
}
return min;
}
}
解法三-二叉搜索树 书上没有
之前解法二虽然优化后更快 但实际上还是O(n^2)的算法,
- 定位- arr[i]到对应MaxV的位置,可以使用二叉搜索树
- 总体复杂度降为O(nlogn),自己写了一版供大家参考。
/记录最小值,二分搜索树搜索(书上没有) 时间复杂度从O(N^2)降到O(N*logN)
public class LIS_C {
public int lis(int arr[]){
assert arr.length>0:"输入错误";
int arrL=arr.length;
int[] lis=new int[arrL];
int[] MaxV=new int[arrL+1];
MaxV[1]=arr[0];//其实也可以不给MaxV[1]赋值(maxLength=0即可),但是比较大小时会很麻烦
MaxV[0]=min(arr)-1;//临界值,元素至少能写入MaxV[1]
int maxLength=1;
for(int i=0;i<arrL;i++){
int j;//对应的次长子序列长度
//两种情况,大于当前最长,所以构成新的最长,maxLength+1,并把当前值赋给MaxV[lis[i]]等同maxV[maxLength]
if(arr[i]>MaxV[maxLength]) {
j = maxLength;
maxLength++;
MaxV[maxLength] = arr[i];
}
else{
j = binarySearch(arr[i], maxLength, MaxV);
MaxV[j + 1] = arr[i];
}
lis[i]=j+1;
}
return maxLength;
}
//二叉搜索树搜索 推荐用双索引 而不是用递归 这样效率更高 MaxV[searchS]<input<=MaxV[searchE] 返回searchS
private int binarySearch(int input,int maxLength,int MaxV[]){
int searchS=0; //游标起点
int searchE=maxLength; //游标终点
while(searchS+1<searchE){
int searchM=searchS+(searchE-searchS)/2;
if(input>MaxV[searchM])
searchS=searchM;
else
searchE=searchM;
}
return searchS;
}
private int min(int arr[]){
int min=arr[0];
for (int i = 1; i < arr.length; i++) {
if(arr[i]<min)
min=arr[i];
}
return min;
}
}
测试函数
public class Main {
public static void main(String[] args) {
int arr[]={12,3,4,5,13,6,7,14};
System.out.println(Arrays.toString(arr));
System.out.println("最长子序列长度为"+new LIS_A().lis(arr));
}
运行结果: [12, 3, 4, 5, 13, 6, 7, 14]
最长子序列长度为6