dp之最长递增、公共子序列总结

1、最长递增子序列模板poj2533(时间复杂度O(n*n))

#include<iostream>

#include<stdio.h>

#include<string.h>

using namespace std;

int dp[1005],a[1005];

int main()

{

    int n;

    while(scanf("%d",&n)>0)

    {

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

        scanf("%d",&a[i]);

        for(int i=0;i<=n;i++)

        dp[i]=1;

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

        {

            for(int j=1;j<i;j++)

            if(a[i]>a[j]&&dp[i]<dp[j]+1)

            dp[i]=dp[j]+1;

        }

        int maxx=0;

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

        if(dp[i]>maxx)

        maxx=dp[i];

        printf("%d\n",maxx);

    }

    return 0;

}

 2、最长递增子序列模板poj3903(时间复杂度O(nlogn))

最长递增子序列,Longest Increasing Subsequence 下面我们简记为 LIS。

排序+LCS算法 以及 DP算法就忽略了,这两个太容易理解了。



假设存在一个序列d[1..9] = 2 1 5 3 6 4 8 9 7,可以看出来它的LIS长度为5。

下面一步一步试着找出它。

我们定义一个序列B,然后令 i = 1 to 9 逐个考察这个序列。

此外,我们用一个变量Len来记录现在最长算到多少了



首先,把d[1]有序地放到B里,令B[1] = 2,就是说当只有1一个数字2的时候,长度为1的LIS的最小末尾是2。这时Len=1



然后,把d[2]有序地放到B里,令B[1] = 1,就是说长度为1的LIS的最小末尾是1,d[1]=2已经没用了,很容易理解吧。这时Len=1



接着,d[3] = 5,d[3]>B[1],所以令B[1+1]=B[2]=d[3]=5,就是说长度为2的LIS的最小末尾是5,很容易理解吧。这时候B[1..2] = 1, 5,Len=2



再来,d[4] = 3,它正好加在1,5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是可以把5淘汰掉,这时候B[1..2] = 1, 3,Len = 2



继续,d[5] = 6,它在3后面,因为B[2] = 3, 而6在3后面,于是很容易可以推知B[3] = 6, 这时B[1..3] = 1, 3, 6,还是很容易理解吧? Len = 3 了噢。



第6个, d[6] = 4,你看它在3和6之间,于是我们就可以把6替换掉,得到B[3] = 4。B[1..3] = 1, 3, 4, Len继续等于3



第7个, d[7] = 8,它很大,比4大,嗯。于是B[4] = 8。Len变成4了



第8个, d[8] = 9,得到B[5] = 9,嗯。Len继续增大,到5了。



最后一个, d[9] = 7,它在B[3] = 4和B[4] = 8之间,所以我们知道,最新的B[4] =7,B[1..5] = 1, 3, 4, 7, 9,Len = 5。



于是我们知道了LIS的长度为5。



!!!!! 注意。这个1,3,4,7,9不是LIS,它只是存储的对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个d[9] = 7更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把8更新到d[5], 9更新到d[6],得出LIS的长度为6。



然后应该发现一件事情了:在B中插入数据是有序的,而且是进行替换而不需要挪动——也就是说,我们可以使用二分查找,将每一个数字的插入时间优化到O(logN)~~~~~于是算法的时间复杂度就降低到了O(NlogN)~!

#include<iostream>

#include<stdio.h>

#include<stdio.h>

using namespace std;

int dp[100005],s[100005];

int main()

{

    int n;

    while(scanf("%d",&n)>0)

    {

        for(int i=0;i<n;i++)

        scanf("%d",&s[i]);

        dp[0]=s[0];

        int len=1;

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

        {

            int left=0,right=len-1,mid;

            if(dp[len-1]<s[i])

            dp[len++]=s[i];

            else

            {

                right=len-1;

                while(left<=right)

                {

                    mid=(left+right)/2;

                    if(dp[mid]<s[i])

                    left=mid+1;

                    else  right=mid-1;

                }

                dp[left]=s[i];

            }

        }

        printf("%d\n",len);

    }

    return 0;

} 

 3、最长公共子序列poj1458

#include<iostream>

#include<stdio.h>

#include<string.h>

using namespace std;

int dp[1000][1000];

char s[1000],t[1000];

int main()

