您的位置:首页 > 产品设计 > UI/UE

编程之美2.16-最长递增子序列(Longest Subsequence)

2017-08-25 22:35 309 查看

最长子序列问题(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<
b00f
/span>(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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息