zoukankan      html  css  js  c++  java
  • 【算法剖析】最长公共子序列与最长递增子序列的浅析

    最长公共子序列

      最长公共子序列(Longest Common Sequence,LCS)问题是典型的适用于动态规划求解的问题。LCS的定义是:


     

      给定一个串,以及另外一个串,如果存在一个单调增的序列,对于所有,有,则称是的一个子序列。如果对于两个串既是的子序列,又是的子序列,那么就称的公共子序列,LCS就是指所有子序列中最长的那个子序列(可能有多个)。


     

      使用动态规划求解LCS时,首先我们需要找出递推公式。令,并设为它们的LCS。我们可以看到:

        (1)如果,并且,那么的LCS;

        (2)如果,并且,那么的LCS;

        (3)如果,并且,那么是的LCS;

      上述3个性质不再证明,读者如果有兴趣,可以阅读算法导论的相关内容。根据上述3个性质,我们可以很容易地写出递推公式。设m[i,j]为串的LCS的长度,则

      

      我使用了C++进行了实现,包括了自底向上的构建方法(solve函数)与自顶向下的递归方法(solve1函数)。m数组记录了LCS的长度,b数组则记录了LCS的路径,其中b[i,j]为1,表示(1)的情形,b[i,j]为2表示(2)的情形,b[i,j]为3表示(3)的情形。但是只有当b[i,j]为1时才产生结果输出,这是因为此时才属于LCS。两种求解方法的时间复杂度都为

    最长公共子序列算法
      1 #include <cstdio>
      2 #include <cstring>
      3 
      4 #define MAX_LENGTH 100
      5 int m[MAX_LENGTH][MAX_LENGTH];
      6 int b[MAX_LENGTH][MAX_LENGTH];
      7 
      8 char str1[MAX_LENGTH];
      9 char str2[MAX_LENGTH];
     10 int length1;
     11 int length2;
     12 void print(int a,int c)
     13 {
     14     if(a==0||c==0) return ;
     15     else if(b[a][c]==1)
     16     {
     17         print(a-1,c-1);
     18         printf("%d %d\n",a-1,c-1);
     19     }
     20     else if(b[a][c]==2)
     21         print(a-1,c);
     22     else if(b[a][c]==3)
     23         print(a,c-1);
     24     /*
     25     if(a==0||c==0) return ;
     26     else if(str1[a-1]==str2[c-1])
     27     {
     28         print(a-1,c-1);
     29         printf("%d %d\n",a-1,c-1);
     30     }
     31     else if(m[a][c]==m[a-1][c])
     32         print(a-1,c);
     33     else if(m[a][c]==m[a][c-1])
     34         print(a,c-1);
     35     */
     36 }
     37 void solve()
     38 {
     39     int i,j,k;
     40     for(i=0;i<=length1;i++)
     41     {
     42         for(j=0;j<=length2;j++)
     43         {
     44             if(i==0||j==0) continue;
     45             if(str1[i-1]==str2[j-1])
     46             {
     47                 m[i][j]=m[i-1][j-1]+1;
     48                 b[i][j]=1;
     49             }
     50             else
     51             {
     52                 if(m[i-1][j]>=m[i][j-1])
     53                 {
     54                     m[i][j]=m[i-1][j];
     55                     b[i][j]=2;
     56                 }
     57                 else 
     58                 {
     59                     m[i][j]=m[i][j-1];
     60                     b[i][j]=3;
     61                 }
     62             }
     63         }
     64     }
     65     printf("%d\n",m[length1][length2]);
     66     print(length1,length2);
     67 }
     68 
     69 int solve1(int a,int c)
     70 {
     71 
     72     if(m[a][c]!=-1) return m[a][c];
     73 
     74     if(a==0||c==0)
     75     {
     76         m[a][c]=0;
     77         return m[a][c];
     78     }
     79 
     80     if(str1[a-1]==str2[c-1])
     81     {
     82         m[a][c]=solve1(a-1,c-1)+1;
     83         b[a][c]=1;
     84     }
     85     else
     86     {
     87         if(solve1(a-1,c)>=solve1(a,c-1))
     88         {
     89             m[a][c]=solve1(a-1,c);
     90             b[a][c]=2;
     91         }
     92         else
     93         {
     94             b[a][c]=3;
     95             m[a][c]=solve1(a,c-1);
     96         }
     97     }
     98     return m[a][c];
     99 }
    100 
    101 int main(void)
    102 {
    103     freopen("data.in","r",stdin);
    104     scanf("%s",str1);
    105     scanf("%s",str2);
    106     length1=strlen(str1);
    107     length2=strlen(str2);
    108 //    solve();
    109     int i,j;
    110     for(i=0;i<=length1;i++)
    111     {
    112         for(j=0;j<=length2;j++)
    113             m[i][j]=-1;
    114     }
    115     solve1(length1,length2);
    116     printf("%d\n",m[length1][length2]);
    117     print(length1,length2);
    118     return 0;
    119 }

      

      当然,在实现的过程中,我们如果要输出结果,不要b数组也是可以的,在print函数中,被注释掉的内容,就没有利用b数组,而是直接使用m数组中的结果进行LCS的构建工作。这样在增加了些许时间复杂度的情况下,将空间复杂度降低了一半。

      同时,如果我们只关心LCS的长度,那么空间复杂度可以再次降低,至多要的空间即可。我们观察递推公式,可以看到,m[i,j]的求解最多只与m[i-1,j-1],m[i-1,j]与m[i,j-1]相关联。当我们使用自底向上(solve函数)的方法求解时,我们甚至可以只用个空间的数组b来保存计算结果,用1个空间来保存m[i-1,j-1]。这是由于,当我们计算m[i,j]时,m[i-1,j-1]所在的位置(b[j-1])已经被本行结果(m[i,j-1])所覆盖,而计算所需的m[i,j-1]在b[j-1]的位置上,并且刚刚被计算出来,m[i-1,j]在计算结果写入b[j]之前,存在于b[j]。

      另外的个空间用来存放较短的那个串。基本原理就是这样,我没有写程序实现,有兴趣的读者可以自己写动手写一下。

    最长递增子序列

      我们接下来考虑另外一个相似的问题,对于一组数字序列,以相同的方法定义子序列,如果这个序列中的数字是单调递增的,则称为递增子序列。我们所要求的就是最长的递增子序列。设串为一个数字串,如果使用暴力搜索求最长递增子序列,时间复杂度为,显然不可行。求解此问题,关键在于如何找出递推式。

      我们可以发现一个明显的关系,设c[i]为串并且包含了的最长递增子序列的长度。设一个串为{8,9,10,1,2},那么c[1]=1,c[2]=2,c[3]=3,c[4]=1,c[5]=2。我们可以很方便地得出递推关系:

      通过该递归式,我们可以求出所有的c[i],最后遍历一遍c,从中找出最长的递增子序列即可,或者直接在求c[i]的过程中保存当前求出的最长递增子序列,当结束的时候,当前最长递增子序列就变成了全局最长递增子序列。该算法的时间复杂度为,当我们需要得到最长递增子序列的内容时,需要另外一个数组d来跟踪最长递增子序列。d[i]中记录的是c[i]那个递增子序列的前一个数。使用递归的方法就可以得到递增子序列。

    最长递增子序列算法
    #include <cstdio>
    
    #define MAX_LENGTH 100
    int N;
    int c[MAX_LENGTH],d[MAX_LENGTH],num[MAX_LENGTH];
    
    void print(int pos)
    {
        if(d[pos]!=pos)
        {
            print(d[pos]);
        }
        printf("%d ",pos);
    }
    void solve()
    {
        int i,j,max=0,maxpos=0;
        for(i=0;i<N;i++)
        {
            if(i==0)
            {
                c[i]=1;
                max=1;
                maxpos=0;
                d[i]=i;
            }
            else
            {
                bool flag=false;
                int tempmax=0;
                int tempmaxpos=0;
                for(j=0;j<i;j++)
                {
                    if(num[j]<num[i])
                    {
                        if(c[j]+1>tempmax)
                        {
                            tempmax=c[j]+1;
                            tempmaxpos=j;
                        }
                        flag=true;
                    }
                }
                if(flag==true)
                {
                    c[i]=tempmax;
                    d[i]=tempmaxpos;
                }
                else
                {
                    c[i]=1;
                    d[i]=i;
                }
                if(c[i]>max)
                {
                    max=c[i];
                    maxpos=i;
                }
            }
        }
        printf("max:%d\n",max);
        printf("path:\n");
        print(maxpos);
        printf("\n");
    }
    
    int main(void)
    {
        scanf("%d",&N);
        int i,j;
        for(i=0;i<N;i++)
        {
            scanf("%d",&num[i]);
            //-1标示还没有得到结果
            //c[i]=-1;
            //d[i]=-1;
        }
        solve();
        return 0;
    }

      如果我们只关心最长递增子序列的长度,我们可以以更快的速度求解。此时,我们需要一个最长为m的数组。我先阐述一个较为不严谨的原理:对于序列中的某一个数,我们期望它能够成为最长递增子序列一个时,我们就必须使当前最长递增子序列中的最大的数尽可能的小。单纯地讲理论无法理解这种思想,并且我也没有信心能够讲好。因此,下面我将就一个例子阐述这种思路。


    示例

      串X={5,6,9,2,3,1,4,6,7,8},当前最长递增子序列的长度max_length=0

        Step 1:读取5,将5加入m,此时m数组为空,直接加入即可:

            m:5;max_length=1

        Step 2:读取6,将6加入m,覆盖位置为仅小于6的数之后的那个数:

            m:5,6;max_length=2

        Step 3:读取9,将9加入m,覆盖位置为仅小于9的数之后的那个数:

            m:5,6,9;max_length=3

        Step 4:读取2,将2加入m,覆盖位置为仅小于2的数之后的那个数,注意数组中的数都比2大,那么就将直接覆盖掉5:

            m:2,6,9;max_length=3

        Step 5:读取3,将3加入m,覆盖位置为仅小于3的数之后的那个数:

            m:2,3,9;max_length=3

      注意Step 4和Step 5的步骤,覆盖掉了m的前两项,这样就破坏了最长递增子序列的内容,但其长度仍然保留下来。

        Step 6:读取1,将1加入m,覆盖位置为仅小于1的数之后的那个数:

            m:1,3,9;max_length=3

        Step 7:读取4,将4加入m,覆盖位置为仅小于4的数之后的那个数:

            m:1,3,4;max_length=3

        Step 8:读取6,将6加入m,覆盖位置为仅小于6的数之后的那个数:

            m:1,3,4,6;max_length=4

      此时最长的递增子序列为2,3,4,6。数组中并没有保留递增子串的内容,只是维护了递增子序列的长度。

        Step 9:读取7,将7加入m,覆盖位置为仅小于7的数之后的那个数:

            m:1,3,4,6,7;max_length=5

        Step 10:读取8,将8加入m,覆盖位置为仅小于8的数之后的那个数:

            m:1,3,4,6,7,8;max_length=6


     

      经过以上各个步骤,我们得到了max_length为6。在计算过程中,我们对于序列中的每一个数都进行了处理,将其插入到数组m适当的位置上,这个插入过程的定位使用二分法定位,复杂度为,m个数的复杂度就为

    End:由于写作仓促,可能会存在错误,欢迎交流。


    作者:Chenny Chen
    出处:http://www.cnblogs.com/XjChenny/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

  • 相关阅读:
    【MPI学习2】MPI并行程序设计模式:对等模式 & 主从模式
    【MPI学习1】简单MPI程序示例
    【多线程】零碎记录1
    【APUE】Chapter17 Advanced IPC & sign extension & 结构体内存对齐
    php-7.1编译记录
    rsyslogd系统日志服务总结
    php配置(php7.3)
    php-fpm回顾和总结
    php-fpm配置项
    Hive之执行计划分析(explain)
  • 原文地址:https://www.cnblogs.com/XjChenny/p/2823650.html
Copyright © 2011-2022 走看看