动态规划——最大子序列、最长递增子序列、最长公共子串、最长公共子序列、字符串最小编辑距离
一、最大子序列
最大子序列是要找出由数组成的一维数组中和最大的连续子序列。比如{5,-3,4,2}的最大子序列就是 {5,-3,4,2},它的和是8,达到最大;而 {5,-6,4,2}的最大子序列是{4,2},它的和是6。你已经看出来了,找最大子序列的方法很简单,只要前i项的和还没有小于0那么子序列就一直向后扩展,否则丢弃之前的子序列开始新的子序列,同时我们要记下各个子序列的和,最后找到和最大的子序列。
状态转移方程:arr为字符串数组,从第一个字符开始,依次求和curr_sum:只要前i项的和还没有小于0那么子序列就一直向后扩展,否则丢弃之前的子序列从下一位置开始新的子序列,
<span style="font-size:14px;">#include <iostream>
#include "vector"
using namespace std;
int maxSubSum(const vector<int> &arr,int &begin,int &end){
int maxsum=0;//记录最大值
int curr_sum=0;//记录当前求和
int newBegin=0;//
<span style="color:#3333ff;">for(int i=0;i<arr.size();i++){
curr_sum+=arr[i];
if(curr_sum>maxsum){
maxsum=curr_sum;
begin=newBegin;
end=i;
}
if(curr_sum<0)//当前i项的子序列和没有<0,就一直往后扩展,否则丢弃之前的子序列,重新开始新的子序列
{
curr_sum=0;
newBegin=i+1;//新的序列开始位置
}
}</span>
return maxsum;
}
int main()
{
while(1){
int len;//数组长度
cout << "最大子序列:连续的子序列的和最大。" << endl;
cout<<"输入数组长度:len"<<endl;
cin>>len;
cout<<"输入长度为len的数字"<<endl;
vector<int> arr;
int a;//读取字符
for(int i=0;i<len;i++){
cin>>a;
arr.push_back(a);
}
int begin,end;//用于表示构成最大子序列开始结束位置。
cout<<"最大值:"<<maxSubSum(arr,begin,end)<<endl<<"最大子序列";
for(int i=begin;i<=end;i++){
cout<<arr[i]<<" ";
}
cout<<endl;
}
return 0;
}</span>
#include <iostream>
#include "vector"
#include "stack"
using namespace std;
void Longest_sub(int *arr,int len){
if(len==0)
return ;//空序列
int S[len];/*定义数组,S[i]表示以arr[i]结尾的子序列的长度,
S[i]=max{S[r]|1<=r<i,a[r]<a[i]}+1,初始情况S[1]=1 */
int maxValue=1;//保存长度最大值
int maxValue_index=0;//长度最大值的索引位置
S[0]=1;//可以将S[]全部清零memset函数
<span style="color:#3333ff;"> for(int i=1;i<len;i++){
int curr_max_value=1;
for(int r=0;r<i;r++){//由i之前的字符构成的序列
if(arr[r]<arr[i]&&S[r]+1>curr_max_value){
curr_max_value=S[r]+1;
}
}
S[i]=curr_max_value;
if(maxValue<curr_max_value){
maxValue=curr_max_value;
maxValue_index=i;
}
}</span>
//输出最大值和最长递增子序列
stack<int> st;
int i=maxValue_index;
cout<<"The length:"<<maxValue<<endl;
while(S[i]>1){
cout<<arr[i]<<"--->";
st.push(arr[i]);
for(int r=0;r<i;r++){
if(S[r]+1==S[i]&&arr[r]<arr[i]){
i=r;break;
}
}
}
cout<<arr[i]<<endl;
st.push(arr[i]);
while(!st.empty()){
cout<<st.top()<<" ";st.pop();
}
}
int main()
{
while(1){
int len;//数组长度
cout << "最长递增子序列:子序列不连续,一次递增。" << endl;
cout<<"输入数组长度len:"<<endl;
cin>>len;
cout<<"输入长度为len的数字"<<endl;
int arr[len];
for(int i=0;i<len;i++){
cin>>arr[i];
}
Longest_sub(arr,len);
cout<<endl;
}
return 0;
}
<span style="font-size:14px;">#include <iostream>
#include<cstring>
#include <stdlib.h>
using namespace std;
const string LCS(const string& str1,const string& str2){
int xlen=str1.size();
int ylen=str2.size();
int maxvalue=0,pos=0;//存储以a[i]为结尾的str1与str2字符串的公共子串长度和位置
int S[xlen];//保存上一行的长度值,模拟二维数组
int tem[xlen];//当前行的长度
memset(S,0,sizeof(S));
<span style="color:#3333ff;">for(int i=0;i<ylen;i++){
memset(tem,0,sizeof(tem));
for(int j=0;j<xlen;j++){
if(str2[i]==str1[j]){
if(j==0||i==0){
tem[j]=1;
}else{
tem[j]=S[j-1]+1;
}
if(tem[j]>maxvalue){
maxvalue=tem[j];
pos=j;
}
}
}
for(int k=0;k<xlen;k++){
S[k]=tem[k];
}
}</span>
cout<<"最长公共子串的长度是:"<<maxvalue<<endl;
string result=str1.substr(pos-maxvalue+1,maxvalue);//从长度最大的位置截取前axvalue个字符
return result;
}
int main()
{
while(1){
cout << "找两个子串的最长公共子串。" << endl;
string str1,str2;
cout<<"输入两个子串"<<endl<<"字符串1:";
cin>>str1;
cout<<"字符串2:";
cin>>str2;
string lcs=LCS(str1,str2);
cout<<lcs<<endl;
}
return 0;
}</span>
四、最长公共子序列
最长公共子序列与最长公共子串的区别在于最长公共子序列不要求在原字符串中是连续的,比如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的过程:
状态转移方程:
MaxLen(n,0) = 0 ( n= 0…len1)
MaxLen(0,n) = 0 ( n=0…len2)
递推公式:
if ( s1[i-1] == s2[j-1] ) //s1的最左边字符是s1[0]
MaxLen(i,j) = MaxLen(i-1,j-1) + 1;
else
MaxLen(i,j) = Max(MaxLen(i,j-1),MaxLen(i-1,j) );
时间复杂度O(mn) m,n是两个字串长度
代码如下:
#include <iostream>
#include<stack>
#include <string>
#define LEFTUP0 0
#define LEFT1 1
#define UP2 2//定义回溯优先级
using namespace std;
int Max(int a,int b,int c,int *m){//找最大时a的优先级最高,上左,左,上
int res=0;//记录来自哪个表格,res的值之后与LEFTUP,LEFT,UP对应。
*m=a;
if(b>*m){
*m=b;
res=1;
}
if(c>*m){
*m=c;
res=2;
}
return res;
}
int max_sum(int a,int b){//若不用输出具体序列。<span style="font-family: SimSun;">MaxLen(i,j) = Max(MaxLen(i,j-1),MaxLen(i-1,j) );</span>
return a>b?a:b;
}
void LCS(const string &str1,const string &str2){
int xlen=str1.size();
int ylen=str2.size();
if(xlen==0||ylen==0)
return ;
int maxvalue=0;//存储以a[i]为结尾的str1与str2字符串的公共子串长度和位置
pair<int,int> S[ylen+1][xlen+1];//定义一个二维数组,
for(int i=0;i<=ylen;i++)
S[i][0].first=0;
for(int j=0;j<=xlen;j++)
S[0][j].first=0;//加边,每列第一个数为0
for(int i=1;i<=ylen;i++)
{
char a=str2.at(i-1);
for(int j=1;j<=xlen;j++)
{
int leftup0=S[i-1][j-1].first;
int left1=S[i][j-1].first;
int up2=S[i-1][j].first;
<span style="color:#3333ff;">if(str1.at(j-1)==a)
{//两个字符串的某个字符相等C1==C2
// S[i][j].first=leftup+1; maxvalue=S[i][j].first;
leftup0++;
}
S[i][j].second=Max(leftup0,left1,up2,&S[i][j].first);
//在Max中将上左,左,上的最大值赋给S[i][j],并且表明是来自哪个方向的数值</span>
if(S[i][j].first>maxvalue)
maxvalue=S[i][j].first;
}
}
cout<<"公共子序列长度:"<<maxvalue<<endl;
//采用回溯法找到组成最长公共子序列的字符值,从最后往前找
stack<int> st;
int i=ylen,j=xlen;
while(i>=0&&j>=0){
if(S[i][j].second==LEFTUP0)//来自左上
{
if(S[i][j].first==S[i-1][j-1].first+1)
st.push(i);
--i;--j;
}
else if(S[i][j].second==LEFT1){
--j;
}
else if(S[i][j].second==UP2){
--i;
}
}
string result="";
cout<<"长度:"<<st.size()<<endl;
while(!st.empty()){
int index=st.top()-1;
result.append(str2.substr(index,1));
st.pop();
}
cout<<result<<endl;
}
int main()
{
while(1){
cout << "找两个字串的最长公共子序列。" << endl;
string str1,str2;
cout<<"输入两个字符串"<<endl<<"字符串1:";
cin>>str1;
cout<<"字符串2:";
cin>>str2;
LCS(str1,str2);
}
return 0;
}
五、字符串最小编辑距离
要想把字符串S1变成S2,可以经过若干次下列原子操作:
1.删除一个字符
2.增加一个字符
3.更改一个字符
字符串S1和S2的编辑距离定义为从S1变成S2所需要原子操作的最少次数。
解法跟上面的最长公共子序列十分相似,都是动态规划,把一个问题转换为若干个规模更小的子问题,并且都借助于一个二维矩阵来实现计算。
约定:字符串S去掉最后一个字符T后为S',T1和T2分别是S1和S2的最后一个字符。
则dist(S1,S2)是下列4个值的最小者:
1.dist(S1',S2')--当T1==T2
2.1+dist(S1',S2)--当T1!=T2,并且删除S1的最后一个字符T1
3.1+dist(S1,S2')--当T1!=T2,并且在S1后面增加一个字符T2
4.1+dist(S1',S2')--当T1!=T2,并且把S1的最的一个字符T1改成T2
把问题转换为二维矩阵:
arr[i][j]表示S1.sub(0,i)和S2.sub(0,j)的编辑距离,则
arr[i][j]=min{1+arr[i][j-1], 1+arr[i-1][j], 1+arr[i-1][j-1](当S1[i]!=S2[j]), arr[i-1][j-1](当S1[i]==S2[j])}
边界情况:arr[0][j]=j, arr[i][0]=i
代码请大家自己尝试。
计算两个字条串的相似度除了Edit Distance,还有一种方法是计算Jaro Distance。具体怎么算读者可以搜一下。