各种序列复习:
(1)最长上升子序列。
1、这个问题用动态规划就很好解决了,设dp[i]是以第i个数字结尾的上升子序列的最长长度。那么方程可以是dp[i]=max(dp[j]+1)。(j<i)。复杂度为O(n^2);
2、另外有一个该经典问题的O(nlogn)算法。
首先知道,当求dp[i]时,如果出现a[k]<a[j],而dp[k]=dp[j]时,应当优先选k吧。那么,既然每次选的都是较小,就可以把字符串按照dp[t]=k这个子序列长度分类。当同样dp[t]=k时,记录下该长度的最小的a[p],设为数组d[k]。注意到d数组是单调不减的。为什么呢?因为假设当前是长度p,记录的位置就为d[p],如果出现d[q]>d[p],q<p,干脆就让以d[p]结尾的子序代替前面的。
于是有这个特点:
A、d[1]<=d[2]<=.......
那么,在每次更新dp[i]时,对于字符a[i],只需找出比它小的最大的k,使d[k]<a[i],不就可以了吗。然后更新dp[i]。对于查找,由于单调,很明显可以使用二分查找。
对于一个比较巧妙的写法。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include <iostream> 2 #include <cstdio> 3 #include <cstring> 4 using namespace std; 5 const int N = 41000; 6 int a[N]; //a[i] 原始数据 7 int d[N]; //d[i] 长度为i的递增子序列的最小值 8 9 int BinSearch(int key, int* d, int low, int high) 10 { 11 while(low<=high) 12 { 13 int mid = (low+high)>>1; 14 if(key>d[mid] && key<=d[mid+1]) 15 return mid; 16 else if(key>d[mid]) 17 low = mid+1; 18 else 19 high = mid-1; 20 } 21 return 0; 22 } 23 24 int LIS(int* a, int n, int* d) 25 { 26 int i,j; 27 d[1] = a[1]; 28 int len = 1; //递增子序列长度 29 for(i = 2; i <= n; i++) 30 { 31 if(d[len]<a[i]) 32 j = ++len; 33 else 34 j = BinSearch(a[i],d,1,len) + 1; 35 d[j] = a[i]; 36 } 37 return len; 38 } 39 40 int main() 41 { 42 int t; 43 int p; 44 scanf("%d",&t); 45 while(t--) 46 { 47 scanf("%d",&p); 48 for(int i = 1; i <= p; i++) 49 scanf("%d",&a[i]); 50 printf("%d\n",LIS(a,p,d)); 51 } 52 return 0; 53 }
(2)最长公共子序列。
设dp[i][j]是第一个字符串以第i个结尾,第二个字符串以第j个结尾的长度。
那么就有dp[i][j]=max{dp[i-1][j],dp[i][j-1],dp[i-1][j-1]}。前两种情况针对a[i]!=a[j]的,后一种是针对相等的。
(3)最长公共上升子序列。
我介绍一种O(N(M^2))的算法,它将是我们向O(NM)进步的阶梯。我们设F[j]为必选择B[j]为末尾时的最长。。。子序列(懒得打),那么F[j] = Max{F[k]}+1,并且通过设置一个i变量来枚举A[i]。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 var 2 f : array[0..5000] of integer; 3 ans : integer; 4 procedure work2(); 5 var 6 i,j,k:integer; 7 begin 8 for i:= 1 to n do 9 for j:= 1 to m do 10 if a[i] = b[j] then 11 for k:= 0 to j-1 do 12 if b[k] < b[j] then 13 if f[j] < f[k]+1 then 14 f[j]:=f[k]+1; 15 for i:= 1 to m do 16 if ans < f[i] then 17 ans:=f[i]; 18 end;
此时我们把空间降到了一维。解释一下,k循环下面比较时,B[k]所对应的A[?]一定在A[i]以前,而k也小于j,这就保证了解的合法性。但注意到其中的k循环,这实际上是用来找最大值用的。那么我们想,为什么不把最大只保存起来呢?i循环没结束时不断更新这个k值就行了啊。那么下面的算法就出来了:
procedure work; var i,j,k:integer; begin for i:= 1 to n do begin k:=0; for j:= 1 to m do begin if a[i] = b[j] then if f[j] < f[k]+1 then f[j]:=f[k]+1; if a[i] > b[j] then if f[k] < f[j] then k:=j; // 更新新的k end; end; for i:= 1 to m do if ans < f[i] then ans:=f[i]; end;
其实,对于上面的更新保存最大值的操作,为什么是可行的呢?
要知道,更新DP数组f是在a[i]==b[j]时才进行的,因为f数组定义的是以b[j]为结尾的最长序列。那么,由于是上升的,则必定是在结尾的字符之前b[k]<a[i]吧?那么,在扫描的过程中,就可以当满足
f[k] < f[j]时更新k了。
对于HDU 4512这道题,也就是最长公共上升子序列的模型。我以两个序列来模拟,一个顺序一个逆序。在代码中可以知道,对于以b[j]为结束序列,在最外层循环到i时,内层循环最多只能到n-i+1,为什么呢?因为两个序列是互逆的,当超出这个值时,它们之前的序列就可能交叉或重叠。当a[n-i+1]==b[j]&&n-不+1==j时,序列最长应该是奇数的。在求解过程中找出最长序列即可。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include <iostream> 2 #include <cstdio> 3 #include <algorithm> 4 #include <cstring> 5 6 using namespace std; 7 8 int num1[210],num2[210]; 9 int f[210]; 10 11 int main(){ 12 int T,n; 13 scanf("%d",&T); 14 while(T--){ 15 scanf("%d",&n); 16 for(int i=1;i<=n;i++){ 17 scanf("%d",&num1[i]); 18 num2[n-i+1]=num1[i]; 19 } 20 memset(f,0,sizeof(f)); 21 int ans=1; 22 for(int i=1;i<=n;i++){ 23 int k=0; 24 for(int j=1;j<=n-i+1;j++){ 25 if(num2[i]==num1[j]){ 26 if(f[j] < f[k]+1) 27 f[j]=f[k]+1; 28 if(j==n-i+1) 29 ans=max(ans,2*f[j]-1); 30 else { 31 ans=max(ans,2*f[j]); 32 } 33 } 34 if(num2[i]>num1[j]){ 35 if(f[k]<f[j]) 36 k=j; 37 } 38 } 39 } 40 printf("%d\n",ans); 41 } 42 return 0; 43 }