编程之美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
相关文章推荐
- 编程之美2.16求数组中最长递增子序列
- 求数组中最长递增子序列(编程之美2.16)
- 2.16 求数组中最长的递增子序列
- [编程之美] PSet2.16 求数组中最长的递增子序列
- 编程之美2.16 最长递增子序列
- 2.16 最长递增子序列 LIS
- 《编程之美》读书笔记17: 2.16 求数组中最长递增子序列
- 编程之美读书笔记2.16—求数列中最长递增序列
- 编程之美2.16 求数组中最长递增子序列
- 编程之美2.16 最长递增子序列
- 编程之美2.16求数组中最长递增子序列Java版
- 编程之美2.16——求数组中最长递增子序列
- 《编程之美》读书笔记17: 2.16 求数组中最长递增子序列
- 编程之美2.16 求数组中最长递增子序列
- 最长递增子序列,时间复杂度(O(nlogn))
- 最大子序列、最长递增子序列、最长公共子串、最长公共子序列、字符串编辑距离
- 最长递增子序列-动态规划(引用编程之美)
- 最长递增子序列B(完美2017实习生)
- 51nod 1134 最长递增子序列 DP
- 求数组中最长递增子序列的长度