動(dòng)態(tài)規(guī)劃&最長公共子序列

一個(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,則:

  1. 如果xm-1=yn-1,那么zk-1=xm-1=yn-1,并且Zk-1是Xm-1和Yn-1的LCS;
  2. 如果xm-1≠yn-1,那么當(dāng)zk-1≠xm-1時(shí)Z是Xm-1和Y的LCS;
  3. 如果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ī)律!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評(píng)論 6 541
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,324評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,018評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,417評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,783評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,960評(píng)論 0 290
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,522評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,267評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,471評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,698評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評(píng)論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,204評(píng)論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,436評(píng)論 2 378

推薦閱讀更多精彩內(nèi)容