最长递增子序列(Longest Increasing Subsequence) ,我们简记为 LIS。
题:求一个一维数组arr[i]中的最长递增子序列的长度,如在序列1,-1,2,-3,4,-5,6,-7中,最长递增子序列长度为4,序列为1,2,4,6。
解法一:快速排序+LCS
刚开始做这道题的时候,由于之前做过几道LCS的题,于是最先想到的是快速排序+LCS的方法。这种方法解决了当时只计算单个case的问题,但是后来面对计算多个 case的问题的时候,第一次遇到Memory Limit Exceeded。于是就意识到这种简单解法的时间和空间的复杂度都太高了。于是只能另寻他法。
public class Main {
static int n;
static int[] a;
static int[] b;
static int[][] z;
public static void QuickSort(int[] a){
QSort(a,1,n);
}
public static void QSort(int[] a,int p,int r){
if(p<r)
{
int q=Partition(a,p,r);
QSort(a,p,q-1);
QSort(a,q+1,r);
}
}
public static int Partition(int[] a,int p,int r){
int x=a[r];
int i=p-1;
for(int j=p;j<r;j++)
{
if(a[j]<=x){
i=i+1;
swap(a, i, j);
}
}
swap(a, i+1, r);
return i+1;
}
public static void swap(int[] a, int i,int j){
int temp;
temp=a[j];
a[j]=a[i];
a[i]=temp;
}
public static int LCS(int a[],int[] b){
z=new int [n+1][n+1];
int i,j;
for( i=0;i<=n;i++)
z[i][0]=0;
for( j=0;j<=n;j++)
z[0][j]=0;
for(i=1;i<=n;i++){
for( j=1;j<=n;j++){
if(a[i]==b[j]){
z[i][j]= z[i-1][j-1]+1;
}
else
z[i][j]=z[i-1][j] > z[i][j-1] ?z[i-1][j]:z[i][j-1];
}
}
return z[n][n];
}
public static void main(String[] args) {
int arr[] = {1,-1,2,-3,4,-5,6,-7};
n=arr.length;
a=new int[n+1];
b=new int[n+1];
int i,j;
for(i=1;i<=n;i++){
b[i]=a[i];
}
QuickSort(a);
//控制严格递增
for(i=1;i<n;i++){
for(j=i+1;j<=n;j++){
if(a[i]!=-1 && a[i]==a[j])
a[j]=-1;
}
}
System.out.println(LCS(a,b));
}
}
解法二:DP(O(N^2))
LIS[i+1] = max{1,LIS[k]+1},aray[k],for any k <=i.
即如果array[i+1]大于array[k],那么第i+1个元素可以接在LIS[k]长的子序列后面构成一个更长的子序列。于此同时array[i+1]本身至少可以构成一个长度为1的子序列。
public int LIS(int[] arr, int size){
int dp[40]; /* dp[i]记录到[0,i]数组的LIS */
int lis; /* LIS 长度 */
for(int i = 0; i < size; ++i){
dp[i] = 1;
for(int j = 0; j < i; ++j){
if(arr[i] > arr[j] && dp[i] < dp[j] + 1){
dp[i] = dp[j] + 1;
if(dp[i] > lis){
lis = dp[i];
}
}
}
}
return lis;
}
解法三:二分查找+DP(O(nlogn))
在解法二中,当考察第i+1个元素的时候,我们是不考虑前面i个元素的分布情况的。现在我们从另一个角度分析,即当考察第i+1个元素的时候考虑前面i个元素的情况。
目的:我们期望在前i个元素中的所有长度为len的递增子序列中找到这样一个序列,它的最大元素比arr[i+1]小,而且长度要尽量的长,如此,我们只需记录len长度的递增子序列中最大元素的最小值就能使得将来的递增子序列尽量地长。
方法:维护一个数组MaxV[i],记录长度为i的递增子序列中最大元素的最小值,并对于数组中的每个元素考察其是哪个子序列的最大元素,二分更新MaxV数组,最终i的值便是最长递增子序列的长度。
仔细的分析请看最长递增子序列 O(NlogN)算法,
public class LIS {
/* 最长递增子序列 LIS
* 设数组长度不超过 30
* DP + BinarySearch
*/
static int[] MaxV=new int[30]; /* 存储长度i+1(len)的子序列最大元素的最小值 */
static int len; /* 存储子序列的最大长度 即MaxV当前的下标*/
static int BinSearch(int[] MaxV, int size, int x){ /* 返回MaxV[i]中刚刚不小于x的那个元素的下标 */
int left = 0, right = size-1;
while(left <= right){
int mid = (left + right) / 2;
if(MaxV[mid] <= x){
left = mid + 1;
}else{
right = mid - 1;
}
}
return left;
}
static int getLIS(int[] arr, int size){
MaxV[0] = arr[0]; /* 初始化 */
len = 1;
for(int i = 1; i < size; ++i){ /* 寻找arr[i]属于哪个长度LIS的最大元素 */
if(arr[i] > MaxV[len-1]){ /* 大于最大的自然无需查找,否则二分查其位置 */
MaxV[len++] = arr[i];
}else{
int pos = BinSearch(MaxV,len,arr[i]);
MaxV[pos] = arr[i];
}
}
return len;
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
int arr[] = {1,-1,2,-3,4,-5,6,-7};
/* 计算LIS长度 */
System.out.println(getLIS(arr,arr.length));
}
}
参考资料:
《编程之美》 2.16
Felix’s Blog:最长递增子序列 O(NlogN)算法
勇幸|Thinking (http://www.ahathinking.com)