动态规划求最长递增子序列

Posted 小鸡炖蘑菇_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划求最长递增子序列相关的知识,希望对你有一定的参考价值。

最长递增子序列问题是一个很基本、较常见的小问题,但这个问题的求解方法却并不那么显而易见,需要较深入的思考和较好的算法素养才能得出良好的算法。由于这个问题能运用学过的基本的算法分析和设计的方法与思想,能够锻炼设计较复杂算法的思维,我对这个问题进行了较深入的分析思考,得出了几种复杂度不同算法,并给出了分析和证明。

一, 最长递增子序列问题的描述

设L=<a1,a2,…,an>是n个不同的实数的序列,L的递增子序列是这样一个子序列Lin=<aK1,ak2,…,akm>,其中k1<k2<…<km且aK1<ak2<…<akm。求最大的m值。

二, 第一种算法:转化为LCS问题求解

设序列X=<b1,b2,…,bn>是对序列L=<a1,a2,…,an>按递增排好序的序列。那么显然X与L的最长公共子序列即为L的最长递增子序列。这样就把求最长递增子序列的问题转化为求最长公共子序列问题LCS了。

最长公共子序列问题用动态规划的算法可解。设Li=< a1,a2,…,ai>,Xj=< b1,b2,…,bj>,它们分别为L和X的子序列。令C[i,j]为Li与Xj的最长公共子序列的长度。则有如下的递推方程:

#include <iostream>
using namespace std;

/* 最长递增子序列 LIS
 * 设数组长度不超过 30
 * quicksort + LCS
*/

void swap(int * arr, int i, int j)

    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;


void qsort(int * arr, int left, int right)

    if(left >= right)    return ;
    int index = left;
    for(int i = left+1; i <= right; ++i)
    
        if(arr[i] < arr[left])
        
            swap(arr,++index,i);
        
    
    swap(arr,index,left);
    qsort(arr,left,index-1);
    qsort(arr,index+1,right);


int dp[31][31];

int LCS(int * arr, int * arrcopy, int len)

    for(int i = 1; i <= len; ++i)
    
        for(int j = 1; j <= len; ++j)
        
            if(arr[i-1] == arrcopy[j-1])
            
                dp[i][j] = dp[i-1][j-1] + 1;
            else if(dp[i-1][j] > dp[i][j-1])
            
                dp[i][j] = dp[i-1][j];
            else
            
                dp[i][j] = dp[i][j-1];
            
        
    
    return dp[len][len];


void main()

    int arr[] = 1,-1,2,-3,4,-5,6,-7;
    int arrcopy [sizeof(arr)/sizeof(int)];

    memcpy(arrcopy,arr,sizeof(arr));
    qsort(arrcopy,0,sizeof(arr)/sizeof(int)-1);

    /* 计算LCS,即LIS长度 */
    int len = sizeof(arr)/sizeof(int);
    printf("%d\\n",LCS(arr,arrcopy,len));

这可以用时间复杂度为O(n2)的算法求解。求最长递增子序列的算法时间复杂度由排序所用的O(nlogn)的时间加上求LCS的O(n2)的时间,算法的最坏时间复杂度为O(nlogn)+O(n2)=O(n2)。

三, 第二种算法:动态规划法

设f(i)表示L中以ai为末元素的最长递增子序列的长度。则有如下的递推方程:
f[i] = max(f[j]) + 1
这个递推方程的意思是,在求以ai为末元素的最长递增子序列时,找到所有序号在L前面且小于ai的元素aj,即j<i且aj<ai。如果这样的元素存在,那么对所有aj,都有一个以aj为末元素的最长递增子序列的长度f(j),把其中最大的f(j)选出来,那么f(i)就等于最大的f(j)加上1,即以ai为末元素的最长递增子序列,等于以使f(j)最大的那个aj为末元素的递增子序列最末再加上ai;如果这样的元素不存在,那么ai自身构成一个长度为1的以ai为末元素的递增子序列。代码如下:

#include <iostream>
using namespace std;

