• LIS和LCS LCIS


    首先介绍一下LIS和LCS的DP解法O(N^2)

    LCS:两个有序序列a和b,求他们公共子序列的最大长度

    我们定义一个数组DP[i][j],表示的是a的前i项和b的前j项的最大公共子序列的长度,那么由于是用迭代法,所以计算DP[i][j]前,DP[i-1][j]和DP[i][j-1]就都已经计算出来了,不难理解就可以得出状态转移方程:

    DP[i][j]  = DP[i-1][j-1] + 1;   如果a[i] == b[j]

                    MAX(DP[i-1][j], DP[i][j-1])  如果a[i] != b[j]

    LIS:一个a序列,求它的最长上升子序列的最大长度

    另外,由于每次都是扫描b数组,那我们就只需要一个DP[2][B]的数组就可以了,DP[0][]依赖于DP[1][],DP[1][]依赖于DP[0][],只要实现动态处理就可以了。

    这个状态转移方程也不难给出

    DP[j] = MAX(DP[i]) + 1 满足条件a[j] > a[i]

    对每一个a[j],枚举一遍a[0]....a[j-1]就可以了

    下面再来看看他们的O(nlogn)的解法

    首先是LCS,我们把a序列中的每个元素在b中出现的位置保存起来,再按照降序排列,排列后再代入a的每个对应元素,那就转化为了求这个新的序列的最长上升子序列了。如:a[] = {a, b, c,} b[] = {a,b,c,b,a,d},那么a中的a,b,c在b中出现的位置分别就是

    {0,4},{1,3},{2}分别按降序排列后代入a序列就是{4,0,2,3,1},之所以要按照降序排列,目的就是为了让每个元素只取到一次。

    接下来的问题就是要求最长升序子序列问题了,也就是求LIS。

    PS:个人认为这种转化并没有多大意义,因为当数据比较大时,举一个例子来说:a[]= “aaaaaaaaaaa”,b[] = "aaaaaaaaaaa",他们都有n个a,那么a[]中‘a’在b[]出现了n次那么替换之后的a就变为了n-1...1,n-1...1,... ...这样a[]就有了n^2个数了这样不仅是空间,就连时间也比原来n^2要大。所以感觉这种转化还是要慎用。

    下面的LIS的O(nlogn)转自于http://hi.baidu.com/fandywang_jlu/item/da673a3d83e2a65980f1a7e1

    一、算法思想

             算法还是容易想到的,两重循环DP即可。不过如果数据规模最大可以达到几十万甚至更大,经典的O(n^2)的动态规划算法明显会超时。我们需要寻找更好的方法来解决是最长上升子序列问题。以下以最长递增子序列为例进行说明:

       先回顾经典的O(n^2)的动态规划算法,设A[i]表示序列中的第i个数,F[i]表示从1到i这一段中以i结尾的最长上升子序列的长度,初始时设 F[i] = 0(i = 1, 2, ..., len(A))。则有动态规划方程:F[i] = max{1, F[j] + 1} (j = 1, 2, ..., i - 1, 且A[j] < A[i])。

      现在,我们仔细考虑计算F[i]时的情况。假设有两个元素A[x]和A[y],满足(1)y < x < i (2)A[x] < A[y] < A[i] (3)F[x] = F[y]

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

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

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

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

      (1) D[k]的值是在整个计算过程中是单调不上升的。//此处需要特别注意!!!关键之所在!

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

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

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

    这个算法还可以扩展到整个最长子序列系列问题,整个算法的难点在于二分查找的设计,需要非常小心注意.

    二、实现

    // By Fandywang 2008.7.21

    // Call: LIS(a, n); 求最大递增/上升子序列(如果为最大非降子序列,只需把上面的注释部分给与替换)

    const int N = 1001;

    int a[N], f[N], d[N]; // d[i]用于记录a[0...i]的最大长度

    int bsearch(const int *f, int size, const int &a)

    {

        int l=0, r=size-1;

        while( l <= r )

        {

            int mid = (l+r)/2;

            if( a > f[mid-1] && a <= f[mid] ) return mid; // >&&<= 换为: >= && <

            else if( a < f[mid] ) r = mid-1;

            else l = mid+1;

        }

    }

    int LIS(const int *a, const int &n){

         int i, j, size = 1;

         f[0] = a[0]; d[0] = 1;

         for( i=1; i < n; ++i ){

              if( a[i] <= f[0] ) j = 0;                 // <= 换为: <

             else if( a[i] > f[size-1] ) j = size++;   // > 换为: >=

             else j = bsearch(f, size, a[i]);

             f[j] = a[i]; d[i] = j+1;

         }

         return size;

    }

    三、学以致用

    JOJ 1829 Candies   最大非下降子序列

    JOJ 2162 Inuyasha And the Monsters 求和最大的非下降子序列 (这个我用的是O(n^2)的算法做的,居然还那个了第一...)

    JOJ 2529 Chorus 最大上升子序列+最大下降子序列

    JOJ 1048 Wooden Sticks 先自定义排下序(先按l从小到大, l相等再按w从小到大), 然后求w的最大下降子序列

    POJ 3636 Nested Dolls 与JOJ1048类似 w从小到大,w相等h从大到小,然后求h最大非升子序列

          至于LCIS,有了上面的比较深刻的理解,相信应该会比较好理解(转自百度文库)

    最长公共上升子序列(LCIS)的O(n^2)算法

    预备知识:动态规划的基本思想,LCS,LIS。

    问题:字符串a,字符串b,求a和b的LCIS(最长公共上升子序列)。

    首先我们可以看到,这个问题具有相当多的重叠子问题。于是我们想到用DP搞。DP的首要任务是什么?定义状态。

    1定义状态F[i][j]表示以a串的前i个字符b串的前j个字符且以b[j]为结尾构成的LCIS的长度。

    为什么是这个而不是其他的状态定义?最重要的原因是我只会这个,还有一个原因是我知道这个定义能搞到平方的算法。而我这只会这个的原因是,这个状态定义实在是太好用了。这一点我后面再说。

    我们来考察一下这个这个状态。思考这个状态能转移到哪些状态似乎有些棘手,如果把思路逆转一下,考察这个状态的最优值依赖于哪些状态,就容易许多了。这个状态依赖于哪些状态呢?

    首先,在a[i]!=b[j]的时候有F[i][j]=F[i-1][j]。为什么呢?因为F[i][j]是以b[j]为结尾的LCIS,如果F[i][j]>0那么就说明a[1]..a[i]中必然有一个字符a[k]等于b[j](如果F[i][j]等于0呢?那赋值与否都没有什么影响了)。因为a[k]!=a[i],那么a[i]对F[i][j]没有贡献,于是我们不考虑它照样能得出F[i][j]的最优值。所以在a[i]!=b[j]的情况下必然有F[i][j]=F[i-1][j]。这一点参考LCS的处理方法。

    那如果a[i]==b[j]呢?首先,这个等于起码保证了长度为1的LCIS。然后我们还需要去找一个最长的且能让b[j]接在其末尾的LCIS。之前最长的LCIS在哪呢?首先我们要去找的F数组的第一维必然是i-1。因为i已经拿去和b[j]配对去了,不能用了。并且也不能是i-2,因为i-1必然比i-2更优。第二维呢?那就需要枚举b[1]..b[j-1]了,因为你不知道这里面哪个最长且哪个小于b[j]。这里还有一个问题,可不可能不配对呢?也就是在a[i]==b[j]的情况下,需不需要考虑F[i][j]=F[i-1][j]的决策呢?答案是不需要。因为如果b[j]不和a[i]配对,那就是和之前的a[1]..a[j-1]配对(假设F[i-1][j]>0,等于0不考虑),这样必然没有和a[i]配对优越。(为什么必然呢?因为b[j]和a[i]配对之后的转移是max(F[i-1][k])+1,而和之前的i`配对则是max(F[i`-1][k])+1。显然有F[i][j]>F[i`][j],i`>i)

    于是我们得出了状态转移方程:

    a[i]!=b[j]:   F[i][j]=F[i-1][j]

    a[i]==b[j]:   F[i][j]=max(F[i-1][k])+1 1<=k<=j-1&&b[j]>b[k]

    不难看到,这是一个时间复杂度为O(n^3)的DP,离平方还有一段距离。

    但是,这个算法最关键的是,如果按照一个合理的递推顺序,max(F[i-1][k])的值我们可以在之前访问F[i][k]的时候通过维护更新一个max变量得到。怎么得到呢?首先递推的顺序必须是状态的第一维在外层循环,第二维在内层循环。也就是算好了F[1][len(b)]再去算F[2][1]。

    如果按照这个递推顺序我们可以在每次外层循环的开始加上令一个max变量为0,然后开始内层循环。当a[i]>b[j]的时候令max=F[i-1][j]。如果循环到了a[i]==b[j]的时候,则令F[i][j]=max+1。

    最后答案是F[len(a)][1]..F[len(a)][len(b)]的最大值。

    参考代码:

    复制代码
     1 #include<cstdio>
     2 #include<cstring>
     3 int f[1005][1005],a[1005],b[1005],i,j,t,n1,n2,max;
     4 int main()
     5 {
     6     scanf("%d",&t);
     7     while(t--)
     8     {
     9         scanf("%d%d",&n1,&n2);
    10         for(i=1;i<=n1;i++) scanf("%d",&a[i]);
    11         for(i=1;i<=n2;i++) scanf("%d",&b[i]);
    12         memset(f,0,sizeof(f));
    13         for(i=1;i<=n1;i++)
    14         {
    15             max=0;
    16             for(j=1;j<=n2;j++)
    17             {
    18                 f[i][j]=f[i-1][j];
    19                 if (a[i]>b[j]&&max<f[i-1][j]) max=f[i-1][j];
    20                 if (a[i]==b[j]) f[i][j]=max+1;
    21             }
    22         }
    23         max=0;
    24         for(i=1;i<=n2;i++) if (max<f[n1][i]) max=f[n1][i];
    25         printf("%d
    ",max);
    26     }
    27 }
    复制代码

    其实还有一个很风骚的一维的算法。在此基础上压掉了一维空间(时间还是平方)。i循环到x的时候,F[i]表示原来F[x][j]。之所以可以这样,是因为如果a[i]!=b[j],因为F[x][j]=F[x-1][j]值不变,F[x]不用改变,沿用过去的就好了,和这个比较维护更新得到的max值依然是我们要的。而a[i]==b[j]的时候,就改变F[x]的值好了。具体结合代码理解。

    参考代码:

    复制代码
     1 #include<cstdio>
     2 #include<cstring>
     3 int f[1005],a[1005],b[1005],i,j,t,n1,n2,max;
     4 int main()
     5 {
     6     scanf("%d",&t);
     7     while(t--)
     8     {
     9         scanf("%d%d",&n1,&n2);
    10         for(i=1;i<=n1;i++) scanf("%d",&a[i]);
    11         for(i=1;i<=n2;i++) scanf("%d",&b[i]);
    12         memset(f,0,sizeof(f));
    13         for(i=1;i<=n1;i++)
    14         {
    15             max=0;
    16             for(j=1;j<=n2;j++)
    17             {
    18                 if (a[i]>b[j]&&max<f[j]) max=f[j];
    19                 if (a[i]==b[j]) f[j]=max+1;
    20             }
    21         }
    22         max=0;
    23         for(i=1;i<=n2;i++) if (max<f[i]) max=f[i];
    24         printf("%d
    ",max);
    25     }
    26 }
    复制代码

    最长公共上升子序列(LCIS)的平方算法

  • 相关阅读:
    oracle 使用正则表达式获取字符串中包含的数字
    oracle 将逗号分隔的字符串转成多行记录
    sqlplus下 查看oracle 执行计划
    使用connect by 虚拟生成100条记录
    Lomsat gelral
    Legacy
    慢慢变小的序列(好题)
    Walls(扫描线)
    线段树求面积并,面积交,周长
    最小圆覆盖(随机增量||模拟退火)
  • 原文地址:https://www.cnblogs.com/FuTaimeng/p/5446307.html
Copyright © 2020-2023  润新知