{

    while(scanf("%s%s",s,t)>0)

    {

         int lens=strlen(s),lent=strlen(t);

         for(int i=0;i<1000;i++)

         dp[0][i]=dp[i][0]=0;

         for(int i=1;i<=lens;i++)

         {

                 for(int j=1;j<=lent;j++)

                 if(s[i-1]==t[j-1])

                 dp[i][j]=dp[i-1][j-1]+1;

                 else

                 {

                     int maxx=0;

                     if(maxx<dp[i-1][j])

                     maxx=dp[i-1][j];

                     if(maxx<dp[i][j-1])

                     maxx=dp[i][j-1];

                     dp[i][j]=maxx;

                 }

         }

         printf("%d\n",dp[lens][lent]);

    }

    return 0;

}

 4、最长公共递增子序列以及其路径记录问题

给两组数字个数分别为n,m的序列,问这两组序列的最长递增公共子序列是多长,并且输出来

数据:

Sample Input

5

1 4 2 5 -12

4

-12 1 2 4

Sample Output

2

1 4

 

#include<iostream>

#include<stdio.h>

#include<string.h>

using namespace std;

int dp[1005][1005],a[1005],b[1005],path[1005][1005];

int f[1005];

int main()

{

    int lena,lenb;

    scanf("%d",&lena);

    {

        for(int i=0;i<lena;i++)

        scanf("%d",&a[i]);

        scanf("%d",&lenb);

        for(int i=0;i<lenb;i++)

        scanf("%d",&b[i]);

        for(int i=0;i<=1004;i++)

        dp[i][0]=dp[0][i]=0;

        int tmp,ans=0;

        int qd,zd,k=0,maxx;

        for(int i=1;i<=lena;i++)

        {

            maxx=0;

            for(int j=1;j<=lenb;j++)

            {

                dp[i][j]=dp[i-1][j];

                if(a[i-1]>b[j-1]&&dp[i][j]>maxx)

                {

                    maxx=dp[i][j];

                    k=j;

                }

                if(a[i-1]==b[j-1])

                {

                    dp[i][j]=maxx+1;

                    path[i][j]=k;

                }

                //printf("%d\n",dp[i][j]);

                if(ans<dp[i][j])

                {

                    ans=dp[i][j];

                    qd=i;

                    zd=j;

                }

            }

        }

        printf("%d\n",ans);

        int i=qd,j=zd;

        int sum=ans;

        if(ans>0)

        f[ans--]=j-1;

        while(ans&&i&&j)

        {

            if(path[i][j]>0)

            {

                f[ans--]=path[i][j]-1;

                j=path[i][j];

            }

            i--;

        }

        for(int i=1;i<sum;i++)

        printf("%d ",b[f[i]]);

        printf("%d\n",b[f[sum]]);

    }

    return 0;

}

 题目:

1、poj2250(单词当字母,记忆路径)

题意:给出两段单词,求这两段单词的最长公共单词数,并依次输出它们........

思路:总的来说,是赤裸裸的最长公共子序列,但是需要把单词当作字母来进行.......还需要记录下路径

#include<iostream>

#include<stdio.h>

#include<string.h>

using namespace std;

int dp[1000][1000];

char path[200][200];

char s[200][50],t[200][50],sum[200][50];

int main()

{

    while(scanf("%s",s[0])>0)

    {

         int lens=1;

         while(1)

         {

              scanf("%s",s[lens++]);

              if(s[lens-1][0]=='#')

              break;

         }

         lens--;

         int lent=0;

         while(1)

         {

              scanf("%s",t[lent++]);

              if(t[lent-1][0]=='#')

              break;

         }

         lent--;

         for(int i=0;i<1000;i++)

         dp[i][0]=dp[0][i]=0;

         memset(path,0,sizeof(path));

         for(int i=1;i<=lens;i++)

         {

              for(int j=1;j<=lent;j++)

              if(strcmp(s[i-1],t[j-1])==0)

              {

                   dp[i][j]=dp[i-1][j-1]+1;

                   path[i][j]='1';

              }

              else if(dp[i-1][j]>dp[i][j-1])

              {

                   dp[i][j]=dp[i-1][j];

                   path[i][j]='2';

              }

              else

              {

                  dp[i][j]=dp[i][j-1];

                  path[i][j]='3';

              }

         }

         int i=lens,j=lent,cnt=0;

         //printf("111\n");

         while(path[i][j]!=0)

         {

              int p=i,q=j;

              if(path[p][q]=='1')   

              p--,q--;

              else  if(path[p][q]=='2')

              p--;

              else  if(path[p][q]=='3')

              q--;

              if(dp[i][j]!=dp[p][q])   

              {

                   strcpy(sum[cnt++],s[i-1]);

                   //printf("%s\n",s[i]);

              }

              i=p;

              j=q;

         }

         for(int f=cnt-1;f>0;f--)

         printf("%s ",sum[f]);

         printf("%s\n",sum[0]);

    }

    return 0;

}

 2、poj1159(回文串的问题)

