LCSの解き方【競技プログラミング】

# はじめに
- LCS(longest common subsequence)の解法はド定番らしく、逆に丁寧に説明されてないことが多いので、最初、よく分からなかったから、自分で備忘的にまとめることにした。

# LCSとは？
- 文字列SとTの部分文字列の集合のうち、一致する部分文字列の中で最長の長さのものを答えよ、と言うもの。
    - 文字列Sの部分文字列の集合とは、例えば、S="abc"のとき、["abc","ab","ac","bc","a","b","c",""]の8つの文字列となる。
    - つまり、長さNの文字列の時、各文字に対し、残す/残さないの2択が可能なので、$2^N$個の部分文字列を作れる。
- 例えば、S="abcdef"、T="cdabe"であれば、LCSの長さは3となり、LCSは"abe"と"cde"になる。
- 例題としては、[DPまとめコンテストのf問題](https://atcoder.jp/contests/dp/tasks/dp_f)がある。

# 簡単に説明される場合の解法
- dp[i][j]を「文字列Sの文字0..<i」「文字列Tの文字0..<j」によるLCSとする。
```swift
extension Array {
    init(_ cnt: Int, _ v: Element) {
        self = [Element](repeating: v, count: cnt)
    }
    init<T>(_ cnt2:Int, _ cnt1:Int, _ v:T) where Element == [T] {
        self = [[T]](cnt2,[T](cnt1,v))
    }
}

// 準備
var dp = [[Int]](S.count+1,T.count+1,O) // 上記extensionによる省略書式

// 遷移
for i in 0..<S.count {
    for j in 0..<T.count {
        dp[i+1][j+1] = max( // ①
            S[i] == T[j] ? dp[i][j] + 1 : 0 // ②
            dp[i+1][j], // ③
            dp[i][j+1], // ④
  
        )
    }
}

// 答え
print(dp[S.count][T.count])
```
- 上記を見て、一般的なdpと違うのは、①において、遷移後のインデックスが「i+1」だけでなく「j+1」もあるってこと。違和感を感じない人は、まぁ良いけどね。この時点で、「j+1」があること自体は、まぁ問題はない。
- 次に、②を見ると、「あぁ、文字が一致するときは、dpを1足すのね。納得」となり、何の問題はない。
- で、違和感バリバリなのが、③④で、遷移前の状態を表す側のインデックスに「i+1」「j+1」が存在すること。
    - ちょ、待てよ！遷移前に「i+1」「j+1」の情報を保持してるってあり得るの？
    - でも、このコードで[例題](https://atcoder.jp/contests/dp/tasks/dp_f)を問題なく解けてんだよなぁ。でも、納得いかんなぁ。
- で、結局③④について納得感を得るために、コードに確認用のprint文を追加して、中身を若干修正して回してみた。結果が長くなるのは嫌だから、S="abc"、T="zbz"として、LCS="b"（長さ1）となるようにした。
```swift
var dp = [[Int]](S.count+1,T.count+1,8) // 初期値を8とした。なんとなく、縁起が良いから？

// 真の初期値を設定。SかTのどちらかで、1文字もチェックしてなければ、LCSの長さは0になる。
for i in 0...S.count { dp[i][0] = 0 }
for j in 0...T.count { dp[0][j] = 0 }

for i in 0..<S.count {
    for j in 0..<T.count {

        //遷移処理前のdpを表示
        print("処理前：",i,j)
        dp.forEach{
            print($0)
        }    
        
        dp[i+1][j+1] = max( // ①
            S[i] == T[j] ? dp[i][j] + 1 : -1 // ② -- 説明のわかりやすさのため、falseのときは-1にして、max関数で選ばれないようにした。
            dp[i+1][j], // ③
            dp[i][j+1], // ④
        )

        //遷移処理後のdpを表示
        print("処理後：",i,j)
        dp.forEach{
            print($0)
        }  
        
        print() // 見やすさのための改行
        
    }
}

```
- 上記コードを回した結果は以下のとおり。なお、下記の（）内の数値は、dpが上記コードで遷移するインデックス[i+1][j+1]①を指し、丸数値は、上記コードmax関数で採用される値(②③④のどれか)を指す。
```
処理前： 0 0
[0, 0, 0, 0]
[0, 8, 8, 8]
[0, 8, 8, 8]
[0, 8, 8, 8]
処理後： 0 0
[0, 0, 0, 0]
[0, 0, 8, 8] // 8→0 （1，1）③④
[0, 8, 8, 8]
[0, 8, 8, 8]

処理前： 0 1
[0, 0, 0, 0]
[0, 0, 8, 8]
[0, 8, 8, 8]
[0, 8, 8, 8]
処理後： 0 1
[0, 0, 0, 0]
[0, 0, 0, 8] // 8→0（1，2）③④
[0, 8, 8, 8]
[0, 8, 8, 8]

処理前： 0 2
[0, 0, 0, 0]
[0, 0, 0, 8]
[0, 8, 8, 8]
[0, 8, 8, 8]
処理後： 0 2
[0, 0, 0, 0]
[0, 0, 0, 0] // 8→0（1，3）③④
[0, 8, 8, 8]
[0, 8, 8, 8]

処理前： 1 0
[0, 0, 0, 0]
[0, 0, 0, 0]
[0, 8, 8, 8]
[0, 8, 8, 8]
処理後： 1 0
[0, 0, 0, 0]
[0, 0, 0, 0]
[0, 0, 8, 8] // 8→0（2，1）③④
[0, 8, 8, 8]

処理前： 1 1
[0, 0, 0, 0]
[0, 0, 0, 0]
[0, 0, 8, 8]
[0, 8, 8, 8]
処理後： 1 1
[0, 0, 0, 0]
[0, 0, 0, 0]
[0, 0, 1, 8] // 8→1（2，2）②
[0, 8, 8, 8]

処理前： 1 2
[0, 0, 0, 0]
[0, 0, 0, 0]
[0, 0, 1, 8]
[0, 8, 8, 8]
処理後： 1 2
[0, 0, 0, 0]
[0, 0, 0, 0]
[0, 0, 1, 1] // 8→1（2，3）③
[0, 8, 8, 8]

処理前： 2 0
[0, 0, 0, 0]
[0, 0, 0, 0]
[0, 0, 1, 1]
[0, 8, 8, 8]
処理後： 2 0
[0, 0, 0, 0]
[0, 0, 0, 0]
[0, 0, 1, 1]
[0, 0, 8, 8] // 8→0（3，1）③④

処理前： 2 1
[0, 0, 0, 0]
[0, 0, 0, 0]
[0, 0, 1, 1]
[0, 0, 8, 8]
処理後： 2 1
[0, 0, 0, 0]
[0, 0, 0, 0]
[0, 0, 1, 1]
[0, 0, 1, 8] // 8→1（3，2）④

処理前： 2 2
[0, 0, 0, 0]
[0, 0, 0, 0]
[0, 0, 1, 1]
[0, 0, 1, 8]
処理後： 2 2
[0, 0, 0, 0]
[0, 0, 0, 0]
[0, 0, 1, 1]
[0, 0, 1, 1] // 8→1（3，3）③④
```
- こうやって回してみると、真の初期値設定`for i in 0...S.count { dp[i][0] = 0 }`と`for j in 0...T.count { dp[0][j] = 0 }`が効いていることが分かるね。
- この初期値設定が、上記dpのマス目において、上辺と左辺の0を形成し、それが「右③」、「下④」または「右下②（1加算）」に遷移しているから、仮初期値の「8」が全て置き換わった。
- なお、二重forループiとjを逆にしても、「処理後:2 2」のdpは同じになる。
- うむ、頭の回転の衰えてきた僕でも納得した！

# 続き（LCSの文字列の取得）-- 計算量無視版
- 上記までだと、LCSの文字数しか出せなかったね。まぁ、文字列を取得するロジックは簡単で、ちょっと書き換えれば出来上がり。
- 具体的には、dp配列が「文字数」を格納していたところ、「文字」を格納することとなる。
- dpの中身が文字となったことから、元のコードのmax関数で②③④で最長のものを選ぶ部分が、少し面倒くさい。
```swift
var dp = [[String]](S.count+1,T.count+1,"")

for i in 0..<S.count {
    for j in 0..<T.count {

        var longest = dp[i+1][j].count // 元の③に相当
        dp[i+1][j+1] = dp[i+1][j]
        
        if dp[i][j+1].count > longest { // 元の④に相当
            longest = dp[i][j+1].count
            dp[i+1][j+1] = dp[i][j+1]
            
        }
        
        if (S[i] == T[j] ? dp[i][j].count + 1 : 0) > longest { // 元の②に相当
            dp[i+1][j+1] = dp[i][j] + String(S[i])
        }

    }
}

print(dp[S.count][T.count])
```
- 例題の下記入力を与えて回すと...
```
abracadabra // S
avadakedavra // T
```
- 答え（aaadara）と一致したからOK！...って思うじゃん？提出するとTLEなのよね。
- 制約って、S、Tとも3000文字以下。だから、二重forループを回すだけなら$10^7$以下になるはずなんだよね。
- でも、forループの一番内側で行っているのは、文字列操作なんだよね。
- 例えば、`dp[i+1][j+1] = dp[i+1][j]`について言えば、右辺の内容を左辺に上書きしている。この上書きしている内容が整数等であれば大した話ではないが、内容が「文字列」だとすると、その実態は配列だから、配列のコピー作業と考えると、O(文字数)の計算量がかかると言うことだろうか。そう考えると、計算量は$|S|^3$に跳ね上がるから、$9×10^9$になるから、TLEになってもおかしくないね。

# 続き（LCSの文字列の取得）-- 計算量考慮版
- 初心に返って、LCSの文字列の長さは、当初の手法で求めることとする。
```swift
// 準備
var dp = [[Int]](S.count+1,T.count+1,O) // 上記extensionによる省略書式

// 遷移
for i in 0..<S.count {
    for j in 0..<T.count {
        dp[i+1][j+1] = max( // ①
            S[i] == T[j] ? dp[i][j] + 1 : 0 // ②
            dp[i+1][j], // ③
            dp[i][j+1], // ④
  
        )
    }
}

// 答え
print(dp[S.count][T.count])
```
- さて、ここで得た、配列dpを元に、文字を取りだしてみる。
- 答えから書いちゃうと以下のとおり
```swift
var ans = ""

var i = S.count
var j = T.count

while i * j > 0 {
    // (i-1, j) -> (i, j) と更新されていた場合
    if (dp[i][j] == dp[i-1][j]) { i -= 1 }

    // (i, j-1) -> (i, j) と更新されていた場合
    else if (dp[i][j] == dp[i][j-1]) { j -= 1 }

    // (i-1, j-1) -> (i, j) と更新されていた場合
    else {
        ans = String(S[i-1]) + ans // 文字を復元する
        i -= 1 ; j -= 1
    }
}
print(ans)
```
- まぁ、説明しなくても、分かるかな？

# さいごに
- Stringの計算量のせいで、LCSの長さパートと文字列復元パートの2つに別れるのが面倒だね。
- でもまぁ、これは一度覚えれば何とかなるタイプ！
- めでたしめでたし