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; }