题意:给你一串字符,问加最少的字符可以使它编程回文串.......

思路:就是求出它正反的最长公共子串,然后用len减去这个长度,就是最少要加的字符数......

3、hdu4512(最长递增公共子序列问题)

有一天,有n个人按顺序站在他的面前,他们的身高分别是h[1], h[2] ... h[n],吉哥希望从中挑出一些人,让这些人形成一个新的队形,新的队形若满足以下三点要求,则称之为完美队形: 
  1、挑出的人保持他们在原队形的相对顺序不变;
  2、左右对称,假设有m个人形成新的队形,则第1个人和第m个人身高相同,第2个人和第m-1个人身高相同,依此类推,当然,如果m是奇数,中间那个人可以任意;
  3、从左到中间那个人,身高需保证递增,如果用H表示新队形的高度,则H[1] < H[2] < H[3] .... < H[mid]。
  现在吉哥想知道:最多能选出多少人组成完美队形?

思路:也是把字符串倒过来正反取最长递增公共子序列,但是需要注意的是,i<j,还有,在i~~j的过程中,也许会有一个很大的数,那么也是可以加进去的,具体看代码

#include<iostream>

#include<stdio.h>

#include<string.h>

using namespace std;

int dp[205][205],a[205];

int main()

{

    int text;

    scanf("%d",&text);

    while(text--)

    {

        int n;

        scanf("%d",&n);

        for(int i=0;i<n;i++)

        scanf("%d",&a[i]);

        memset(dp,0,sizeof(dp));

        int sum=0;

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

        {

            int maxx=0;

            for(int j=n;j>i;j--)

            {

                dp[i][j]=dp[i-1][j];

                if(a[i-1]>a[j-1]&&dp[i][j]>maxx)

                maxx=dp[i][j];

                if(a[i-1]==a[j-1])

                dp[i][j]=maxx+1;

                if(dp[i][j]*2>sum)

                sum=dp[i][j]*2;

                for(int k=i+1;k<j;k++)

                {

                    if(a[k-1]>a[j-1]&&dp[i][j]*2+1>sum)

                    sum=dp[i][j]*2+1;

                }

            }    

        }

        printf("%d\n",sum);

    }

    return 0;

}

 4、hdu4512(必须距离k以上的最长递增子序列)

提起小明序列,他给出的定义是这样的:
  ①首先定义S为一个有序序列,S={ A1 , A2 , A3 , ... , An },n为元素个数 ;
  ②然后定义Sub为S中取出的一个子序列,Sub={ Ai1 , Ai2 , Ai3 , ... , Aim },m为元素个数 ;
  ③其中Sub满足 Ai1 < Ai2 < Ai3 < ... < Aij-1 < Aij < Aij+1 < ... < Aim ;
  ④同时Sub满足对于任意相连的两个Aij-1与Aij都有 ij - ij-1 > d (1 < j <= m, d为给定的整数);
  ⑤显然满足这样的Sub子序列会有许许多多,而在取出的这些子序列Sub中,元素个数最多的称为“小明序列”(即m最大的一个Sub子序列)。
  例如:序列S={2,1,3,4} ,其中d=1;
  可得“小明序列”的m=2。即Sub={2,3}或者{2,4}或者{1,4}都是“小明序列”。
  当小明发明了“小明序列”那一刻,情绪非常激动,以至于头脑凌乱,于是他想请你来帮他算算在给定的S序列以及整数d的情况下,“小明序列”中的元素需要多少个呢?

思路:这是一个必须距离k以上的最长递增子序列,总的来说,就是开了一个记录路径的数组,说起来说不清楚,但是很神奇的样子,看代码就可以明白的......

#include<iostream>

#include<stdio.h>

#include<stdio.h>

using namespace std;

int dp[100005],s[100005],c[100005];

int main()

