在做OJ题目的时候,经常会用到字符串的处理。例如,比较二个字符串相似度。这篇文章介绍一下求两个字符串的最长公共子序列。
一个字符串的子序列,是指从该字符串中去掉任意多个字符后剩下的字符在不改变顺序的情况下组成的新字符串。
最长公共子序列,是指多个字符串可具有的长度最大的公共的子序列。
(1)递归方法求最长公共子序列的长度
1)设有字符串a[0...n],b[0...m],下面就是递推公式。
当数组a和b对应位置字符相同时,则直接求解下一个位置;当不同时取两种情况中的较大数值。
2)代码如下:
#include<stdio.h> #include<string.h> char a[30],b[30]; int lena,lenb; int LCS(int,int); ///两个参数分别表示数组a的下标和数组b的下标 int main() { strcpy(a,"ABCBDAB"); strcpy(b,"BDCABA"); lena=strlen(a); lenb=strlen(b); printf("%d\n",LCS(0,0)); return 0; } int LCS(int i,int j) { if(i>=lena || j>=lenb) return 0; if(a[i]==b[j]) return 1+LCS(i+1,j+1); else return LCS(i+1,j)>LCS(i,j+1)? LCS(i+1,j):LCS(i,j+1); }
用递归的方法优点是编程简单,容易理解。缺点是效率不高,有大量的重复执行递归调用,而且只能求出最大公共子序列的长度,求不出具体的最大公共子序列。
(2)动态规划求最长公共子序列的长度
动态规划采用二维数组来标识中间计算结果,避免重复的计算来提高效率。
1)最长公共子序列的长度的动态规划方程
设有字符串a[0...n],b[0...m],下面就是递推公式。字符串a对应的是二维数组num的行,字符串b对应的是二维数组num的列。
另外,采用二维数组flag来记录下标i和j的走向。数字"1"表示,斜向下;数字"2"表示,水平向右;数字"3"表示,竖直向下。这样便于以后的求解最长公共子序列。
(2)求解公共子序列代码
#include<stdio.h> #include<string.h> char a[500],b[500]; char num[501][501]; ///记录中间结果的数组 char flag[501][501]; ///标记数组,用于标识下标的走向,构造出公共子序列 void LCS(); ///动态规划求解 void getLCS(); ///采用倒推方式求最长公共子序列 int main() { int i; strcpy(a,"ABCBDAB"); strcpy(b,"BDCABA"); memset(num,0,sizeof(num)); memset(flag,0,sizeof(flag)); LCS(); printf("%d\n",num[strlen(a)][strlen(b)]); getLCS(); return 0; } void LCS() { int i,j; for(i=1;i<=strlen(a);i++) { for(j=1;j<=strlen(b);j++) { if(a[i-1]==b[j-1]) ///注意这里的下标是i-1与j-1 { num[i][j]=num[i-1][j-1]+1; flag[i][j]=1; ///斜向下标记 } else if(num[i][j-1]>num[i-1][j]) { num[i][j]=num[i][j-1]; flag[i][j]=2; ///向右标记 } else { num[i][j]=num[i-1][j]; flag[i][j]=3; ///向下标记 } } } } void getLCS() { char res[500]; int i=strlen(a); int j=strlen(b); int k=0; ///用于保存结果的数组标志位 while(i>0 && j>0) { if(flag[i][j]==1) ///如果是斜向下标记 { res[k]=a[i-1]; k++; i--; j--; } else if(flag[i][j]==2) ///如果是斜向右标记 j--; else if(flag[i][j]==3) ///如果是斜向下标记 i--; } for(i=k-1;i>=0;i--) printf("%c",res[i]); }
(3)图示
字符串相似度算法 递归与动态规划求解分析
1.概念
编辑距离,指的是两个字符串之间,由一个转换成另一个所需的最少编辑操作次数。许可的编辑操作包括:(1)将一个字符替换成另一个字符,(2)插入一个字符,(3)删除一个字符。
相似度,等于“编辑距离+1”的倒数。
2.分析
设有字符串a[0...n],b[0...m]。
(1)当a[i]=b[j]时,说明这时候不需要编辑操作。编辑距离保持,即f(i,j)=f(i-1,j-1)
(2)当a[i]!=b[j]时,可以有三种编辑操作。
其中删除和插入操作,只对一个下标i或者j产生影响。如在下图中,当前匹配到(t1,t2)处,如果采用删除'g',只改变t1的下标。
其中替换操作,会对2个下标都产生影响。如在下图中,当前匹配到(t1,t2)处,如果将'g'替换成'm',则下次就需要执行(t1+1,t2+1)处。
所以可以推导出下面就是递推公式。
3.用递归求解代码
#include<stdio.h> #include<string.h> char *a="abcgh"; char *b="aecdgh"; int min(int t1,int t2,int t3) ///求三个数的最小值 { int min; min=t1<t2?t1:t2; min=min<t3?min:t3; return min; } int calculate(int i,int enda,int j,int endb) { int t1,t2,t3; if(i>enda) ///i指示超过a[]的范围时 { if(j>endb) return 0; else return endb-j+1; } if(j>endb) ///j指示超过b[]的范围时 { if(i>enda) return 0; else return enda-i+1; } if(*(a+i) == *(b+j)) ///如果两个相等,则直接求下一个位置 return calculate(i+1,enda,j+1,endb); else { t1=calculate(i+1,enda,j,endb); ///删除a[i]或在b中插入a[i] t2=calculate(i,enda,j+1,endb); ///删除b[j]或在a中插入b[j] t3=calculate(i+1,enda,j+1,endb); ///替换 return 1+min(t1,t2,t3); } } int main() { int dis=calculate(0,strlen(a)-1,0,strlen(b)-1); printf("dis=%d",dis); return 1; }
4.用动态规划求解代码
#include<stdio.h> #include<string.h> #define MAX 1000 int dp[MAX][MAX]; ///dp[i][j]表示当前a[0..i-1]与b[0..j-1]的编辑距离 char *a="agbgd"; char *b="ggd"; int min(int t1,int t2,int t3) ///求三个数的最小值 { int min; min=t1<t2?t1:t2; min=min<t3?min:t3; return min; } int main() { int i,j; int lena=strlen(a),lenb=strlen(b); memset(dp,0,sizeof(dp)); for(i=0;i<=lena;i++) ///a作为行,当b为空串时 dp[0][i]=i; for(i=0;i<=lenb;i++) ///b作为列,当a为空串时 dp[i][0]=i; for(i=1;i<=lena;i++) { for(j=1;j<=lenb;j++) { if(*(a+i)==*(b+j)) ///相等时 dp[i][j]=dp[i-1][j-1]; else dp[i][j]=1+min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1]); ///不相等时,取三种可能操作的最小数值+1 } } printf("编辑距离为:dis=%d\n",dp[lena][lenb]); return ; }