最长上升子序列LIS(Longest increasing subsequence)

时间:2021-08-25 19:28:09

介绍

最长上升子序列问题,也就是Longest increasing subsequence缩写为LIS。是指在一个序列中求长度最长的一个上升子序列的问题。

问题描述:
给出一个序列a1,a2,a3,a4,a5,a6,a7….an,求它的一个子序列(设为s1,s2,…sn),使得这个子序列满足这样的性质,s1 <s2 <s3 <… <sn并且这个子序列的长度最长。输出这个最长的长度(为了简化该类问题,我们将诸如最长下降子序列及最长不上升子序列等问题都看成同一个问题,其实仔细思考就会发现,这其实只是 <符号定义上的问题,并不影响问题的实质)

例如有一个序列:1 7 3 5 9 4 8,它的最长上升子序列就是 1 3 4 8 长度为4。最长上升子序列问题有三种解法,第一种就是暴力破除,这种解法时间复杂度为在这里就不介绍了;一个是动态规划的解法,时间复杂度为O(n^2);最后介绍一种非常nice的解法,时间复杂度为O(nlogn)。

动态规划

dp[i表示以i结尾的子序列中LIS的长度。然后我用 dp[j](0 ≤j<i)来表示在i之前的LIS的长度。然后我们可以看到,只有当 a[i] >a[j] 的时候,我们需要进行判断,是否将a[i]加入到dp[j]当中。为了保证我们每次加入都是得到一个最优的LIS,有两点需要注意:
第一,每一次,a[i]都应当加入最大的那个dp[j],保证局部性质最优,也就是我们需要找到 max(dp[j](0 ≤j<i));
第二,每一次加入之后,我们都应当更新dp[j]的值,显然, dp[i]=dp[j]+1 。如果写成递推公式,我们可以得到 dp[i]=max(dp[j](0<= j<i))+(a[i]>a[j]?1:0)。

public static int dpLIS(int[] a){
    int[] dp=new int[a.length];
    int max;
    dp[0]=1;
    for(int i=1;i<a.length;i++){
        for(int j=0;j<i;j++){
            if(a[i]>a[j]&&dp[j]+1>dp[i]){
                dp[i]=dp[j]+1;
            }
        }
    }
    for(int i=max=0;i<dp.length;i++){
        if(dp[i]>max)
            max=dp[i];
    }
    return max;
}

O(nlogn)的方法

设 A[t]表示序列中的第t个数,F[t]表示从1到t这一段中以t结尾的最长上升子序列的长度,初始时设F [t] = 0(t = 1, 2, …, len(A))。则有动态规划方程:F[t] = max{1, F[j] + 1} (j = 1, 2, …, t - 1, 且A[j] <A[t])。现在,我们仔细考虑计算F[t]时的情况。假设有两个元素A[x]和A[y],满足

(1)x < y < t

(2)A[x] <A[y] <A[t]

(3)F[x] = F[y]

此时,选择F[x]和选择F[y]都可以得到同样的F[t]值,那么,在最长上升子序列的这个位置中,应该选择A[x]还是应该选择A[y]呢?

很明显,选择A[x]比选择A[y]要好。因为由于条件(2),在A[x+1] … A[t-1]这一段中,如果存在A[z],A[x] < A[z] < a[y],则与选择A[y]相比,将会得到更长的上升子序列。

再根据条件(3),我们会得到一个启示:根据F[]的值进行分类。对于F[]的每一个取值k,我们只需要保留满足F[t] =k的所有A[t]中的最小值。设D[k]记录这个值,即D[k] = min{A[t]} (F[t] = k)。

注意到D[]的两个特点:

(1) D[k]的值是在整个计算过程中是单调上升的。

(2) D[]的值是有序的,即D[1] < D[2] < D[3] < …<D[n]。

利用D[],我们可以得到另外一种计算最长上升子序列长度的方法。设当前已经求出的最长上升子序列长度为 len。先判断A[t]与D[len]。若A [t] > D[len],则将A[t]接在D[len]后将得到一个更长的上升子序列,len =len + 1,D[len] = A [t];否则,在D[1]..D[len]中,找到最大的j,满足D[j] < A[t]。令k = j + 1,则有A[t]≤D[k],将A[t]接在D[j]后将得到一个更长的上升子序列,更新D[k] = A[t]。最后,len即为所要求的最长上升子序列的度。

在上述算法中,若使用朴素的顺序查找在D[1]..D[len]查找,由于共有O(n)个元素需要计
算,每次计算时的复度 是O(n),则整个算法的 时间复杂度为O(n^2),与原来的算法相比没有任何进步。但是由于D[]的特点(2),我们在D[]中查找时,可以使用二分查找高效地完成,则整个算法 的时间复杂度下降为O(nlogn),有了非常显著的提高。需要注意的是,D[]在算法结束后记录的并不是一个符合题意的最长上升子序列。

于是我们就能够得到O(nlogn)方法的实现:

public static int binLIS(int[] arr){
     int[] maxV=new int[arr.length];
     maxV[0] = arr[0]; /* 初始化 */
     int len = 1;
        for(int i = 1; i < arr.length; ++i) /* 寻找arr[i]属于哪个长度LIS的最大元素 */
        {
            if(arr[i] > maxV[len-1]) /* 大于最大的自然无需查找,否则二分查其位置 */
            {
                maxV[len++] = arr[i];
            }else
            {
                int pos = binSeach(maxV,len,arr[i]);
                maxV[pos] = arr[i];
            }
        }
        return len;
}
public static int binSeach(int[] a,int len,int value){
    int left=0,right=len-1,mid;
    while(left<=right){
        mid=left+(right-left)/2;
        if(value>a[mid])
            left=mid+1;
        else if(a[mid]>value)
            right=mid-1;
        else {
            return mid;
        }

    }
    return left;
}
}