{

    int n,k;

    while(scanf("%d %d",&n,&k)>0)

    {

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

        {

            scanf("%d",&s[i]);

            c[i]=10000000;

        }

        int len=0;

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

        {

            int left=1,right=n,mid;

            while(left<=right)

            {

                mid=(left+right)/2;

                if(s[i]>c[mid])

                left=mid+1;

                else  right=mid-1;

            }

            dp[i]=left;

            //printf("dp==%d\n",left);

            if(dp[i]>len)

            len=dp[i];

            int j=i-k;

            if(j>0&&c[dp[j]]>s[j])

            c[dp[j]]=s[j];

            //printf("c==%d\n",c[dp[j]]);

        }

        printf("%d\n",len);

    }

    return 0;

} 

 5、hdu4681(需要正反预处理的最长公共子序列)

题意:给你A,B,C三个字符串,其中A和B都包含C,现在要找这样一个D串,D串要包含C串,并且是A和B串的最长子串.......

超时思路:在A与B串中枚举C串的位置,然后截取掉,然后就是A串的开头与B串的开头的最长公共子序列,再是A串的结尾与B串的结尾的最长公共子序列,再加上C串的长度.....

ac思路:做预处理,先对A和B正序做一次最长公共子序列,再对A和B反序做一次最长公共子序列,然后分别在A串和B串中枚举C串出现的各个起始位置,记录下来,在进行比较,选择最长的......当然,要注意,反序的dp中,字符出现的位置要处理好......

#include<iostream>

#include<stdio.h>

#include<string.h>

#include<algorithm>

using namespace std;

char s[1005],t[1005],f[1005],s1[1005],t1[1005];

int dp1[1005][1005],dp2[1005][1005];

int cnts=0;

int cntt=0;

int lens,lent,lenf;

int deal(int beg[],int end[],char str[])

{

    int len=strlen(str);

    int flag=0;

    for(int i=0;i<len;i++)

    {

        int p1=0,p2=i;

        if(str[p2]==f[p1])

        {

            while(p1<lenf&&p2<len)

            {

                if(str[p2]==f[p1])

                {

                    p1++;

                    p2++;

                }

                else

                p2++;

            }

            if(p1==lenf)

            {

                beg[flag]=i;

                end[flag++]=p2-1;

            }

        }

    }

    return flag;

}

int main()

{

    int text,kp=0;

    scanf("%d",&text);

    while(text--)

    {

        scanf("%s",s);

        scanf("%s",t);

        scanf("%s",f);

        memset(dp1,0,sizeof(dp1));

        memset(dp2,0,sizeof(dp1));

        lens=strlen(s);

        lent=strlen(t);

        lenf=strlen(f);

        for(int i=1;i<=lens;i++)

        {

            for(int j=1;j<=lent;j++)

            if(s[i-1]==t[j-1])

            dp1[i][j]=dp1[i-1][j-1]+1;

            else

            {

                int maxx=0;

                if(maxx<dp1[i-1][j])

                maxx=dp1[i-1][j];

                if(maxx<dp1[i][j-1])

                maxx=dp1[i][j-1];

                dp1[i][j]=maxx;

            }

         }

         int cnt=0;

         for(int i=lens-1;i>=0;i--)

         s1[cnt++]=s[i];

         cnt=0;

         for(int i=lent-1;i>=0;i--)

         t1[cnt++]=t[i];

         for(int i=1;i<=lens;i++)

         {

            for(int j=1;j<=lent;j++)

            if(s1[i-1]==t1[j-1])

            dp2[i][j]=dp2[i-1][j-1]+1;

            else

            {

                int maxx=0;

                if(maxx<dp2[i-1][j])

                maxx=dp2[i-1][j];

                if(maxx<dp2[i][j-1])

                maxx=dp2[i][j-1];

                dp2[i][j]=maxx;

            }

         }

         int beg1[1005],end1[1005];

         int beg2[1005],end2[1005];

         cnts=deal(beg1,end1,s);

         cntt=deal(beg2,end2,t);

         int maxn=0;

         //for(int i=0;i<cnts;i++)

         //printf("%d %d\n",beg1[i],end1[i]);

         for(int i=0;i<cnts;i++)

         for(int j=0;j<cntt;j++)

         if(maxn<dp1[beg1[i]+1][beg2[j]+1]+dp2[lens-end1[i]-1][lent-end2[j]-1]+lenf)

         maxn=dp1[beg1[i]+1][beg2[j]+1]+dp2[lens-end1[i]-1][lent-end2[j]-1]+lenf;

         printf("Case #%d: %d\n",++kp,maxn-1);

    }

    return 0;

}

 6、uva10635

题意:给你两个数组,A和B,每个数组里面的元素是唯一的,求这两个数组的最长公共子序列........

