• 动态规划 最长公共子序列 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

  • 相关阅读:
    CRM详细介绍网址
    element上传多图片
    多图片上传WebForm
    MemCached原理
    Redis的原理
    Ninject用法详解
    SQLsugar和unity 的用法
    json中的转义字符的处理
    fiddler
    orm多表关系
  • 原文地址:https://www.cnblogs.com/youxin/p/3269294.html
Copyright © 2020-2023  润新知