一個(gè)字符串的子串是字符串中連續(xù)的一個(gè)序列,而一個(gè)字符串的子序列是字符串中保持相對(duì)位置的字符序列,譬如,"adi"可以使字符串"abcdefghi"的子序列但不是子串。這也就決定了在解這兩種"LCS"問題上的一些區(qū)別。
Longest-Common-Substring和Longest-Common-Subsequence是不一樣的。
參考:
wiki-動(dòng)態(tài)規(guī)劃
何海濤微博
動(dòng)態(tài)規(guī)劃DP
通過把原問題分解為相對(duì)簡單的子問題的方式求解復(fù)雜問題的方法。
動(dòng)態(tài)規(guī)劃常常適用于有[重疊子問題]和[最優(yōu)子結(jié)構(gòu)]性質(zhì)的問題,動(dòng)態(tài)規(guī)劃方法所耗時(shí)間往往遠(yuǎn)少于樸素解法。
動(dòng)態(tài)規(guī)劃背后的基本思想非常簡單。大致上,若要解一個(gè)給定問題,我們需要解其不同部分(即子問題),再合并子問題的解以得出原問題的解。
通常許多子問題非常相似,為此動(dòng)態(tài)規(guī)劃法試圖僅僅解決每個(gè)子問題一次,從而減少計(jì)算量:一旦某個(gè)給定子問題的解已經(jīng)算出,則將其[記憶化]存儲(chǔ),以便下次需要同一個(gè)子問題解之時(shí)直接查表。這種做法在重復(fù)子問題的數(shù)目關(guān)于輸入的規(guī)模呈[指數(shù)增長]時(shí)特別有用。
DP問題有幾個(gè)典型應(yīng)用:
解整數(shù)背包問題: 設(shè)有n件物品,每件價(jià)值記為Pi,每件體積記為Vi,用一個(gè)最大容積為Vmax的背包,求裝入物品的最大價(jià)值。 用一個(gè)數(shù)組f[i,j]表示取i件商品填充一個(gè)容積為j的背包的最大價(jià)值,顯然問題的解就是f[n,Vmax].
f[i,j]=
f[i-1,j] {j<Vi}
max{f[i-1,j],f[i,j-Vi]+Pi} {j>=Vi}
0 {i=0 OR j=0}
對(duì)于特例01背包問題(即每件物品最多放1件,否則不放入)的問題,狀態(tài)轉(zhuǎn)移方程:
f[i,j]=
f[i-1,j] {j<Vi}
max{f[i-1,j],f[i-1,j-Vi]+Pi} {j>=Vi}
0 {i=0 OR j=0}
WIKI上舉的第一個(gè)例子是Fibonacci數(shù)列,普通的遞歸求法會(huì)重復(fù)計(jì)算很多次前面的值因而效率很低,所以我們可以從低位算起。這樣就可以利用前面的值。
到目前為止我對(duì)DP的理解就是,每一步的結(jié)果都與上一步有關(guān)。
LCS(longest common subsequence)
我們有兩個(gè)字符串,現(xiàn)在求兩個(gè)字符串的最長公共子序列(注:這里的要求不包括字符必須連續(xù))
例:abdhgf和dadchgm的LCS就是adhg
這種問題確實(shí)不好做,一般的思路解決太復(fù)雜了。如果用遍歷的方式,不想等的時(shí)候往前移動(dòng),相等的話,看后面的是否相等,后面的一個(gè)也存在之前的情況。。
但是我們也可以假設(shè)我們已經(jīng)有一段字符串滿足相同的子序列了,那么我們關(guān)心當(dāng)前的這一個(gè)就可以了。
我們假設(shè)兩個(gè)字符串的長度為m,n;LCS的長度為k;并且假設(shè)LCS里面的所有字符都是滿足條件的
并且假設(shè)前k個(gè)都滿足情況了,我們討論第k個(gè):
設(shè)Xm={x0,x1,…xm-1}和Yn={y0,y1,…,yn-1}為兩個(gè)字符串,而Zk={z0,z1,…zk-1}是它們的LCS,則:
- 如果xm-1=yn-1,那么zk-1=xm-1=yn-1,并且Zk-1是Xm-1和Yn-1的LCS;
- 如果xm-1≠yn-1,那么當(dāng)zk-1≠xm-1時(shí)Z是Xm-1和Y的LCS;
- 如果xm-1≠yn-1,那么當(dāng)zk-1≠yn-1時(shí)Z是Yn-1和X的LCS
注:這個(gè)關(guān)系大家多想一想,是最重要的部分。
如果覺得上面的文字有干擾的話,可以自己去理解。
上面的關(guān)系式實(shí)際上是逆推,即我們假設(shè)已經(jīng)有了LCS,我們?nèi)フ襆CS在兩個(gè)字符串里面的位置。
- 我覺得很重要的一點(diǎn)就是,我們只關(guān)心這一步(和上一步的關(guān)系),至于上一步也不滿足的話,那就是遞歸的事情了。這樣想能簡化思路!
**性質(zhì)大家可以反證法證明一下,如果不能理解,可以舉個(gè)例子試一下,LCS里面的字符肯定會(huì)出現(xiàn)在x,y里面,如果x,y的最后面是無關(guān)的字符,后面的兩個(gè)條件就可以逐步把無關(guān)的刪除掉;
其實(shí)后面兩個(gè)條件就是去掉無關(guān)字符的過程,討論的都是當(dāng)x!=y的情況。
**
由上面的三種情況:
我們可以得出如下的思路:求兩字符串Xm={x0, x1,…xm-1}和Yn={y0,y1,…,yn-1}的LCS,
- 如果xm-1=yn-1, 那么只需求得Xm-1和Yn-1的LCS, 并在其后添加xm-1( yn-1) 即可;
- 如果xm-1≠yn-1, 我們分別求得Xm-1和Y的LCS和Yn-1和X的LCS,并且這兩個(gè)LCS中較長的一個(gè)為X和Y的LCS
這就是DP的特點(diǎn)吧,每一步的情況都有兩種(多種),你看著辦吧。
如果我們記字符串Xi和Yj的LCS的長度為c[i,j],我們可以遞歸地求c[i,j]:
0, if i<0 or j<0
c[i-1,j-1]+1 ,if i,j>=0 and xi=xj
max(c[i,j-1],c[i-1,j] if i,j>=0 and xi≠xj
根據(jù)這個(gè)思路,我們創(chuàng)建一個(gè)矩陣lcs_length來記錄對(duì)應(yīng)的i,j的值,之所以這樣是為了避免類似于Fibonacci里面的重復(fù)求值的問題,以及方便輸出。
在下面的代碼里面,還創(chuàng)建了一個(gè)lcs_dir的矩陣,這個(gè)也是為了保存每一次值的來源,方便我們打印的時(shí)候知道取哪個(gè)值。
- 這個(gè)程序的主干部分是這樣的:
矩陣的橫列是str2,豎列是str1
結(jié)合上面的實(shí)例,我們得到的矩陣是這樣的:
str| a |b|d|h|g|f
----|------|----|---|----|
d | 0|0|1|0|0|0
a | 1|1 |1 |1 |1 | 1
d |1| 1|2 |2 |2 |2
c|0| 1| 2|2 |2 |2
h|0| 1|2 |3 | 3|3
g| 0| 1| 2| 3| 4|4
下面是代碼:
//c[i,k]=
// 0, if i<0 or j<0
// c[i - 1, j - 1] + 1, if i, j >= 0 and xi = xj
// max(c[i, j - 1], c[i - 1, j] if i, j >= 0 and xi≠xj
#include <iostream>
enum dir {kinit=0,kup,kleftup,kleft};//c[i,j] comes from 3 directions
//we have a matrix holding value,another holding the direction
int lcs(char* str1,char* str2)
{
if (!str1 || !str2)
return;
int len1 = strlen(str1);
int len2 = strlen(str2);
if (!len1 || !len2)
return 0;
unsigned int i, j;
int** lcs_len = (int**)(new int[len1]);
for (i = 0; i < len1; i++)
lcs_len[i] = (int*)new int[len2];
for (i = 0; i < len1; i++)
for (j = 0; j < len2; j++)
lcs_len[i][j] = 0;
int** lcs_dir = (int**)(new int[len1]);
for (i = 0; i < len1; i++)
lcs_dir[i] = (int*)new int[len2];
for (i = 0; i < len1; i++)
for (j = 0; j < len2; j++)
lcs_dir[i][j] =kinit ;
//core: detect every unit
for (i = 0; i < len1; i++)
for (j = 0; j < len2; j++)
{
if (i == 0 || j == 0)//the begin of common string
{
if (str1[i] == str2[j])
{
lcs_len[i][j] = 1;
lcs_dir[i][j] = kleftup;
}
else
lcs_len[i][j] = 0;
}
else if (str1[i] == str2[j])
{
lcs_len[i][j] = lcs_len[i - 1][j - 1] + 1;//case 1
lcs_dir[i][j] = kleftup;
}
else if (lcs_len[i - 1][j] > lcs_len[i][j - 1])
{
lcs_len[i][j] = lcs_len[i - 1][j] ;
lcs_dir[i][j] = kup;
}
else
{
lcs_len[i][j] = lcs_len[i][j-1] ;
lcs_dir[i][j] = kleft;
}
}
return lcs_len[len1 - 1][len2 - 1];
}
然后我們根據(jù)得到的矩陣打印:
只需要打印方向矩陣?yán)锩妫较驑?biāo)識(shí)為leftup的字符,其它的根據(jù)方向標(biāo)識(shí)來移動(dòng)。
既然這樣的話,我們可以用遞歸的方式打印,簡化代碼量:
//only lcs_dir=kleftup are to be printed
void print_path(int**lcs_dir, char* str1, char* str2, size_t row, size_t col)
{
if (!str1 || !str2)
return;
size_t len1 = strlen(str1);
size_t len2 = strlen(str2);
if (len1 == 0 || len2 == 0 || !(row < len1&&col < len2))
return;
if (lcs_dir[row][col] == kleftup)
{
if (row > 0 && col > 0)
print_path(lcs_dir, str1, str2, row - 1, col - 1);
std::cout << str1[row];
}
if (lcs_dir[row][col]==kleft)
print_path(lcs_dir, str1, str2, row , col - 1);
if (lcs_dir[row][col] == kup)
print_path(lcs_dir, str1, str2, row-1, col );
}
測試了一下沒問題的。
總結(jié)一下吧,關(guān)于動(dòng)態(tài)規(guī)劃這個(gè)概念大家不要太糾結(jié),其實(shí)重心在于如何找出規(guī)律!