动态规划 最长公共子序列 LCS,最长单独递增子序列,最长公共子串

LCS:给出两个序列S1和S2,求出的这两个序列的最大公共部分S3就是就是S1和S2的最长公共子序列了。公共部分

须是以相同的顺序出现,但是不必要是连续的。

出最长公共子序列。对于长度为n的序列,其子序列共有2的n次方个,这样的话这种算法的时间复杂度就为指数级

了,这显然不太适合用于序列很长的求解了。

解法二:既然学到了动态规划,就来看看能否用动态规划的思想来解决这个问题。要使用动态规划,必须满足两个条

件:有最优子结构和重叠子问题。为了便于学习,我们先来了解下这两个概念。

如果问题的一个最优解中包含了子问题的最优解,则该问题具有最优子结构。当一个递归算法不断地调用同一问题

时,我们说该最优问题包含重叠子问题。

说明:动态规划要求其子问题既要独立又要重叠,这初看上去貌似存在矛盾,其实不然。只是这里说的是两个不同的

概念。独立指的是同一问题的两个子问题不共享资源。而重叠子问题指的是子问题可以作为不同的问题的子问题出

现。

设X = <x1,x2...xm>, Y = <y1,y2...yn>为两个序列,并设Z = <z1,z2...zk>为X和Y的任意一个LCS。可以得出:

1、如果xm = yn,那么zk = xm = yn而且Z(k-1)是X(m-1)和Y(n-1)的一个LCS;

2、如果xm != yn,那么zk != xm蕴含Z是X(m-1)和Y的一个LCS;

3、如果xm != yn,那么zk != yn蕴含Z是X和Y(n-1)的一个LCS。

注:上面的Z(k-1)表示序列Z<z1,z2...zn>,其中n=k-1。其它的X()和Y()也是一样的。

很容易证明上述三点是成立的,详细证明见算法导论。所以LCS具有最优子结构。从上面也可以看出LCS问题中的重

叠子问题的性质。所以我们可以用动态规划来解决LCS问题。由LCS问题的最优子结构可得出递归式:

代码:

#include<iostream>
using namespace std;

#define M 7  
#define N 6  

int lcs(char *x,char *y,int c[M+1][N+1],int b[M+1][N+1])
{
    int m=M,n=N;
    for(int i=0;i<=m;i++)
        c[i][0]=0;
    for(int j=0;j<=n;j++)
        c[0][j]=0;
    for(int i=1;i<=m;i++)
    {
        for(int j=1;j<=n;j++)
        {
            if(x[i]==y[j])
            {
                c[i][j]=c[i-1][j-1]+1;
                b[i][j]=0;
            }
            else if(c[i-1][j]>=c[i][j-1])
            {
                c[i][j]=c[i-1][j];
                b[i][j]=1;
            }
            else
            {
                c[i][j]=c[i][j-1];
                b[i][j]=-1;
            }
        }
    }
    return c[m][n];
}

void printLcs(int b[][7],char *x,int i,int j)
{
    if(i==0||j==0)
    {
        return;
    }
    if(b[i][j]==0)
    {
        printLcs(b,x,i-1,j-1);
        cout<<x[i];
    }
    else if(b[i][j]==1)
    {
        printLcs(b,x,i-1,j);
    }
    else
    {
        printLcs(b,x,i,j-1);
    }
}

int main()
{
     
    char x[M+1]={'0','A','B','C','B','D','A','B'};
    char  y[N+1]={'0','B','D','C','A','B','A'};   
    int c[M+1][N+1];//注意大小要为strlen+1;因为要存0
    int b[M+1][N+1];
    cout<<lcs(x,y,c,b)<<endl;
    cout<<"X:"<<x<<endl;
    cout<<"y:"<<y<<endl;
     
    for(int i=1;i<=7;i++)
    {
        for(int j=1;j<=6;j++)
        {
            cout<<b[i][j]<<ends;
        }
        cout<<endl;
    }
    cout<<"最长子序列为:"<<endl;
    printLcs(b,x,7,6);
}

注意我们的b[0][i] b[i][0]没有存储任何东西。

输出:4

BCBA.

一个问题:

 按照这种方式:

         char x[M+1]={'0','A','B','C','B','D','A','B'}; //M+1=8

cout<<x输出什么?

答:每次运行都会不同,如X:0ABCBDAB烫烫b'灤?。

为什么,因为用char x[] 这种方式系统不会主动添加字符串结束符。如果我们改为

char x[8]="0ABCBDAB"; 

提示数组越界,为什么?因为用直接用字符串赋值的方式,系统会自动在末尾添加,这样,就有了9个字符所有越界了。

上面的代码写的很烂,原因是赋值的方式太烂了,

  char x[M+1]={'0','A','B','C','B','D','A','B'}; //M+1=8

需要一个一个赋值。这么做是为了是数组中的x[1]为A。前面的0纯属占位符

 下面的代码更好一点:

#include<iostream>
using namespace std;

int c[100][100];
int b[100][100];
  

int lcs(char x[],char y[])
{
    int m=strlen(x);
    int n=strlen(y);

    for(int i=0;i<=m;i++)
        c[i][0]=0;
    for(int j=0;j<=n;j++)
        c[0][j]=0;
    for(int i=1;i<=m;i++)
    {
        for(int j=1;j<=n;j++)
        {
            if(x[i-1]==y[j-1]) //这是关键点。
            {
                c[i][j]=c[i-1][j-1]+1;
                b[i][j]=0;
            }
            else if(c[i-1][j]>=c[i][j-1])
            {
                c[i][j]=c[i-1][j];
                b[i][j]=1;
            }
            else
            {
                c[i][j]=c[i][j-1];
                b[i][j]=-1;
            }
        }
    }
    return c[m][n];
}