/* 最长递增子序列 LIS
 * 设数组长度不超过 30
 * DP
*/

int dp[31]; /* dp[i]记录到[0,i]数组的LIS */
int lis;    /* LIS 长度 */

int LIS(int * arr, int size)

    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;


/* 输出LIS */
void outputLIS(int * arr, int index)

    bool isLIS = 0;
    if(index < 0 || lis == 0)
    
        return;
    
    if(dp[index] == lis)
    
        --lis;
        isLIS = 1;
    

    outputLIS(arr,--index);

    if(isLIS)
    
        printf("%d ",arr[index+1]);
    


void main()

    int arr[] = 1,-1,2,-3,4,-5,6,-7;

    /* 输出LIS长度; sizeof 计算数组长度 */
    printf("%d\\n",LIS(arr,sizeof(arr)/sizeof(int)));

    /* 输出LIS */
    outputLIS(arr,sizeof(arr)/sizeof(int) - 1);
    printf("\\n");

这个算法有两层循环,外层循环次数为n-1次,内层循环次数为i次,算法的时间复杂度T(n)=O(n2)。这个算法的最坏时间复杂度与第一种算法的阶是相同的。但这个算法没有排序的时间,所以时间复杂度要优于第一种算法。
这个方法也最容易想到也是最传统的解决方案,对于该方法和LIS,有以下两点说明:

  • 由LIS可以衍生出来最长非递减子序列,最长递减子序列,道理是一样的。
  • 对于输出序列,也是可以再申请一数组pre[i]记录子序列中array[i]的前驱,道理跟本节的实现也是一样的

四, 对第二种算法的改进

《编程之美》对于这个方法有提到,不过它的讲解我看得比较难受,好长时间才明白,涉及到的数组也比较多,除了源数据数组,有LIS[i]和MaxV[LIS[i]],后来发现编程之美中的这个数组MaxV[LIS[i]]在记录信息上其实是饶了弯的,因为我们在寻找某一长度子序列所对应的最大元素最小值时,完全没必要通过LIS[i]去定位,即没必要与数据arr[i]挂钩,直接将MaxV[i]的下标作为LIS的长度,来记录最小值就可以了(表达能力太次,囧。。。),一句话,就是不需要LIS[i]这个数组了,只用MaxV[i]即可达到效果,而且原理容易理解,代码表达也比较直观、简单。

下面说说原理:

目的:我们期望在前i个元素中的所有长度为len的递增子序列中找到这样一个序列,它的最大元素比arr[i+1]小,而且长度要尽量的长,如此,我们只需记录len长度的递增子序列中最大元素的最小值就能使得将来的递增子序列尽量地长。

方法:维护一个数组MaxV[i],记录长度为i的递增子序列中最大元素的最小值,并对于数组中的每个元素考察其是哪个子序列的最大元素,二分更新MaxV数组,最终i的值便是最长递增子序列的长度。这个方法真是太巧妙了,妙不可言。

#include <iostream>
using namespace std;

/* 最长递增子序列 LIS
 * 设数组长度不超过 30
 * DP + BinarySearch
*/

int MaxV[30]; /* 存储长度i+1len)的子序列最大元素的最小值 */
int len;      /* 存储子序列的最大长度 即MaxV当前的下标*/

/* 返回MaxV[i]中刚刚大于x的那个元素的下标 */
int BinSearch(int * MaxV, int size, int 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;


int LIS(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;


void main()

    int arr[] = 1,-1,2,-3,4,-5,6,-7;

    /* 计算LIS长度 */
    printf("%d\\n",LIS(arr,sizeof(arr)/sizeof(int)));

以上是关于动态规划求最长递增子序列的主要内容,如果未能解决你的问题,请参考以下文章

算法 LC 动态规划 - 最大递增子序列

JAVA动态规划,最长递增子序列的代码太难理解,求大神帮我讲解一下!

动态规划求最长递增子序列的长度

动态规划求最长递增子序列

单调递增最长子序列(动态规划)

nyoj 17-单调递增最长子序列(动态规划,演算法)