算法之动态规划2(LCS最长公共子序列, edit distance,交叉子串)
说过了装配站和矩阵链之后,这里再写三个跟字符串相关的,使用了动态规划算法的实例,一个是LCS,最长公共子序列,一个是Edit Distance,也就是字符串之间转换的代价。还有一个是interleaving string,也就是交叉子串的判断。
1 LCS,最长公共子序列,是评判两个字符串之间相关性的一个量度,它记录的是两个字符串中所有相等并且按照从前向后的顺序组合起来的字符的集合:
eg 对于s1 = "ABCFDRT" 和 s2 = "AGCFRL" 来说, 它们的最长公共子序列就是ACFR, 两个字符串之间的最长公共子序列可以并不唯一,下面就对这个问题剖析一下。
- 首先描述一个最长的公共子序列
假设字符串s1由 x1....xm构成,s2 由 y1...yn构成。s3作为LCS由z1...zk构成。
如果说xm == yn, 那么zk = xm = yn, 并且z1...zk-1所构成的一个序列,一定是s1(m-1)和s2(n-1)的LCS
如果xm != yn, 那么s3 应该是s1(m-1)和s2 的LCS 以及 s1 和 s2(n-1)的LCS中比较大的那一个。
- 由此引出了递归解
矩阵c[i,j]用来描述s1的前i个字符 与 s2的前j个字符,所对应的LCS的长度。
- 接下来计算LCS的长度
矩阵b[i,j]用来表述当前c[i,j]的值是来自哪里的,从而在最后用于重建LCS序列。
- 最后利用矩阵b构造一个LCS
注意到我们使用了两个矩阵c和b,额外的空间是2*m*n, 如果在构造LCS的时候,不用b的值,而是利用c中的值进行比较,也可以实现同样的效果,这时候额外的空间是m*n.
而如果我们只需要知道LCS的长度,不需要最后重建出LCS的话,那么所使用的空间可以进一步的减小,达到min(m,n)+常数空间。 这里假设n是比较小的那一个。
int LevenshteinDistance(string s1, string s2){ int m = s1.length(); int n = s2.length(); vector<vector<int>> dist(m+1,vector<int>(n+1,0)); // 如果任一字符串长度为0,那么转换代价就是另外一个字符串的长度。 for (int i = 1; i <= m;i++) { dist(i, 0) = i; } for (int j = 1; j <= n;j++) { dist(j, 0) = j; } for (int j = 1; j <= n ; j++) { for( int i = 1; i <= m ; i++) { if s1[i-1] = s2[j-1] dist(i, j) = dist(i-1, j-1) // 没有任何的代价 else dist(i, j) = min ( mins(d(i-1, j) + 1, // a deletion d(i, j-1) + 1), // an insertion d(i-1, j-1) + 1 // a substitution ) } } return d(m,n) }
这里需要的空间是O(mn),可以通过进一步的优化达到动态规划算法空间的节省,为min(m,n)+常数
int minDistance(string word1, string word2) { int l1 = word1.length(); int l2 = word2.length(); string& w1 = (l1< l2?word1:word2);//shorter - horizontal string& w2 = (l1< l2?word2:word1);//longer - vertical int length1 = w1.length(); int length2 = w2.length(); if(!length1) return length2; vector<int> dist(length1,0); for(int i = 0; i < length1; i++) dist[i] = i+1; int diag,left,up; for(int jj = 0; jj < length2; jj++) { diag = jj; left = jj+1; for(int ii = 0; ii < length1;ii++) { int upper = dist[ii]; if(w1[ii] == w2[jj]) dist[ii] = diag; else dist[ii] = min(min(diag,left),upper) + 1; diag = upper; left = dist[ii]; } } return dist[length1 - 1]; }
bool isInterleave(string s1, string s2, string s3) { int m = s1.length(),n = s2.length(),k = s3.length(); // 长度简单判断 if(k != m+n) return false; vector<vector<int>> matrix(m+1,vector<int>(n+1,false)); matrix[0][0] = true; // 在开头找s1和s3的重合部分,初始化 for(int i = 1; i <= m; i++) { char c1 = s1[i-1]; char c3 = s3[i-1]; if(c1 == c3) matrix[i][0] = true; else break; } // 在开头找s2和s3的重合部分,初始化 for(int j = 1; j <= n; j++) { char c2 = s2[j-1]; char c3 = s3[j-1]; if(c2 == c3) matrix[0][j] = true; else break; } for(int i = 1; i <= m; i++) { char c1 = s1[i-1]; for(int j = 1; j <= n; j++) { char c2 = s2[j-1]; char c3 = s3[i+j-1]; if(c1 == c3) { matrix[i][j] = matrix[i][j]||matrix[i-1][j]; //字符串s1向前退一步 } if(c2 == c3) matrix[i][j] = matrix[i][j]||matrix[i][j-1]; //字符串s2向前退一步 } } return matrix[m][n]; }
当然肯定也有一个一维的算法,更加的节省空间:
bool isInterleave(string s1, string s2, string s3) { int m=s1.size(), n=s2.size(), k=s3.size(); if(m+n!= k) return false; //switch to save space later on. if(n>m) {string tmp=s2; s2=s1; s1=tmp;} vector<bool> matrix(s2.size()+1, false); matrix[0]=true; for(int i=1; i<=s2.size(); i++) { { char c2 = s2[i-1]; char c3 = s3[i-1]; if(c2 == c3) matrix[i] = true; else break; } for(int i=1; i<=s1.size(); i++){ matrix[0] = s1.substr(0, i)==s3.substr(0,i); for(int j=1; j<=s2.size(); j++) { if(s2[j-1] == s3[i+j-1]) matrix[j] = matrix[j-1]; if(s1[i-1] == s3[i+j-1]) matrix[j] = matrix[j]; } } return matrix.back(); }