void printLcs( char x[],int i,int j)
{
    if(i==0||j==0)
    {
        return;
    }
    if(b[i][j]==0)
    {
        printLcs(x,i-1,j-1);
        cout<<x[i-1];
    }
    else if(b[i][j]==1)
    {
        printLcs(x,i-1,j);
    }
    else
    {
        printLcs(x,i,j-1);
    }
}

int main()
{
     
    char x[]="ABCBDAB";
    char y[]="BDCABA";
  
    cout<<lcs(x,y)<<endl;
    cout<<"X:"<<x<<endl;
    cout<<"y:"<<y<<endl;
     
    for(int i=1;i<=7;i++)
    {
        for(int j=1;j<=6;j++)
        {
            cout<<b[i][j]<<ends;
        }
        cout<<endl;
    }
    cout<<"最长子序列为:"<<endl;
    printLcs(x,7,6);
}

 程序画红线的地方值得注意,因为开始写的时候花了很长时间找错误。

算法导论后面有2个习题:

最长递增子序列:注意没有公共,即只有一个序列。

14.4-5 请给出一个O(n^2)时间的算法,使之能找出n个数的序列中最长的单调递增子序列

答案:

monotonic:单调的.

序列为X=(x1,x2,x3,x4...),首先排序X得到X',找出X和X'的最长公共子序列即可

另一种思维:

 先回顾经典的O(n^2)的动态规划算法,设A[i]表示序列中的第i个数,F[i]表示从1到i这一段中以i结尾的最长上升子序列的长度(也就是说F[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])。

代码:

int main()
{
     
     char *a="abcdaaabb";
     int n=strlen(a);
     int *c=new int[n];
     
       c[0]=1;//初始化,以a[0]结尾的最长递增子序列为1
     
     for(int i=1;i<n;i++)
     {
         c[i]=1;//c[i]的最小值
         for(int j=0;j<i;j++)
         {
             if(a[j]<=a[i] && c[j]+1>c[i])
                c[i]=c[j]+1;
         }

     } 
     int max=0;
     for(int i=0;i<n;i++)
         if(c[i]>max)
             max=c[i];
     cout<<max<<endl;
     
     for(int i=0;i<=n-1;i++)
     {
         cout<<c[i]<<ends;
     }

  
     
}

输出: 6

1 2 3 4 2 3 4 5 6         (abcdaaabb)

仔细看c[i]的结果,c[i]表示已a[i]结尾的最大长度

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

   (1)x < y < 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] = 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[]在算法结束后记录的并不是一个符合题意的最长上升子序列!

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

(个人觉得上面说的不好理解,下面是好理解一点的:

O(nlgn)的算法。
对于序列Sn,考虑其长度为i的单调子列(1<=i<=m),这样的子列可能有多个。我们选取这些子列的结尾元素(子列的最后一个元素)的最小值。用Li表示。易知L1<=L2<=…<=Lm {如果Li>Lj(i<j);么去掉以Lj结尾的递增子序列的最后j-i个元素,得到一个长度为i的子序列,该列的结素ak<=Lj<Li;这与Li标识了长度为i的递增子序列的最小结尾元素相矛盾,于是证明了上述结论。}


    • 现在,我们来寻找Sn对应的L序列,如果我们找到的最大的Li是Lm,那么m就是最大单调子列的长度。下面的方法可以用来维护L。
      从左至右扫描Sn,对于每一个ai,它可能
      (1) ai<L1,那么L1=ai
      (2) ai>=Lm,那么Lm+1=ai,m=m+1 (其中m是当前见到的最大的L下标)
      (3) Ls<=ai<Ls+1(1<=s<=m),那么Ls+1=ai;
      扫描完成后,我们也就得到了最长递增子序列的长度。从上述方法可知,对于每一个元素,我们需要对L进行查找(类似折半查找)操作,由于L有序,所以这个操作为lgn,于是总的复杂度为O(nlogn)。)
int find2(char L[],int len,char key)//若返回值为x,则a[x]>=key
{
     int low,high,mid;
     low=0,high=len;
     while(low<=high)
     {
         mid=(low+high)/2;
         if(L[mid]==key)
             return mid;
         else if(L[mid]<key)
             low=mid+1;
         else 
             high=mid-1;
     }
     return low;
}
int main()
{
     
    char *a="abcdaaabb";
     
     int n=strlen(a);
       char *L=new char[n];
     int len=0;//新开一变量len,用来储存每次循环结束后c中已经求出值的元素的最大下标
       L[0]=-1;//很巧妙,这里把L[0]设置-1
     L[1]=a[0];
     len=1;///此时只有L[0]求出来,最长递增子序列的长度为1
     int j;
     for(int i=1;i<n;i++)
     {
         
         j=find2(L,len,a[i]);
         L[j]=a[i];
         if(j>len)
             len=j;
     }
     cout<<len<<endl;

}

char *a="abcdaaabb";

输出:4 

如果我们求序列,输出L[i]:

  for(int i=1;i<=len;i++) { cout<<L[i]<<ends;  }

结果是:abcd

abcdaaabbcdefgh

输出8.

L:abcdefgh

若要递增,非严格,不能采用此种方法。

举例:如aab 采用上面的,错误。

更多:

http://my.oschina.net/leejun2005/blog/117167

原文地址:https://www.cnblogs.com/youxin/p/3269294.html