一、问题介绍:
本题提出了一些关于将字符串x[1..m]转换成y[1..n]的操作。
这些操作有复制、替代、删除、插入、互换和终止。
这些操作所需的开销是不同的,但每个操作的开销都可以看是一个我们已经的常量,我们假设复制和替代这类操作的开销要比插入和删除这类操作的开销少。
我们用x[1..m]来保存原字符串,数组下标用i表示,初始化为1;
用y[1..n]来保存转换后的字符串,数组下标用j来表示,初始化为1;数组z用来存放中间结果,下标用j来表示,初始化为0。
题目有两个待解决的问题:
第一,给定两个序列x[1..m]和y[1..n]以及变换操作开销的集合。从x到y的编辑距离指的就是将x转换成y时的最小开销的操作序列。用动态规划的算法找出从x到y的编辑距离并输出最优操作序列。分析算法的时空复杂度。
第二, 解释如何从给定的编辑操作集选择一个子集,从而把寻找总分最高的对齐结果的问题,转化为求序列编辑距离的问题的一个特例。
二、算法分析:
问题a解答:
此题跟最长公共子序列的问题十分类似。
因此,我仿照最长公共子序列问题的方法来求解这个问题。
首先,定义了两个序列Xi=x[1,2,...,m]和Yj=y[1,2,…,n],题目所求的就是将序列xi变为yj的编辑操作序列的最小开销。
刻画最优解的结构:
对于序列Xi(0≤i≤m)和序列Yj(0≤j≤n)来说,定义c[i,j]为Xi转换成Yj的操作序列的最小开销。假定我们已经知道了最后一次执行的操作,此时分情况讨论问题的最优解结构。
1. 最后一次操作为copy。此时,根据题目的定义可知x[i]=y[j],我们待解决的问题就转化成了求将Xi-1转换为Yj-1的最小开销。将Xi-1转换为Yj-1的最优解一定包含在Xi转换为Yj的最优解内。用反证法证明:若存在从Xi-1到yj-1转换的更小的开销,那么用这个更小的开销来代替Xi-1转换为yj-1的最优解,这样就会得到一个更小的从Xi转换为Yj的开销,与已知的Xi转换为Yj的最优解矛盾,所以假设不成立。因此,当最后一次操作为copy时,可以定义c[i,j]=c[i-1,j-1]+cost(copy)。
2. 最后一次操作为replace。此时,根据题目的定义可知x[i]≠y[j]。仿照上述分析,可以得到相同的最优子结构。此时c[i,j]=c[i-1,j-1]+cost(replace)。
3. 最后一次操作为delete。根据题意,这时只是将x[i]从序列Xi中除去,对序列Yj没有任何影响,此时问题的最优子结构的形式为将Xi-1转换成Yj,于是可以得到c[i,j]=c[i-1,j]+cost(delete)。
4. 最后一次操作为insert。根据题意,在进行插入操作时,序列Xi无任何变化,序列Yj添加一个字符,因此,这时候问题的最优子结构的形式为将Xi转换成为Yj-1,此时c[i,j]=c[i,j-1]+cost(insert)。
5. 最后一次操作为twiddle。根据题意,互换操作是针对两个字符进行的,此时有y[j]=x[i-1]和y[j-1]=x[i]。在进行这个操作的时候,需要满足的条件是i,j≥2。在这种情况下,问题的最优子结构的形式为Xi-2转换为Yi-2,此时c[i,j]=c[i-2,j-2]+cost(twiddle)。
6. 最后一次操作为kill。这种情况比较特殊,由于kill操作可以一次删除一个子串,对所有i=1..m-1, j=1..n,当X被检查到xi,Y被生成到yj的时候,用一个kill操作可以删除X中剩下的元素,用(n-i)个insert操作可以插入Y中尚未出现的全部元素。这样即可把X中的元素全部检查到,并且生成完整的Y。这种方式把X变成Y的总开销为c[i][j]+cost(kill)+(n-i) *cost(insert)。对所有c[i][j]求这个值,并且和上述忽略kill操作的前提下求出的编辑距离相比,取其中最小值,即求得编辑距离。
考虑一些特殊的情况,当X和Y为空串时,c[0,0]=0。当Xi为一个非空序列Yj为空序列时,可以很明显的看出此时的操作全为delete,因此有c[i,0]=i*cost(delete)。
当Xi为一个空序列Yj为非空序列时,可以很明显的看出此时的操作全为insert,因此有c[0,j]=j*cost(insert)
算法的伪代码如下所示:
Edit-Distance(x,y,m,n)
for i←0 to m
do c[i,0]=i*cost(delete)
op[i,0] ←delete
for j←0 to n
do c[0,j]=j*cost(insert)
op[j,0] ←insert
for i←1 to m
do for j←1 to n
do c[i,j] ←∞
if x[i]=y[j]
then c[i,j] ←c[i-1,j-1]+cost(copy)
op[i,j] ←copy
if x[i]≠y[j] and c[i-1,j-1]+cost(replace)<c[i,j]
then c[i,j]= c[i-1,j-1]+cost(replace)
op[i,j] ← replace
if c[i-1,j]+cost(delete)<c[i,j]
then c[i,j] ←c[i-1,j]+cost(delete)
op ←delete
if c[i,j-1]+cost(insert)<c[i,j]
then c[i,j]←c[i,j-1]+cost(insert)
op ← insert
if i≥2 and j≥2 and x[i-1]=y[j] and x[i]=y[j-1] and c[i-2,j-2]+cost(twiddle)<c[i,j]
then c[i,j]= c[i-2,j-2]+cost(twiddle)
op ← twiddle
for i ←0 to m-1
do if c[i,n]+cost(kill)<c[m,n]
then c[m,n]←c[i,n]+cost(kill)
op[m,n]←killi
return c and op
算法的时间复杂度分析:该算法是对一个(m+1)(n+1)的表格进行填写的过程,填写每一个格c[i][j]花费的开销,只是根据 c[i-1][j], c[i][j-1], c[i-1][j-1], c[i-2][j-1]进行简单的加减运算,得到五个数字,选择其中最小的一个。所以该算法的时间复杂度为O(mn)。
最后构造问题的最优解,编辑序列输出的算法伪代码如下所示:
Out-Operation(op,i,j)
if i=0 and j=0
then return
if op[i,j] = copy or op[i,j] = replace
then i’←i-1
j’←j-1
else if op[i,j]=delete
then i’=i-1
j’=j
else if op[i,j] = insert
then i’=i
j’=j-1
else if op[i,j]=twiddle
then i’=i-2
j’=j-2
else
let op[i,j]=killk
i’=k
j’=j
Out-Operation(op,i’,j’)
print op[i,j]
C++的源码如下(没有实现kill,个人认为没有使用价值):
#include <iostream> #include <cstring> #include <vector> using namespace std; #define _DEBUG //definition the cost of the operation const int cost_copy = 0; //0 const int cost_replace = 1; //1 const int cost_delete = 1; //2 const int cost_insert = 1; //3 const int cost_twiddle = 1; //4 const int cost_kill = 1; //5 template<class T> void DisplayArray(vector<T>&array) { for (int i = 0;i<array.size();++i) cout<<array[i]<<" "; } template<class T> void Display2DArray(vector< vector<T> >&array) { for (int i = 0;i<array.size();++i) { DisplayArray(array[i]); cout<<endl; } } void MinEditDistence(const char *X,const char* Y,vector<vector<int> >&B,vector<vector<int> >&C) {//B is operation matrix and C is the cost matrix int m = strlen(X), n = strlen(Y); for (int i = 0;i<m+1;++i)//if string X is Null, cost is delete all element { C[i][0] = i*cost_delete; B[i][0] = 2;//delete = 2 } for (int i = 0;i<n+1;++i) { C[0][i] = i*cost_insert; B[0][i] = 3 ;//3 = insert } #ifdef _DEBUG Display2DArray(B); cout<<endl; Display2DArray(C); cout<<endl; #endif // _DEBUG for (int i = 1;i<m+1;++i) { for (int j =1;j<n+1;++j) { C[i][j] = INT_MAX; if (X[i-1] == Y[j-1])//copy { C[i][j] = C[i-1][j-1] + cost_copy; B[i][j] = 0;// copy =0 } else { if (C[i-1][j-1] + cost_replace < C[i][j])//replace { C[i][j] = C[i-1][j-1] + cost_replace; B[i][j] = 1; } if (C[i][j] > C[i-1][j] + cost_delete)//delete { C[i][j] = C[i-1][j] + cost_delete; B[i][j] = 2; } if (C[i][j] > C[i][j-1] + cost_insert)//insert { C[i][j] = C[i][j-1] + cost_insert; B[i][j] = 3; } if (i>=2 && j>= 2 && X[i-1] == Y[j-2] && X[i-2] == Y[j-1] && C[i][j] > C[i-2][j-2] + cost_twiddle)//twiddle { C[i][j] = C[i-2][j-2] + cost_twiddle; B[i][j] = 4; } } } } // for (int i = 0;i<m;++i)//kill // { // if (C[m][n] > C[i][n] + cost_kill) // { // C[m][n] = C[i][n] + cost_kill; // B[m][n] = 5;//kill = 5 // } // } } void PrintMEDOperation(vector<vector<int> >&Operation,int m,int n) { if( m==0 && n==0) return; int i,j; if (Operation[m][n] == 0)//copy { i = m-1; j = n-1; } else if (Operation[m][n] == 1)//replace { i = m-1; j = n-1; } else if (Operation[m][n] == 2)//delete { i = m-1; j = n; } else if (Operation[m][n] == 3)//insert { i = m; j = n-1; } else if (Operation[m][n] == 4)//twiddle { i = m-2; j = n-2; } else if (Operation[m][n] == 5)//kill { } PrintMEDOperation(Operation,i,j); cout<<"["<<i<<","<<j<<"] = "<<Operation[m][n]<<endl; } int main() { char *X = "algorithm"; char *Y = "altruistic"; int m = strlen(X), n = strlen(Y); vector<vector<int> >B(m+1,vector<int>(n+1,0)),C(m+1,vector<int>(n+1,0)); MinEditDistence(X,Y,B,C); Display2DArray(B); cout<<endl; Display2DArray(C); cout<<endl; PrintMEDOperation(B,m,n); return 0; }
问题b解答:
在DNA对齐问题中,最高得分的对齐方法可以通过选择下面四个操作并相应赋给开销,来转换成一个编辑距离问题:
insert, -2
delete, -2
copy, +1
replace, -1
(1)如果x1[j]==y1[j],1分;对应的是copy操作;
(2)如果x1[i]!=y1[j]并且两者都不是空格,-1分;对应的是replace操作;
(3)如果x1[j]或者y1[j]是空格,-2分,对应的是insert和delete操作。