思路:这个题目数据量很大,求最长公共子序列要o(n*n)会超时,其实可以转换为求最长递增子序列,然后可以在O(nlogn)的时间复杂度求解........

就是,给A中的元素一次编号,在输入B中元素的时候,先判断A中是否有这个元素,没有的话,直接过滤,有的话,把这个元素在A中的编号记录在B数组,然后对B求一次最长递增子序列.........

#include<iostream>

#include<stdio.h>

#include<string.h>

using namespace std;

int a[100000],b[100000],dp[100000];

int main()

{

    int text,f=0;

    scanf("%d",&text);

    while(text--)

    {

        int n,p,q;

        scanf("%d%d%d",&n,&p,&q);

        memset(a,0,sizeof(a));

        for(int i=1;i<=p+1;i++)

        {

            int tmp;

            scanf("%d",&tmp);

            a[tmp]=i;

        }

        int cnt=0;

        for(int i=1;i<=q+1;i++)

        {

            int tmp;

            scanf("%d",&tmp);

            if(a[tmp]==0)  continue;

            b[cnt++]=a[tmp];

        }

        int len=1;

        //for(int i=0;i<cnt;i++)

        //printf("%d\n",b[i]);

        dp[0]=b[0];

        for(int i=1;i<cnt;i++)

        {

            int left=0,right=len-1,mid;

            if(dp[len-1]<b[i])

            dp[len++]=b[i];

            else

            {

                right=len-1;

                while(left<=right)

                {

                    mid=(left+right)/2;

                    if(dp[mid]<b[i])

                    left=mid+1;

                    else  right=mid-1;

                }

                dp[left]=b[i];

            }

        }

        printf("Case %d: %d\n",++f,len);

    }

    return 0;

}

 7、poj1952(求子序列个数)

题意:求最长递减(严格递减)子序列长度,并输出不相同的最长递减子序列的个数

思路:

题解:两次DP。首先求出最长的递减子序列的长度,状态转移方程为dp[i]=max(dp[j])+1;(0<=j<i);(下标从0开始)其中dp[i]表示以第i个数结尾的最长递减子序列的长度。这里在最后加一个dp[n]=-1,具体为什么在下一步讲述。

         然后求每一种长度的递减子序列共有几个。状态转移方程:count[i]=sum(coun[j]);(其中a[i]<a[j]&&dp[j]+1==dp[i]);count[i]是以a[i]结尾的以dp[i]长度的递减子序列的个数。这里要注意排除子序列相同的情况!这里给出一个例子加以说明:

                                i  0  1  2  3  4  5   6

                            a[i]  5  8  4  4  3  2  -1

                          dp[i]  1  1  2  2  3  4   5

                     count[i]   1  1  2  2  2  2   2

       这里在统计count[4]时,当加上count[3]后要忽略count[2]。

      还有在数组最后加上a[n]=-1是保证count[n]一定是最终的答案,而不需要递推完count[]后再一一查找出最大的count[]中的最大值。

上面如果理解了dp[]和count[]的数组的意义之后此题的解法就很明了了。

代码:

#include<iostream>

#include<stdio.h>

#include<string.h>

using namespace std;

int dp[5005],cont[5005],a[5005];

int main()

{

    int n;

    //scanf("%d",&text);

    while(scanf("%d",&n)>0)

    {

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

        scanf("%d",&a[i]);

        a[n+1]=-100000;

        for(int i=0;i<=n+1;i++)

        {

            dp[i]=1;

            //cont[i]=1;

        }

        int maxn1=0;

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

        {

            for(int j=1;j<i;j++)

            if(a[i]<a[j]&&dp[i]<dp[j]+1)

            dp[i]=dp[j]+1;

            if(dp[i]>maxn1)

            maxn1=dp[i];

        }

        int flag=0;

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

        {

            if(dp[i]==1)

            {

               cont[i]=1;

               continue;

            }

            else

            cont[i]=0;

            for(int j=i-1;j>=1;j--)

            {

                if(a[i]<a[j]&&dp[i]==dp[j]+1)

                {

                    flag=1;

                    for(int k=j+1;k<i;k++)

                    {

                        if(a[k]==a[j])

                        {

                            flag=0;

                            break;

                        }

                    }

                    if(flag)

                    cont[i]+=cont[j];

                }

            }

        }

        printf("%d %d\n",maxn1-1,cont[n+1]);

    }

    return 0;

}

 

你可能感兴趣的:(总结)