【算法剖析】最长公共子序列与最长递增子序列的浅析

最长公共子序列

  最长公共子序列(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/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文地址:https://www.cnblogs.com/XjChenny/p/2823650.html