算法學習——DP(二)

LCS問題(最長公共子序列)

上一篇文章
所有代碼和markdown在chux0519/algs同步更新辆影。

計算LCS長度

遞歸解法

LCS問題是具有最優(yōu)子結(jié)構(gòu)的。
假設長度為m的字符串X[0..m-1]和長度為n的字符串Y[0..n-1],用L(X[0..m-1], Y[0..n-1]) 來表示它們的LCS長度控嗜。則可以得到以下結(jié)論:

  1. 如果X[m-1] = Y[n-1], 那么L(X[0..m-1], Y[0..n-1]) = 1 + L(X[0..m-2], Y[0..n-2])
  2. 如果X[m-1] != Y[n-1], 那么L(X[0..m-1], Y[0..n-1]) = MAX ( L(X[0..m-2], Y[0..n-1]), L(X[0..m-1], Y[0..n-2])

用JS描述如下

function lcs (str1, str2, len1, len2) {
  if (len1 === 0 || len2 === 0) return 0
  if (str1[len1] === str2[len2]) {
    return 1 + lcs(str1, str2, len1 - 1, len2 - 1)
  } else {
    return Math.max(
      lcs(str1, str2, len1 - 1, len2),
      lcs(str1, str2, len1, len2 - 1)
    )
  }
}

進一步分析

假設X="AXYT", Y="AYZX"呢撞,畫出以上代碼的調(diào)用圖,得到:

                         lcs("AXYT", "AYZX")
                       /                 
         lcs("AXY", "AYZX")            lcs("AXYT", "AYZ")
         /                              /               
lcs("AX", "AYZX") lcs("AXY", "AYZ")   lcs("AXY", "AYZ") lcs("AXYT", "AY")

可以看出lcs("AXY", "AYZ")被重復計算了胡本,隨著層數(shù)的增多,可以分析出畸悬,有許多重復子問題會出現(xiàn)侧甫,于是,可以利用記憶化或者制表來進行優(yōu)化算法。

制表解法

// tab
function lcsWithTab (str1, str2, len1, len2) {
  var tab = new Array(len1 + 1).fill([])
  for (var i = 0; i <= len1; i++) {
    for (var j = 0; j <= len2; j++) {
      if (i === 0 || j === 0) {
        tab[i][j] = 0
      } else if (str1[i - 1] === str2[j - 1]) {
        tab[i][j] = tab[i - 1][j - 1] + 1
      } else {
        tab[i][j] = Math.max(
          tab[i - 1][j],
          tab[i][j - 1]
        )
      }
    }
  }
  return tab[len1][len2]
}

制表解法披粟,通過定義好基線條件tab[i][j] = 0 where i ==0 || j == 0咒锻,自底向上計算出結(jié)果,提升速度的同時消除了遞歸守屉。

回溯LCS

存儲回溯數(shù)組

在上述算法中惑艇,增加一個數(shù)組用于存儲回溯路徑即可,稍作改動如下拇泛。

function LCS (str1, str2, len1, len2) {
  var tab = []
  var back = []
  for (var m = 0; m <= len1; m++) {
    tab[m] = []
    back[m] = []
    for (var n = 0; n <= len2; n++) {
      tab[m][n] = null
      back[m][n] = null
    }
  }

  for (var i = 0; i <= len1; i++) {
    for (var j = 0; j <= len2; j++) {
      if (i === 0 || j === 0) {
        tab[i][j] = 0
        back[i][j] = null
      } else if (str1[i - 1] === str2[j - 1]) {
        tab[i][j] = tab[i - 1][j - 1] + 1
        back[i][j] = '↖'
      } else if (tab[i - 1][j] > tab[i][j - 1]) {
        tab[i][j] = tab[i - 1][j]
        back[i][j] = '↑'
      } else if (tab[i - 1][j] === tab[i][j - 1]) {
        tab[i][j] = tab[i - 1][j]
        back[i][j] = '←/↑'
      } else {
        tab[i][j] = tab[i][j - 1]
        back[i][j] = '←'
      }
    }
  }
  return { tab: tab, bt: back }
}

運行

var str1 = 'GAC'
var str2 = 'AGCAT'
var result = LCS(str1, str2, str1.length, str2.length)
console.log(result.tab)
console.log(result.bt)

可以得到如下輸出:

[ [ 0, 0, 0, 0, 0, 0 ],
  [ 0, 0, 1, 1, 1, 1 ],
  [ 0, 1, 1, 1, 2, 2 ],
  [ 0, 1, 1, 2, 2, 2 ] ]
[ [ null, null, null, null, null, null ],
  [ null, '←/↑', '↖', '←', '←', '←' ],
  [ null, '↖', '←/↑', '←/↑', '↖', '←' ],
  [ null, '↑', '←/↑', '↖', '←/↑', '←/↑' ] ]

tab數(shù)組的輸出為LCS的長度敦捧,?bt數(shù)組的內(nèi)容是回溯的方向,例子取自維基百科碰镜。

輸出一個LCS

輸出LCS結(jié)果兢卵,需要利用bt數(shù)組進行回溯,只輸出一個LCS時绪颖,可以用以下回溯函數(shù)秽荤。

function backtrace (bt, str1, str2, i, j) {
  if (i === 0 || j === 0) {
    return ''
  } else if (bt[i][j] === '↖') {
    return backtrace(bt, str1, str2, i - 1, j - 1) + str1[i]
  } else {
    if (bt[i][j] === '←') {
      return backtrace(bt, str1, str2, i, j - 1)
    } else {
      return backtrace(bt, str1, str2, i - 1, j)
    }
  }
}
console.log(backtrace(result.bt, str1, str2, str1.length, str2.length))

得到輸出AC

實際上不用bt數(shù)組,直接使用保存LCS長度的tab數(shù)組也能直接回溯柠横。稍微修改backtrace的判斷條件即可窃款。如下:

function backtraceByTab (tab, str1, str2, i, j) {
  if (i === 0 || j === 0) {
    return ''
  } else if (str1[i - 1] === str2[j - 1]) {
    return backtraceByTab(tab, str1, str2, i - 1, j - 1) + str1[i]
  } else {
    if (tab[i][j - 1] > tab[i - 1][j]) {
      return backtraceByTab(tab, str1, str2, i, j - 1)
    } else {
      return backtraceByTab(tab, str1, str2, i - 1, j)
    }
  }
}
console.log(backtraceByTab(result.tab, str1, str2, str1.length, str2.length))

得到輸出仍舊為AC


20170906 update

輸出所有LCS

輸出所有LCS結(jié)果,在這里使用保存LCS長度的tab數(shù)組牍氛。
wiki給出的偽代碼如下

function backtrackAll(C[0..m,0..n], X[1..m], Y[1..n], i, j)
    if i = 0 or j = 0
        return {""}
    else if X[i] = Y[j]
        return {Z + X[i] for all Z in backtrackAll(C, X, Y, i-1, j-1)}
    else
        R := {}
        if C[i,j-1] ≥ C[i-1,j]
            R := R ∪ backtrackAll(C, X, Y, i, j-1)
        if C[i-1,j] ≥ C[i,j-1]
            R := R ∪ backtrackAll(C, X, Y, i-1, j)
        return R

使用JS實現(xiàn)如下

// backtrace all
function backtraceAllByTab (tab, str1, str2, i, j) {
  function _backtraceAllByTab (tab, str1, str2, i, j) {
    if (i === 0 || j === 0) {
      return ['']
    } else if (str1[i - 1] === str2[j - 1]) {
      return [].map.call(_backtraceAllByTab(tab, str1, str2, i - 1, j - 1), each => each + str1[i - 1])
    } else {
      var r = []
      if (tab[i][j - 1] >= tab[i - 1][j]) {
        r = r.concat(_backtraceAllByTab(tab, str1, str2, i, j - 1)) // 本應該求并集晨继,這里直接連接起來,最后去重一次
      }
      if (tab[i - 1][j] >= tab[i][j - 1]) {
        r = r.concat(_backtraceAllByTab(tab, str1, str2, i - 1, j)) // 本應該求并集搬俊,這里直接連接起來紊扬,最后去重一次
      }
      return r
    }
  }
  return Array.from(new Set(_backtraceAllByTab(tab, str1, str2, i, j))) // 去重
}

調(diào)用

var str1 = 'GAC'
var str2 = 'AGCAT'
console.log(backtraceAllByTab(result.tab, str1, str2, str1.length, str2.length))

輸出

[ 'AC', 'GC', 'GA' ]

值得注意的是,輸出所有的LCS是不保證時間復雜度為多項式復雜度的唉擂,如果兩個字符串比較接近餐屎,那么可能每一步都會有分枝。

輸出diff

這里JS實現(xiàn)完全參考wiki玩祟,等號的判定條件放在>=中腹缩,若將其替換為'>',則diff的輸出可能不同空扎。

function printDiff (tab, str1, str2, i, j) {
  if (i > 0 && j > 0 && str1[i - 1] === str2[j - 1]) {
    printDiff(tab, str1, str2, i - 1, j - 1)
    console.log(`  ${str1[i - 1]}`)
  } else if (j > 0 && tab[i][j - 1] >= tab[i - 1][j]) {
    printDiff(tab, str1, str2, i, j - 1)
    console.log(`+ ${str2[j - 1]}`)
  } else if (i > 0) {
    printDiff(tab, str1, str2, i - 1, j)
    console.log(`- ${str1[i - 1]}`)
  } else {
    console.log('')
  }
}

調(diào)用

var str1 = 'GAC'
var str2 = 'AGCAT'
printDiff(result.tab, str1, str2, str1.length, str2.length)

輸出

- G
  A
+ G
  C
+ A
+ T

思考——算法優(yōu)化

  • 使用trim

    在字符串長度很長時藏鹊,記錄LCS的長度的表會非常占用空間,因此如果兩個字符串有很多類似的部分转锈,可以對首尾相同的部分進行跳過盘寡,從而縮短要進行比較的部分,達到優(yōu)化的目的黑忱。例如wiki中給出的偽代碼宴抚。

    function LCS(X[1..m], Y[1..n])
      start := 1
      m_end := m
      n_end := n
      trim off the matching items at the beginning
      while start ≤ m_end and start ≤ n_end and X[start] = Y[start]
          start := start + 1
      trim off the matching items at the end
      while start ≤ m_end and start ≤ n_end and X[m_end] = Y[n_end]
          m_end := m_end - 1
          n_end := n_end - 1
      C = array(start-1..m_end, start-1..n_end)
      only loop over the items that have changed
      for i := start..m_end
          for j := start..n_end
              the algorithm continues as before ...
    
  • 減少比較次數(shù)

    在上述的算法中,我們進行的比較是逐字符比較的甫煞,而實際的應用中菇曲,我們通常是采用逐行比較的方式,將每一行看作是一個元素來進行比較抚吠,從而到達減少比較次數(shù)的目的常潮。

  • 縮短字符串長度

    在使用了上面的方法后,將每一行(字符串)看作元素楷力,例如在比較源碼異同時喊式,通常一行有多余60個的字符,這時利用哈希或是check sum通诚舫可以將長度縮短到8-40個字符岔留。但是,這種做法還是有一些弊端检柬。

    • 首先献联,哈希或是check sum的計算會額外的需要一部分時間何址。
    • 其次里逆,哈希或是check sum的計算會額外的需要一部分空間用爪。
    • 上述兩點雖說有些耗時原押,但是比起逐字符比較,這樣的代價其實很小偎血。
    • 最后一點真正弊端是诸衔,字符串的哈希可能導致碰撞(不同的字符串產(chǎn)生相同的哈希)颇玷,一旦發(fā)生碰撞署隘,這會使結(jié)果不正確。但是亚隙,這樣的情況仍是有解決辦法的(例如對哈希進行再加密等等)磁餐。
  • Hirschberg's算法

    這里提一下Hirschberg's algorithm,這個算法可以將節(jié)點消耗的內(nèi)存降低到min(m,n)+1,但是會相應略微的增加一部分時間復雜度(仍是平方時間復雜度)阿弃。

  • 使用更高級的算法

    LCS的DP解法復雜度時平方時間诊霹,理論上應該也不能在高了,但是同樣的時間復雜度渣淳,在應用中的實際平均耗時是有區(qū)別的脾还。這里mark一篇論文,后續(xù)可能會繼續(xù)寫相關LCS的文章入愧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鄙漏,一起剝皮案震驚了整個濱河市嗤谚,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌怔蚌,老刑警劉巖巩步,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異桦踊,居然都是意外死亡椅野,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進店門籍胯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來竟闪,“玉大人,你說我怎么就攤上這事杖狼×陡颍” “怎么了?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵蝶涩,是天一觀的道長鲸湃。 經(jīng)常有香客問我,道長子寓,這世上最難降的妖魔是什么暗挑? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮斜友,結(jié)果婚禮上炸裆,老公的妹妹穿的比我還像新娘。我一直安慰自己鲜屏,他們只是感情好烹看,可當我...
    茶點故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著洛史,像睡著了一般惯殊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上也殖,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天土思,我揣著相機與錄音,去河邊找鬼忆嗜。 笑死己儒,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的捆毫。 我是一名探鬼主播闪湾,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼绩卤!你這毒婦竟也來了途样?” 一聲冷哼從身側(cè)響起江醇,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎何暇,沒想到半個月后陶夜,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡赖晶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了辐烂。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片遏插。...
    茶點故事閱讀 40,110評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖纠修,靈堂內(nèi)的尸體忽然破棺而出胳嘲,到底是詐尸還是另有隱情,我是刑警寧澤扣草,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布了牛,位于F島的核電站,受9級特大地震影響辰妙,放射性物質(zhì)發(fā)生泄漏鹰祸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一密浑、第九天 我趴在偏房一處隱蔽的房頂上張望蛙婴。 院中可真熱鬧,春花似錦尔破、人聲如沸街图。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽餐济。三九已至,卻和暖如春胆剧,著一層夾襖步出監(jiān)牢的瞬間絮姆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工秩霍, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留滚朵,地道東北人。 一個月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓前域,卻偏偏與公主長得像辕近,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子匿垄,可洞房花燭夜當晚...
    茶點故事閱讀 45,047評論 2 355

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