最长递增子序列(LIS)
给出一个序列,找出其中最长的增长序列。
法一:将此序列排序形成一个新序列,求新序列和原序列的最长公共子序列(O(n2))
法二:引入一个数组,用于记录所有长度为n的递增子序列中最小的末尾数 ,然后遍历的时候就可以用二分查找。
b[1] = arr[0]; int max = 1; for(int i=1; i < n; ++i){ int l = 1 , r = max; while(l <= r){ int m = (l+r)/2; if( b[m] < arr[i] ){ l = m+1; } else r = m-1; } b[l] = arr[i]; if(l>max) max++; }
最长公共子串(LCS)
找两个字符串的最长公共子串,这个子串要求在原字符串中是连续的。其实这又是一个序贯决策问题,可以用动态规划来求解。我们采用一个二维矩阵来记录中间的结果。这个二维矩阵怎么构造呢?直接举个例子吧:"bab"和"caba"(当然我们现在一眼就可以看出来最长公共子串是"ba"或"ab")
b a b
c 0 0 0
a 0 1 0
b 1 0 1
a 0 1 0
我们看矩阵的斜对角线最长的那个就能找出最长公共子串。
不过在二维矩阵上找最长的由1组成的斜对角线也是件麻烦费时的事,下面改进:当要在矩阵是填1时让它等于其左上角元素加1。
b a b
c 0 0 0
a 0 1 0
b 1 0 2
a 0 2 0
这样矩阵中的最大元素就是 最长公共子串的长度。
在构造这个二维矩阵的过程中由于得出矩阵的某一行后其上一行就没用了,所以实际上在程序中可以用一维数组来代替这个矩阵。
代码如下:
#include<iostream> #include<cstring> #include<vector> using namespace std; //str1为横向,str2这纵向 const string LCS(const string& str1,const string& str2){ int xlen=str1.size(); //横向长度 vector<int> tmp(xlen); //保存矩阵的上一行 vector<int> arr(tmp); //当前行 int ylen=str2.size(); //纵向长度 int maxele=0; //矩阵元素中的最大值 int pos=0; //矩阵元素最大值出现在第几列 for(int i=0;i<ylen;i++){ string s=str2.substr(i,1); arr.assign(xlen,0); //数组清0 for(int j=0;j<xlen;j++){ if(str1.compare(j,1,s)==0){ if(j==0) arr[j]=1; else arr[j]=tmp[j-1]+1; if(arr[j]>maxele){ maxele=arr[j]; pos=j; } } } // { // vector<int>::iterator iter=arr.begin(); // while(iter!=arr.end()) // cout<<*iter++; // cout<<endl; // } tmp.assign(arr.begin(),arr.end()); } string res=str1.substr(pos-maxele+1,maxele); return res; } int main(){ string str1("21232523311324"); string str2("312123223445"); string lcs=LCS(str1,str2); cout<<lcs<<endl; return 0; }
最长公共子序列
最长公共子序列与最长公共子串的区别在于最长公共子序列不要求在原字符串中是连续的,比如ADE和ABCDE的最长公共子序列是ADE。
我们用动态规划的方法来思考这个问题如是求解。首先要找到状态转移方程:
符号约定,C1是S1的最右侧字符,C2是S2的最右侧字符,S1‘是从S1中去除C1的部分,S2'是从S2中去除C2的部分。
LCS(S1,S2)等于下列3项的最大者:
(1)LCS(S1,S2’)
(2)LCS(S1’,S2)
(3)LCS(S1’,S2’)--如果C1不等于C2; LCS(S1',S2')+C1--如果C1等于C2;
边界终止条件:如果S1和S2都是空串,则结果也是空串。
下面我们同样要构建一个矩阵来存储动态规划过程中子问题的解。这个矩阵中的每个数字代表了该行和该列之前的LCS的长度。与上面刚刚分析出的状态转移议程相对应,矩阵中每个格子里的数字应该这么填,它等于以下3项的最大值:
(1)上面一个格子里的数字
(2)左边一个格子里的数字
(3)左上角那个格子里的数字(如果 C1不等于C2); 左上角那个格子里的数字+1( 如果C1等于C2)
举个例子:
G C T A
0 0 0 0 0
G 0 1 1 1 1
B 0 1 1 1 1
T 0 1 1 2 2
A 0 1 1 2 3
填写最后一个数字时,它应该是下面三个的最大者:
(1)上边的数字2
(2)左边的数字2
(3)左上角的数字2+1=3,因为此时C1==C2
所以最终结果是3。
在填写过程中我们还是记录下当前单元格的数字来自于哪个单元格,以方便最后我们回溯找出最长公共子串。有时候左上、左、上三者中有多个同时达到最大,那么任取其中之一,但是在整个过程中你必须遵循固定的优先标准。在我的代码中优先级别是左上>左>上。
下图给出了回溯法找出LCS的过程:
奉上代码:
#include<iostream> #include<cstring> #include<stack> #include<utility> #define LEFTUP 0 #define LEFT 1 #define UP 2 using namespace std; int Max(int a,int b,int c,int *max){ //找最大者时a的优先级别最高,c的最低.最大值保存在*max中 int res=0; //res记录来自于哪个单元格 *max=a; if(b>*max){ *max=b; res=1; } if(c>*max){ *max=c; res=2; } return res; } //调用此函数时请注意把较长的字符串赋给str1,这主要是为了在回溯最长子序列时节省时间。如果没有把较长的字符串赋给str1不影响程序的正确执行。 string LCS(const string &str1,const string &str2){ int xlen=str1.size(); //横向长度 int ylen=str2.size(); //纵向长度 if(xlen==0||ylen==0) //str1和str2中只要有一个为空,则返回空 return ""; pair<int,int> arr[ylen+1][xlen+1]; //构造pair二维数组,first记录数据,second记录来源 for(int i=0;i<=xlen;i++) //首行清0 arr[0][i].first=0; for(int j=0;j<=ylen;j++) //首列清0 arr[j][0].first=0; for(int i=1;i<=ylen;i++){ char s=str2.at(i-1); for(int j=1;j<=xlen;j++){ int leftup=arr[i-1][j-1].first; int left=arr[i][j-1].first; int up=arr[i-1][j].first; if(str1.at(j-1)==s) //C1==C2 leftup++; int max; arr[i][j].second=Max(leftup,left,up,&arr[i][j].first); // cout<<arr[i][j].first<<arr[i][j].second<<" "; } // cout<<endl; } /*矩阵构造完毕*/ //回溯找出最长公共子序列 stack<int> st; int i=ylen,j=xlen; while(i>=0&&j>=0){ if(arr[i][j].second==LEFTUP){ if(arr[i][j].first==arr[i-1][j-1].first+1) st.push(i); --i; --j; } else if(arr[i][j].second==LEFT){ --j; } else if(arr[i][j].second==UP){ --i; } } string res=""; while(!st.empty()){ int index=st.top()-1; res.append(str2.substr(index,1)); st.pop(); } return res; } int main(){ string str1="GCCCTAGCG"; string str2="GCGCAATG"; string lcs=LCS(str1,str2); cout<<lcs<<endl; return 0; }
最长公共上升序列:
Description
Input
Output
Sample Input
Sample Output
一、我根据上面所提到的方法进行改编的自创方法
也是类似于填棋盘的方法,只不过加一个flag,每次要多加一个的时候需要判断该点的数字是否大于 flag
附上我的代码(一次AC哦!!)
#include<iostream> #include<cstdio> #include<cmath> #include<algorithm> using namespace std; struct A { int num ; int flag ; }a[510][510]; int n1[510]; int n2[510]; int min2( int a , int b) { return a>b?b:a; } int max2( int a , int b, int c) { if( b< a) b= a; if( b> c) return b; else return c; } int main() { int T; cin>>T; for( int q = 0 ; q < T ; ++q) { int m , n ; cin>>m; for( int i= 1 ; i<= m ;++i) cin>>n1[i]; cin>>n; for( int i = 1; i<=n; ++i) cin>>n2[i]; // input finish for( int i = 0 ; i<=m ; ++i) { a[0][i].num = 0 ; a[0][i].flag = 0 ; //cout<<n1[i]; } for( int i = 0 ; i<=n ; ++i) { a[i][0].num = 0; a[i][0].flag = 0; //cout<<n2[i]; } //将第一行第一列清零 for( int i= 1 ; i<= n ; ++i) { for( int j = 1; j<= m ; ++j) { int left = a[i][j-1].num; int up = a[i-1][j].num; int leftup = a[i-1][j-1].num; if( n1[j] == n2[i] && n1[j] > a[i-1][j-1].flag ) leftup ++; // a[i][j]= max( a[i-1][j] , a[i][j-1], a[i-1][j-1]); a[i][j].num = max2(left , up, leftup); if( left == leftup && up== leftup) { a[i][j].flag = min2( a[i-1][j].flag , a[i][j-1].flag ); a[i][j].flag = min2( a[i][j].flag , a[i-1][j-1].flag); } else if( left == up) { if( leftup > left) a[i][j].flag = n1[j]; else { a[i][j].flag = min2( a[i-1][j].flag , a[i][j-1].flag ); } } else { int flag3 = 0; if( a[i][j].num == leftup ) { a[i][j].flag = n1[j]; flag3 = 1; } if( a[i][j].num == left ) { if(flag3 ==0 ) a[i][j].flag = a[i][j-1].flag ; else { a[i][j].flag = min2( a[i][j-1].flag , a[i][j].flag); } } if( a[i][j].num == up ) { if(flag3 ==0 ) a[i][j].flag = a[i-1][j].flag ; else { a[i][j].flag = min2( a[i-1][j].flag , a[i][j].flag); } } } //flag over } }//end for /* for( int i = 0 ; i<= n ; ++i) { for( int j = 0 ; j<= m; ++j) cout<<a[i][j].flag; cout<<endl; } for( int i = 0 ; i<= n ; ++i) { for( int j = 0 ; j<= m; ++j) cout<<a[i][j].num; cout<<endl; } */ cout<<a[n][m].num<<endl; if( q!= T-1 ) cout<<endl; } //system("pause"); return 0; }
二、需好好理解的方法:
1. 定义状态F[i][j]表示以a串的前i个字符 , b串的前j个字符 且 以b[j]为结尾构成的LCIS的长度
2.则可知 F[i][j] =
a[i]!=b[j] : F[i][j] = F[i-1][j] ;
a[i]==b[j] : F[i][j] = max(1<=k<=j)F[i-1][k] + 1 && b[j]=a[i]>b[k];
3.重点来了!!! 根据以上两点可以写出o(n^3)的算法, 可以通过记录max(1<=k<=j)F[i-1][k]的方法来简化 。因为是一层一层循环, 在没行循环的时候加个max 就行了。
附上代码:
// 时间复杂度 O(n^2), 空间复杂度 O(n^2) /* l1为a的大小, l2为b的大小 结果在ans中 f记录路径,DP记录长度 用a对b扫描,逐步最优化 */ #include<string.h> #include<stdio.h> #include<iostream.h> #define MAXN 500 typedef int elem_t; int GCIS(int l1, elem_t a[], int l2, elem_t b[], elem_t ans[]) { int f[MAXN+1][MAXN+1]; int DP[MAXN+1]; int i,j,k,max; memset(f,0,sizeof(f)); memset(DP,0,sizeof(DP)); for (i=1;i<=l1;i++) { k=0; memcpy(f[i],f[i-1],sizeof(f[0])); for (j=1;j<=l2;j++) { if (b[j-1]<a[i-1] && DP[j]>DP[k]) k=j; if (b[j-1]==a[i-1] && DP[k]+1>DP[j]) DP[j]=DP[k]+1,f[i][j]=i*(l2+1)+k; } } for(max=0,i=1;i<=l2;i++) if (DP[i]>DP[max]) max=i; for(i=l1*l2+l1+max,j=DP[max];j;i=f[i/(l2+1)][i%(l2+1)],j--) ans[j-1]=b[i%(l2+1)-1]; return DP[max]; }