0 、前言
動態規劃就是把一個大的問題拆分成幾個類似的子問題,通過求解子問題來獲得最終的結果,常採用遞歸的方法。由於遞歸的方法中會重複地計算相同的子問題,使得效率較低。爲減少重複計算相同子問題的時間,引入一個數組,把所有子問題的解存放於該子數組,這是動態規劃採用的基本方法。
編輯距離 、最長公共子串、最長公共子序列以及最長遞增子序列都是採用動態規劃方法進行求解的,而且他們之間有相同和不同之處,下面細作分析。
1、編輯距離
編輯距離解題思路:
首先定義這樣一個函數——edit(i, j),它表示第一個字符串的長度爲i的子串到第二個字符串的長度爲j的子串的編輯距離。顯然可以有如下動態規劃公式:
· if i == 0 且 j == 0,edit(i, j) = 0
· if i == 0 且 j > 0,edit(i, j) = j
· if i > 0 且j == 0,edit(i, j) = i
· if i ≥ 1 且 j ≥ 1 ,edit(i, j) == min{ edit(i-1, j) + 1, edit(i, j-1) + 1, edit(i-1, j-1) +f(i, j) },當第一個字符串的第i個字符不等於第二個字符串的第j個字符時,f(i, j) = 1;否則,f(i, j) = 0。
|
0 |
f |
a |
i |
l |
i |
n |
g |
0 |
|
|
|
|
|
|
|
|
s |
|
|
|
|
|
|
|
|
a |
|
|
|
|
|
|
|
|
i |
|
|
|
|
|
|
|
|
l |
|
|
|
|
|
|
|
|
n |
|
|
|
|
|
|
|
|
|
0 |
f |
a |
i |
l |
i |
n |
g |
0 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
s |
1 |
|
|
|
|
|
|
|
a |
2 |
|
|
|
|
|
|
|
i |
3 |
|
|
|
|
|
|
|
l |
4 |
|
|
|
|
|
|
|
n |
5 |
|
|
|
|
|
|
|
計算edit(1, 1),edit(0, 1) + 1 == 2,edit(1, 0) + 1 == 2,edit(0, 0) + f(1, 1) == 0 + 1 == 1,min(edit(0, 1),edit(1, 0),edit(0, 0) + f(1, 1))==1,因此edit(1, 1) == 1。 依次類推:
|
0 |
f |
a |
i |
l |
i |
n |
g |
0 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
s |
1 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
a |
2 |
2 |
|
|
|
|
|
|
i |
3 |
|
|
|
|
|
|
|
l |
4 |
|
|
|
|
|
|
|
n |
5 |
|
|
|
|
|
|
|
edit(2, 1) + 1 ==3,edit(1, 2) + 1 == 3,edit(1, 1) + f(2, 2) == 1 + 0 == 1,其中s1[2] == 'a' 而 s2[1] == 'f'‘,兩者不相同,所以交換相鄰字符的操作不計入比較最小數中計算。以此計算,得出最後矩陣爲:
|
0 |
f |
a |
i |
l |
i |
n |
g |
0 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
s |
1 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
a |
2 |
2 |
1 |
2 |
3 |
4 |
5 |
6 |
i |
3 |
3 |
2 |
1 |
2 |
3 |
4 |
5 |
l |
4 |
4 |
3 |
2 |
1 |
2 |
3 |
4 |
n |
5 |
5 |
4 |
3 |
2 |
2 |
2 |
3 |
#include <iostream>
#include <string>
using namespace std;
int min(int a, int b)
{
return a < b ? a : b;
}
int edit(string str1, string str2)
{
int max1 = str1.size();
int max2 = str2.size();
int **ptr = new int*[max1 + 1];
for(int i = 0; i < max1 + 1 ;i++)
{
ptr[i] = new int[max2 + 1];
}
for(int i = 0 ;i < max1 + 1 ;i++)
{
ptr[i][0] = i;
}
for(int i = 0 ;i < max2 + 1;i++)
{
ptr[0][i] = i;
}
for(int i = 1 ;i < max1 + 1 ;i++)
{
for(int j = 1 ;j< max2 + 1; j++)
{
int d;
int temp = min(ptr[i-1][j] + 1, ptr[i][j-1] + 1);
if(str1[i-1] == str2[j-1])
{
d = 0 ;
}
else
{
d = 1 ;
}
ptr[i][j] = min(temp, ptr[i-1][j-1] + d);
}
}
cout << "**************************" << endl;
for(int i = 0 ;i < max1 + 1 ;i++)
{
for(int j = 0; j< max2 + 1; j++)
{
cout << ptr[i][j] << " " ;
}
cout << endl;
}
cout << "**************************" << endl;
int dis = ptr[max1][max2];
for(int i = 0; i < max1 + 1; i++)
{
delete[] ptr[i];
ptr[i] = NULL;
}
delete[] ptr;
ptr = NULL;
return dis;
}
int main(void)
{
string str1 = "sailn";
string str2 = "failing";
int r = edit(str1, str2);
cout << "the dis is : " << r << endl;
return 0;
}
2、最長公共子串和最長公共子序列
最長公共子串(Longest Common Substring)是指兩個或者多個字符串中都包含的最長的子符串,其在原字符創中是連續的。最長公共子序列(Longest Common Subsequence)是指兩個或者多個字符串中都包含的最長的子序列,其不要求連續,但在原字符創中的相對位置是不變的。
例如,字符串A:12345;字符串B:23465 。則他們的最長公共子串爲:234,最長公共子序列爲2345。
2.1 最長公共子序列
最長公共子序列解題思路:
這種題目使用動態規劃解決。爲了節約重複求相同子問題的時間,引入一個數組,不管它們是否對最終解有用,把所有子問題的解存於該數組中,這就是動態規劃法所採用的基本方法。所以此處引進一個二維數組c[][],用c[i][j]記錄X[i]與Y[j] 的LCS 的長度,b[i][j]記錄c[i][j]是通過哪一個子問題的值求得的,以決定搜索的方向。
我們首先進行自底向上的遞推計算,那麼在計算c[i,j]之前,c[i-1][j-1],c[i-1][j]與c[i][j-1]均已計算出來。此時我們根據X[i] = Y[j]還是X[i] != Y[j],就可以計算出c[i][j]。具體思路如下
if(x[i]==y[j])//如果X[i]和 Y[j]相等,那麼c[i][j]的值可以通過1+c[i-1][j-1]得出。而c[i-1][j-1]已經推算出來。
c[i][j]=1+c[i-1][j-1]
else{//如果X[i] 和 Y[j]不相等,那麼x[i]和y[j]的最長公共子序列可能是x[i]與y[j-1]的最長公共子序列,也可能是x[i-1]與y[j]的最長公共子序列,我們求其最大值
c[i][j]=max(c[i][j-1],c[i-1][j])
}
我們上面的自底向上推算是在“c[i-1][j-1],c[i-1][j]與c[i][j-1]已經計算出來後再求c[i,j]”這個前提下進行的。所以我們構建c[][]這個數組是從頭開始的。在創建好c[][]以後我們需要初始化這個二維數組,c[0][0...n]=0,c[0...m][0]=0。這是爲了便於計算後面的c[1][1]使用。用於表示x[1]和y[1]的最長公共子序列,但是我們發現字符數組char* x[]跟char* y[]是從下標0開始計算的,所以在這裏x[i][j]表示的是x[i-1]和y[j-1]的最長公共子序列。
最後問題可以用遞歸式寫成:
上述方法只給出了最長公共子序列的長度,但是沒有輸出最長公共子序列,如前所述我們需要通過一個b[][]來記錄c[][]是由哪一步得到的。
1. b[i][j]=0;//表示c[i][j]由c[i-1][j-1]+1得到
2. b[i][j]=1;//表示c[i][j]由c[i][j-1]得到
3. b[i][j]=-1;//表示c[i][j]由c[i-1][j]得到
這樣在輸出最長子序列的時候,我們從c[][]最後一個位置開始遞歸遍歷。代碼實現如下:
/最長公共子序列,英文縮寫爲LCS(LongestCommon Subsequence)
#include<iostream>
#include<stdlib.h>
using namespace std;
#define MaxLen 100
int max(int a,int b)
{
return a>b?a:b;
}
void LCSLength(char* s1, char* s2, intlen1, int len2, int c[][MaxLen], int b[][MaxLen])
{
int i,j;
//初始化c[][]
for(i=0;i<=len1;i++)//從0開始
{
c[i][0]=0;
}
for(j=0;j<=len2;j++)//從0開始或者從1開始都可以
{
c[0][j]=0;
}
for(i=1;i<=len1;i++)//從1開始
for(j=1;j<=len2;j++)//從1開始
{
if(s1[i-1]==s2[j-1])//注意這裏是i-1和j-1,因爲字符串數組從下標0開始。
{
c[i][j]=c[i-1][j-1]+1;
b[i][j]=0;//表示c[i][j]由c[i-1][j-1]+1得到
}
else if(c[i][j-1]>=c[i-1][j])
{
c[i][j]=c[i][j-1];
b[i][j]=1;//表示c[i][j]由c[i][j-1]得到
}
else
{
c[i][j]=c[i-1][j];
b[i][j]=-1;//表示c[i][j]由c[i-1][j]得到
}
}
////輸出c[][]
//for(i=0;i<=len1;i++)
//{
// for(j=0;j<=len2;j++)
// {
// cout<<c[i][j]<<" ";
// }
// cout<<endl;
//}
}
void PrintLCS(char* s1,int i,int j,intb[][MaxLen])
{
//遞歸退出條件
if(i<=0||j<=0)
return;
if(b[i][j]==0)
{
PrintLCS(s1,i-1,j-1,b);
cout<<s1[i-1];
}
else if(b[i][j]==1)//表示c[i][j]由c[i][j-1]得到
{
PrintLCS(s1,i,j-1,b);
}
else
{
PrintLCS(s1,i-1,j,b);
}
}
int main()
{
char* s1="ABCBDAB";
char* s2="BDCABA";
int len1=strlen(s1);
int len2=strlen(s2);
//cout<<len1<<endl;
int c[MaxLen][MaxLen];//c[i][j]記錄X[i]與Y[j] 的LCS 的長度
int b[MaxLen][MaxLen];//b[i][j]記錄c[i][j]是通過哪一個子問題的值求得的,以決定搜索的方向
LCSLength(s1,s2,len1,len2,c,b);
PrintLCS(s1,len1,len2,b);
system("pause");
return 0;
}
2.2 最長公共子串
解題思路:
找兩個字符串的最長公共子串,這個子串要求在原字符串中是連續的。可以用動態規劃來求解。我們採用一個二維矩陣來記錄中間的結果。這個二維矩陣怎麼構造呢?首先初始化這個二維數組c[][],
1. 如果x[0..i]=y[0],則c[i][0]=1,否則c[i][0]=0
2. 如果y[0..j]=x[0],則c[0][j]=1,否則c[0][j]=0
然後計算c[i][j]的值,如果x[i]==y[j],則c[i][j]=c[i-1][j-1]+1。
直接舉個例子吧:"ABCBDAB"和"BDCABA"(當然我們現在一眼就可以看出來最長公共子串是"AB"或"BD")
B D C A B A
A 0 0 0 1 0 1
B 1 0 0 0 2 0
C 0 0 1 0 0 0
B 1 0 0 0 1 0
D 0 2 0 0 0 0
A 0 0 0 1 0 1
B 1 0 0 0 2 0
代碼實現
//最長公共子串,英文縮寫爲LCS(LongestCommon Substring)
#include<iostream>
#include<stdlib.h>
using namespace std;
#define MaxLen 100
void LCSubstring(char* s1, char* s2, int len1, int len2,int c[][MaxLen])
{
int i,j;
//初始化c[][]
for(i=0;i<len1;i++)
{
if(s1[i]==s2[0])
c[i][0]=1;
else
c[i][0]=0;
}
for(j=0;j<len2;j++)
{
if(s2[j]==s1[0])
c[0][j]=1;
else
c[0][j]=0;
}
int max=0;
int m,n;
for(i=1;i<len1;i++)//從1開始
for(j=1;j<len2;j++)//從1開始
{
if(s1[i]==s2[j])
{
c[i][j]=c[i-1][j-1]+1;
if(c[i][j]>max)//記錄最長公共字串的位置
{
max=c[i][j];
m=i;
n=j;
}
}
else
{
c[i][j]=0;
}
}
for(i=m-max+1;i<=m;i++)//輸出公共字串
{
cout<<s1[i];
}
cout<<endl;
//輸出c[][]
for(i=0;i<len1;i++)
{
for(j=0;j<len2;j++)
{
cout<<c[i][j]<<" ";
}
cout<<endl;
}
}
int main()
{
char* s1="ABCBDAB";
char* s2="BDCABA";
int len1=strlen(s1);
int len2=strlen(s2);
int c[MaxLen][MaxLen];//c[i][j]記錄X[i]與Y[j] 的LCS 的長度
LCSubstring(s1,s2,len1,len2,c);
system("pause");
return 0;
}
3.最長遞增子序列
3.1參考文獻:
http://blog.csdn.net/hhygcy/article/details/3950158
3.2解題思路
既然已經說到了最長公共子序列,就把這個遞增子序列也說了。同樣的,這裏subsequence表明了這樣的子序列不要求是連續的。比如說有子序列{1,9, 3, 8, 11, 4, 5, 6, 4, 19, 7, 1, 7 },這樣一個字符串的的最長遞增子序列就是{1,3,4,5,6,7}或者{1,3,4,5,6,19}。
其實這個問題和前面的最長公共子序列問題還是有一定的關聯的。
1. 假設我們的初始的序列 S1= {1,9, 3, 8, 11, 4, 5, 6, 4, 19, 7, 1, 7 }。
2. 那我們從小到大先排序一下。得到了S1'={1, 1, 3, 4, 4, 5, 6, 7, 7 , 8, 9, 11, 19}。
這樣我們再求S1和S1'的最長公共子序列就是S1的最長遞增子序列了。這個過程還是比較直觀的。但是這個不是這次要說的重點,這個問題有比較傳統的做法的.
我們定義L(j)是一個優化的子結構,也就是最長遞增子序列。那麼L(j)和L(1..j-1)的關係可以描述成
L(j) =max {L(i), i<j && Ai<Aj } + 1; 也就是說L(j)等於之前所有的L(i)中最大的的L(i)加一。這樣的L(i)需要滿足的條件就是Ai<Aj。這個推斷還是比較容易理解的。就是選擇j之前所有的滿足小於當前數組的最大值。
最後求max(L(j))就是最長遞增子序列,需要注意的是L[len-1]並不一定是最長遞增子序列的長度。
代碼實現
#include<iostream>
#include<stdlib.h>
using namespace std;
#define MaxLen 100
//方法1:通過求有向無環圖的最長路徑來求最長遞增子序列,通過二維數組構建有向無環圖。
int LIS(int arry[],int len,int e[][MaxLen],int L[])
{
int i,j;
//初始化有向圖
for(i=0;i<len;i++)
{
L[i]=1;//初始化L[]
for(j=i+1;j<len;j++)
{
if(arry[i]<arry[j])
e[i][j]=1;
else
e[i][j]=0;
}
}
//轉換爲求有向圖的最長路徑
for(i=0;i<len;i++)
{
for(j=i+1;j<len;j++)
{
if(e[i][j]==1&&L[j]<1+L[i])
{
L[j]=1+L[i];
}
}
}
int max=0;
//根據L[i]求最長遞增子序列
for(int i=0;i<len;i++)
if(L[i]>max)
max=L[i];
return max;//L[len-1]不一定就最長遞增子序列的長度
}
//方法2:
int LIS2(int arry[],int len,int L[])
{
int i,j;
for(i=0;i<len;i++)
{
L[i]=1;//初始化L[]
}
for(i=1;i<len;i++)
{
for(int j=i+1;j<len;j++)
{
if(arry[i]<arry[j]&&L[j]<1+L[i])
L[j]=1+L[i];
}
}
int max=0;
//根據L[i]求最長遞增子序列
for(int i=0;i<len;i++)
if(L[i]>max)
max=L[i];
return max;//L[len-1]不一定就最長遞增子序列的長度
}
//打印最長遞增子序列
void printString(int p[],int k,int arry[])
{
if(p[k]==-1)
return;
printString(p,p[k],arry);
cout<<arry[k];
}
//方法3:
int LIS3(int arry[],int len,int L[])
{
int p[MaxLen];
int i,j;
for(i=0;i<len;i++)
{
L[i]=1;//初始化L[]
p[i]=-1;
}
for(i=1;i<len;i++)
{
for(int j=i+1;j<len;j++)
{
if(arry[i]<arry[j]&&L[j]<1+L[i])
{
L[j]=1+L[i];
p[j]=i;//表示arry[j]的前一個元素使arry[i]。
}
}
}
int max=0;
int k;
//根據L[i]求最長遞增子序列
for(int i=0;i<len;i++)
{
if(L[i]>max)
{
max=L[i];
k=i;
}
}
printString(p,k,arry);
cout<<endl;
return max;//L[len-1]不一定就最長遞增子序列的長度
}
void main()
{
int arry[]={5,2,8,6,3,6,9,7};
int len=sizeof(arry)/sizeof(int);
int e[MaxLen][MaxLen];
int *L=new int[len];
cout<<LIS(arry,len,e,L)<<endl;
cout<<LIS2(arry,len,L)<<endl;
cout<<LIS3(arry,len,L)<<endl;
system("pause");
}
但是上面的 LIS和 LIS2兩種實現都沒有給出遞增子序列本身,只給出了長度。而 LIS3能夠輸出一個子序列。舉例說明其實現方式:
arry[]={5,2,8,9,3}
L[0...i] p[0...i]
1, 1, 1, 1, 1 -1, -1, -1, -1, -1
2, 2 0, 0
2 1
3 2
如上述所示,我們首先初始化數組L[]和p[],其中L用於記錄遞增子序列的長度,而p[]用於回溯遞增子序列,其中p[j]=i表示arry[i]->arry[j]是一個遞增子序列。我們在L[]中能夠求出遞增子序列的長度max以及遞增子序列的末尾元素所在位置k。然後通過k我們回溯數組p,因此這裏使用了遞歸方式打印遞增子序列。
參考:http://www.cnblogs.com/xwdreamer/archive/2011/06/21/2296995.html
http://www.cnblogs.com/biyeymyhjob/archive/2012/09/28/2707343.html