三种寻找最长递增(减)子序列的方法【LIS】

来源:互联网 发布:广电网络梅江电话 编辑:程序博客网 时间:2024/06/11 04:04

最长递增(减)子序列【LIS】三种解法

问题:

给定一个序列data[]={1, 6, 2, 5, 7, 9}求出他的的最长递增子序列,容易看出为{1, 2, 5, 7, 9},长度为5.同时这种问题还有一些衍生问法如:最长非递增(减)增子序列,最长递减子序列等解法都是一样的理解后改变一下条件就行了。

解法一:动态规划法(O(n^2))

动态规划法需要一个数组来保存每一个元素前LIS的长度,设长度为N的序列为{a0,a1, a2, …an-1),则假定以aj结尾的数组序列的LIS长度为L(j),则状态转移方程为:

l[i]={  l[j]+1 if l[j]<l[i]&&Xj<Xi

也就是说,我们需要遍历在i之前的所有位置j(从0到i-1),找出满足条件a[j]

代码
#include <iostream>#include <stdio.h>#include <algorithm>#include <string.h>#define N 100005        //最大长度using namespace std;int main()                //动态规划解法  O(N~2){    int w,x[N],l[N],max=0;    int n;cin>>n;    while(n--)    {        max=1;        cin>>w;        for(int i=0;i<w;i++)        {            cin>>x[i];            l[i]=1;            //起始最大长度都为1,即本身        }        for(int i=1;i<w;i++)            for(int j=0;j<i;j++)            {                if(x[j]<x[i]&&l[i]<=l[j])   //符合条件                {                    l[i]=l[j]+1;                    if(max<l[i])max=l[i];  //判断是否为当前最大值                }            }    cout<<max<<endl;// 输出最大值    }    return 0;}

解法二:最小末尾插入法(O(nlogn))

上面一种方法虽然可以求出但是数据量很大时,耗费时间会很长。所以使用这种方法时间复杂度为O(nlogn)是比较快的,关于这种方法网上也没有统一的名字就自己给它起个名字,原理就是遍历每一个元素将它插入数组L[N]中,此时数组l[]的下标为LIS的长度,数组中储存的则是当前长度时最小的元素,这样遍历到一个新的元素时,如果比L[N]中最后一个元素还要大,将它放到L[N]末尾L[N]的长度+1,否则的话查找元素应该插入的位置。这样讲可能不是很清楚,借用切梦博客中描述的过程

假设存在一个序列d[1..9] ={ 2,1 ,5 ,3 ,6,4, 8 ,9, 7},可以看出来它的LIS长度为5。
下面一步一步试着找出它。
我们定义一个序列B,然后令 i = 1 to 9 逐个考察这个序列。
此外,我们用一个变量Len来记录现在最长算到多少了

首先,把d[1]有序地放到B里,令B[1] = 2,就是说当只有1一个数字2的时候,长度为1的LIS的最小末尾是2。这时Len=1

然后,把d[2]有序地放到B里,令B[1] = 1,就是说长度为1的LIS的最小末尾是1,d[1]=2已经没用了,很容易理解吧。这时Len=1

接着,d[3] = 5,d[3]>B[1],所以令B[1+1]=B[2]=d[3]=5,就是说长度为2的LIS的最小末尾是5,很容易理解吧。这时候B[1..2] = 1, 5,Len=2

再来,d[4] = 3,它正好加在1,5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是可以把5淘汰掉,这时候B[1..2] = 1, 3,Len = 2

继续,d[5] = 6,它在3后面,因为B[2] = 3, 而6在3后面,于是很容易可以推知B[3] = 6, 这时B[1..3] = 1, 3, 6,还是很容易理解吧? Len = 3 了噢。

第6个, d[6] = 4,你看它在3和6之间,于是我们就可以把6替换掉,得到B[3] = 4。B[1..3] = 1, 3, 4, Len继续等于3

第7个, d[7] = 8,它很大,比4大,嗯。于是B[4] = 8。Len变成4了

第8个, d[8] = 9,得到B[5] = 9,嗯。Len继续增大,到5了。

最后一个, d[9] = 7,它在B[3] = 4和B[4] = 8之间,所以我们知道,最新的B[4] =7,> B[1..5] = 1, 3, 4, 7, 9,Len = 5。

于是我们知道了LIS的长度为5。

注意,这个1,3,4,7,9不是LIS,它只是存储的对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个d[9] = 7更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把8更新到d[5], 9更新到d[6],得出LIS的长度为6。

然后应该发现一件事情了:在B中插入数据是有序的,而且是进行替换而不需要挪动——也就是说,我们可以使用二分查找,将每一个数字的插入时间优化到O(logN)~于是算法的时间复杂度就降低到了O(NlogN)~!

代码
#include <iostream>#include <stdio.h>#include <algorithm>#include <string.h>#define N 100005  //最大长度using namespace std;int _2fen(int n[],int l,int a)//二分查找,n[]为标记数组,k为lis当前最大长度,a为需要插入的元素{    int mid,head=0,tail=l-1;    while(head<=tail)    {        mid=(head+tail)/2;        if(n[mid]<a)            head=mid+1;        else if(n[mid]>a)            tail=mid-1;        else            return mid;    }    return head;}int lis(int a[],int n)//a[]为输入的序列,n为序列长度{    int vis[N],k=1;    vis[0]=a[0];    for(int i=1;i<n;i++)    {        if(a[i]>vis[k-1])  //增减规则        {            vis[k++]=a[i];        }        else        {            int mid=_2fen(vis,k,a[i]);            vis[mid]=a[i];        }    }    return k; //返回lis长度}int main(){    int w,x[N];                //LIS  最长递增子序列   O(Nlongn)    int n;cin>>n;    while(n--){    cin>>w;    for(int i=0;i<w;i++)    {        cin>>x[i];    }    cout<<lis(x,w)<<endl;}    return 0;}

解法三:最长公共子序列(LCS)

这种方法对于已经学会LCS的来说是一个偷懒的办法,容易想出我们要求的子序列一定是一个有序的序列(因为递增或递减嘛),所以我们用另外一个数组将要求的系列存起来排序好,求出两个序列的LCS长度就是我们要的LIS了(不过这样对于有重复元素的序列来说不做另外处理只能求出最长非递增(减)子序列长度),对于LCS就简单介绍一下:算导上给出定理

假定两个序列为X={x1, x2, …, xm}和Y={y1, y2, …, yn),并设Z={z1, z2, …, zk}为X和Y的任意一个LCS。
1. 如果xm = yn,则zk = xm=yn,且Zk-1是Xm-1和Yn-1的一个LCS。
2. 如果xm != yn, 则zk != xm蕴含Z是Xm-1和Y得一个LCS。
3. 如果xm != yn, 则zk != yn蕴含Z是X和Yn-1的一个LCS。

所以得出状态转移方程为:

dp[i,j]=0dp[i1,j1]+1max(dp[i1,j],dp[i,j1]) if i=0 || j=0 if i,j>0&&Xi = Yj if i,j>0&&Xi != Yj

enter image description here
最后只要输出最后一个元素储存的数值即为LIS的长度,可以看出这个方法非常浪费空间。事实上通过状态转移方程我们可以看出只用到了当前一行和上一行所以可以只存储两行反复使用来达到节省空间的目的。

代码
#define N 1005char s[N],s2[N];int LCS(int l,int l2){    int dp[3][N];    memset(dp,0,sizeof(dp));    for(int i=0; i<l; i++)    {        for(int j=0; j<l2; j++)        {            if(s[i]==s2[j])            {                dp[i%2+1][j]=dp[(i+1)%2+1][j-1]+1;            }            else            {                dp[i%2+1][j]=max(dp[(i+1)%2+1][j],dp[(i)%2+1][j-1]);            }        }    }    return dp[(l-1)%2+1][l2-1];}
0 0