まずはこのステップを実行してみて - 文字列比較アルゴリズムの効率UPと実装トラブル減に役立つヒント集
- 最初に文字列長を比較して一致しなければ即終了させる
不要な詳細比較を最大100%カットできパフォーマンス大幅向上
- 10回以上の連続判定が必要ならKMP法など部分マッチテーブルを活用する
無駄な再比較が減り処理時間が半分以下になることも
- *等価*・*順序*両方使う場合は小さい方の長さでforループ回数を事前決定
`どちらか短い側`基準で進めば誤検出や計算ロス防止につながる
- *空文字・記号含むケースは必ず全体の5%以上でテストデータ生成*
実装バグや例外発生率が3割以上下げられる可能性高い
文字列比較の前に考えること、意外な落とし穴
# DP With Google Sheets - Longest Common Subsequence
あー、これ、コーディング面接でよく出るやつだよね。うん…なんか、「またか」ってちょっと思ったりもするんだけど。でも、まあ重要なんだろうなぁ。さて、本題だけど…あ、今カフェの隣の席が急にざわめいてて気になったけど、戻るわ。
さてさて、質問。2つの文字列が与えられた場合、その最長共通部分列の長さを求めてくださいとのこと。例えば例1――`abcde` と `race` なら、最長共通部分列は `ace` だから3になるんだよね。ふーん…。何か直感的にも納得できる。
次に例2ね。`abcde` と `acbe` のときはどうかというと、この場合も `ace` または `abe` が該当するから3を返す、と。でも一瞬「他にもある?」って考えちゃったけど、結局3で合ってるみたい。不思議とこういう問題を見ると昔読んだアルゴリズム本のページを思い出しそうになるけど…いや、今それはいらないか。
で、大切なのは注意点かな。部分列というのは文字自体が順番通り並ぶ必要があるものの、それぞれ連続している必要はないということなんだよね。例えばさ、《xaxbxc》みたいな文字列で考えても、《abc》は有効な部分列として認識される、と…。ま、いいか。
---
## ブルートフォース法1
…ここまで説明してきたけど、「ブルートフォース法」って聞くとなんとなく頭痛くなる日もあるよね。本当に全部試す方法ってシンプルだけど意外と時間食うしさ。でもまあ、一応紹介しておくべきなんだろうなぁ…。
あー、これ、コーディング面接でよく出るやつだよね。うん…なんか、「またか」ってちょっと思ったりもするんだけど。でも、まあ重要なんだろうなぁ。さて、本題だけど…あ、今カフェの隣の席が急にざわめいてて気になったけど、戻るわ。
さてさて、質問。2つの文字列が与えられた場合、その最長共通部分列の長さを求めてくださいとのこと。例えば例1――`abcde` と `race` なら、最長共通部分列は `ace` だから3になるんだよね。ふーん…。何か直感的にも納得できる。
次に例2ね。`abcde` と `acbe` のときはどうかというと、この場合も `ace` または `abe` が該当するから3を返す、と。でも一瞬「他にもある?」って考えちゃったけど、結局3で合ってるみたい。不思議とこういう問題を見ると昔読んだアルゴリズム本のページを思い出しそうになるけど…いや、今それはいらないか。
で、大切なのは注意点かな。部分列というのは文字自体が順番通り並ぶ必要があるものの、それぞれ連続している必要はないということなんだよね。例えばさ、《xaxbxc》みたいな文字列で考えても、《abc》は有効な部分列として認識される、と…。ま、いいか。
---
## ブルートフォース法1
…ここまで説明してきたけど、「ブルートフォース法」って聞くとなんとなく頭痛くなる日もあるよね。本当に全部試す方法ってシンプルだけど意外と時間食うしさ。でもまあ、一応紹介しておくべきなんだろうなぁ…。
力技で全部列挙?最初は無理やりでもいいかも
1. まず、`a`のすべての部分列を生成するんだけど…(O(2^M) 時間かかるから、まあ現実的にはちょっと気が重いよね)。うーん、とにかく全部作るしかない。
2. 次に、`b`についても同じように全部の部分列を出してみる(O(2^N) 時間だし…こう書いてて思ったけど本当に大丈夫かな?不安になるわけで)。えっと、それでも進めるしかないって自分をなだめつつ。
3. それで、`a`と`b`がそれぞれ持っている部分列全体、その組み合わせ一個ずつ比較することになる(O(2^M) × O(2^N)、数字を見るだけで頭痛くなる気配)。ああ、ここまでやっておいて何なんだけど、本当にこんな力技やりたくはないよね。でも手順としては外せないか。
4. 部分列どうしが等しい場合には、その長さを見て、一番長いやつだけ記録しておく感じかな。ま、いいか。このへん地味な作業っぽいけど仕方なくて…。
5. 最後には、その中で最大だった長さ――つまり最長のやつ――だけ返す流れになるんじゃないかな。
とはいえだよ、この方法そのまま答えに使ったら計算量として O(2^M) * O(2^N) という巨大な数字になってしまう。正直言えばまともに動きそうもなくて、不吉な予感しかしない。
## この問題への「ざっくりした」直観
動的計画法で解決する話に入る前に……ちょっと先回りして想像してみたい時あるじゃん?小さいケースとか考えてみたりして。いや、脇道それちゃったけど、ともかく準備運動的な意味合いもあるので、一旦そこから始めようと思います。
2. 次に、`b`についても同じように全部の部分列を出してみる(O(2^N) 時間だし…こう書いてて思ったけど本当に大丈夫かな?不安になるわけで)。えっと、それでも進めるしかないって自分をなだめつつ。
3. それで、`a`と`b`がそれぞれ持っている部分列全体、その組み合わせ一個ずつ比較することになる(O(2^M) × O(2^N)、数字を見るだけで頭痛くなる気配)。ああ、ここまでやっておいて何なんだけど、本当にこんな力技やりたくはないよね。でも手順としては外せないか。
4. 部分列どうしが等しい場合には、その長さを見て、一番長いやつだけ記録しておく感じかな。ま、いいか。このへん地味な作業っぽいけど仕方なくて…。
5. 最後には、その中で最大だった長さ――つまり最長のやつ――だけ返す流れになるんじゃないかな。
とはいえだよ、この方法そのまま答えに使ったら計算量として O(2^M) * O(2^N) という巨大な数字になってしまう。正直言えばまともに動きそうもなくて、不吉な予感しかしない。
## この問題への「ざっくりした」直観
動的計画法で解決する話に入る前に……ちょっと先回りして想像してみたい時あるじゃん?小さいケースとか考えてみたりして。いや、脇道それちゃったけど、ともかく準備運動的な意味合いもあるので、一旦そこから始めようと思います。

ゼロから始めるアイデア出し、空文字どうする?
### ケース 0 - 空文字列
ああ、空の文字列 `""` が与えられて、しかももう一方はどんな文字列でもいいって状況…これは何回考えても結果は変わらなくて、出力はきっちり 0 になるんだよね。なんか拍子抜けするくらい単純。ま、それだけの話。
### ケース 1 - 一文字の単語
`a` と `a` を比べるとき、最長共通部分列(LCS)の長さが 1。まあ当然すぎてツッコミどころないな…。逆に `a` と `b` の場合だとさ、実は最長共通部分列は全く成立しなくて、その結果として長さはやっぱり 0。うーん、この辺りで悩む人いるかな?たぶん少ない気がする。でも自分も昔ちょっと混乱した記憶あるから油断できないなぁ。
### ケース 2 - 複数文字の単語で、最初の文字が同じ場合
例えば `abcde` と `ace` が与えられてるケースだね。一番最初の文字が両方とも `a` で一致してるところから始まって…おっと脇道に逸れるけど、こういう偶然同じパターンを見ると妙に嬉しいの自分だけ?まあ、それはさておき、「lcss("abcde", "ace")」の場合にはまず頭の一字分「1」足される。それで残った部分、「bcde」と「ace」で lcss を続行する感じになる。こんな風につながっていく計算過程って意外と地味だけど重要だったりする。不思議な感覚。
### ケース 3 - 複数文字の単語で、最初の文字が異なる場合
今度は `abcde` と `race` を比較すると…はい、一目瞭然というか最初から違うじゃん!みたいな展開なんだよな。【注意事項】—いやこれ違う、本題戻そう。この時点では何も一致しないことになるので、とりあえず次どう進めばいいか迷いそう。でも冷静になれば、大丈夫、その時点では一致なしと判断できる。そしてこの処理を積み重ねていくことで全体像が見えてくるんだろうね、多分。
ああ、空の文字列 `""` が与えられて、しかももう一方はどんな文字列でもいいって状況…これは何回考えても結果は変わらなくて、出力はきっちり 0 になるんだよね。なんか拍子抜けするくらい単純。ま、それだけの話。
### ケース 1 - 一文字の単語
`a` と `a` を比べるとき、最長共通部分列(LCS)の長さが 1。まあ当然すぎてツッコミどころないな…。逆に `a` と `b` の場合だとさ、実は最長共通部分列は全く成立しなくて、その結果として長さはやっぱり 0。うーん、この辺りで悩む人いるかな?たぶん少ない気がする。でも自分も昔ちょっと混乱した記憶あるから油断できないなぁ。
### ケース 2 - 複数文字の単語で、最初の文字が同じ場合
例えば `abcde` と `ace` が与えられてるケースだね。一番最初の文字が両方とも `a` で一致してるところから始まって…おっと脇道に逸れるけど、こういう偶然同じパターンを見ると妙に嬉しいの自分だけ?まあ、それはさておき、「lcss("abcde", "ace")」の場合にはまず頭の一字分「1」足される。それで残った部分、「bcde」と「ace」で lcss を続行する感じになる。こんな風につながっていく計算過程って意外と地味だけど重要だったりする。不思議な感覚。
### ケース 3 - 複数文字の単語で、最初の文字が異なる場合
今度は `abcde` と `race` を比較すると…はい、一目瞭然というか最初から違うじゃん!みたいな展開なんだよな。【注意事項】—いやこれ違う、本題戻そう。この時点では何も一致しないことになるので、とりあえず次どう進めばいいか迷いそう。でも冷静になれば、大丈夫、その時点では一致なしと判断できる。そしてこの処理を積み重ねていくことで全体像が見えてくるんだろうね、多分。
一文字ずつ見てみたら…単純だけど奥深い話
1文字ずつ見ていくと、うーん…たぶん `lcss("abcde", "race")` の出力ってさ、なんかこう2パターン考えられる気がしてきた。いや、ちょっと待てよ?大きい方を選ぶ必要があるのか…。つまり `lcss("bcde", "race")` か、それとも `lcss("abcde", "ace")` のどちらか大きいほうになるっぽい。まあ、このへんは後でまた戻るとして。
## Google Sheetsを使った動的計画法アプローチ(ボトムアップ)
まず最初に両方の単語用グリッドを作るわけだけど——ああ、そういえば昨日Google Sheets固まって焦ったな……まあ今は関係ないや、とにかく
! []
それぞれのマス目が何を表しているのかなあと少し考えてみよう。うーむ…まあ感覚的には進めば分かるから深く悩みすぎても仕方ないね。
! []
上記で緑色になっているマスは要するに……じゃなくて、「`lcss("race", "abcde")`」の出力が表示されている部分だと思う、多分。
! []
ちなみに「`lcss("race", "bcde")`」も出力できる場所があるし——話飛びそうだから元に戻すけど、
! []
それから「`lcss("ace", "bcde")`」も同じようにちゃんと確認しておいたほうがいい。【注意事項】
## Google Sheetsを使った動的計画法アプローチ(ボトムアップ)
まず最初に両方の単語用グリッドを作るわけだけど——ああ、そういえば昨日Google Sheets固まって焦ったな……まあ今は関係ないや、とにかく
! []
それぞれのマス目が何を表しているのかなあと少し考えてみよう。うーむ…まあ感覚的には進めば分かるから深く悩みすぎても仕方ないね。
! []
上記で緑色になっているマスは要するに……じゃなくて、「`lcss("race", "abcde")`」の出力が表示されている部分だと思う、多分。
! []
ちなみに「`lcss("race", "bcde")`」も出力できる場所があるし——話飛びそうだから元に戻すけど、
! []
それから「`lcss("ace", "bcde")`」も同じようにちゃんと確認しておいたほうがいい。【注意事項】

頭が同じなら進め!違ったら分岐する謎ルート
[[] ^ `lcss("e", "e")` の出力 ! [] ここにあるすべてのゼロは、片方が空文字列のとき、つまり `lcss(whatever, "")` の結果を意味してる。ああ、なんか最初から数字ゼロだらけで目が疲れるんだけど…ま、仕方ないか。えっと、とりあえずテーブルを下から上へボトムアップ方式で埋めていくしかないよね。
### lcss("e", "e") を求める ! []
おや、「e」がちゃんと一致したっぽい。でも、こんな簡単なケースでもなんとなく戸惑ってしまう。実際には同じ文字「e」が並んでいるわけだから…。ま、ともかく一つ進めてみようかな。
### lcss("e", "e") を求める ! []
おや、「e」がちゃんと一致したっぽい。でも、こんな簡単なケースでもなんとなく戸惑ってしまう。実際には同じ文字「e」が並んでいるわけだから…。ま、ともかく一つ進めてみようかな。
グリッドを作って埋める準備、本当に地味な手順
したがって、`lcss("e", "e")`(緑)は `1 + lcss("", "")`(赤)と結局同じことになるんだよね。つまりね、これは単純に1になる。まあ、ここで特に妙な仕掛けはないし…なんというか、やっぱり計算としては当然の流れかな。ああ、それよりも思ったほど計算式が複雑じゃなくて少し拍子抜けだな、と今更ながら感じている自分がいる。でも本筋に戻ろう。
### lcss("e", "de") を求める
![]
えっと、このケースでは一致する文字が一つも無いわけで…。だからこそ、`lcss("e", "de")`は `lcss("e", "e")` それから `lcss("", "de")`(赤)の中で大きい方を選ぶしかないのよね。本当はどちらもダメそうに見えて実際には `lcss("e", "e")` が答えとしてすぐ浮かんでくる。この辺り繰り返し考えると、何度やっても地味に混乱しそうだけど。
### lcss("e", "cde") を求める
![]
この場合でもさ、一致している文字なんて全然見当たらないんだよなぁ…。ほんと不思議なくらい。でもたぶんこれって仕様通りなんだろう、と諦めモードになったりもする。いや待て、自分何言ってたっけ?一度頭を整理して…よし、本筋へ戻ろう。
### lcss("e", "de") を求める
![]
えっと、このケースでは一致する文字が一つも無いわけで…。だからこそ、`lcss("e", "de")`は `lcss("e", "e")` それから `lcss("", "de")`(赤)の中で大きい方を選ぶしかないのよね。本当はどちらもダメそうに見えて実際には `lcss("e", "e")` が答えとしてすぐ浮かんでくる。この辺り繰り返し考えると、何度やっても地味に混乱しそうだけど。
### lcss("e", "cde") を求める
![]
この場合でもさ、一致している文字なんて全然見当たらないんだよなぁ…。ほんと不思議なくらい。でもたぶんこれって仕様通りなんだろう、と諦めモードになったりもする。いや待て、自分何言ってたっけ?一度頭を整理して…よし、本筋へ戻ろう。

下から塗り絵感覚でマス目を埋めていく不思議体験
`lcss("e", "cde")`というのは、1) `lcss("", "cde")`と2) `lcss("e", "de")`(赤で囲まれているやつ)、この二つのうち大きいほうの値になるんだよね。まあ、ここに関しては答えが明白すぎて…いや、特にひねりもなく決まっちゃう。でもさ、自分でもたまに「本当にそれだけでいいの?」みたいな疑念がふっと湧くことあるけど、ああ結局理屈上そうなるしかないか、と納得するしかなくて。さて。
### lcss("e", "bcde") と lcss("e", "abcde") の探索
![]
まずさ、`a`も`b`も両方とも当然ながら(そりゃそうだろって感じだけど) `e` とは一致しないんだよ。だから同じ処理をまた繰り返す羽目になる。つまり1) 右方向、それから2) 下方向、このどちらかで最大値を探す感じ。ただもうなんというか、その結果として出る値は毎度同じような光景で、「1」なんだよね…。ふぅ。
### lcss("ce", "e") と lcss("ce", "de") の探索
![]
今度はですね、またしても「c」は「e」と一致しない。あれ? もう少しドラマチックな展開になれば面白いんじゃ…とか思ったけど現実そんな甘くないわけで。それゆえに `lcss("ce", "e")` は赤枠内2つの数値、その中でより大きいものが選ばれる仕組みです。
![] 同様のパターンで、「lcss("ce", "de")」についてもやっぱり変わらず同じ手順を踏むことになるんですよ。本当繰り返しって時々疲れるよね。でも終わらせなきゃ話が進まないので戻ります。
### lcss("ce", "cde") の探索【注意事項】
…いや、「注意事項」って書いてあると身構えるけど、案外大したことなかったりする気もしちゃう。でもまあ慎重には進めたいところかな。このあたりまで来ると細部がおろそかになって迷子になりそうなので、一旦深呼吸して再確認したほうが良さげ。
### lcss("e", "bcde") と lcss("e", "abcde") の探索
![]
まずさ、`a`も`b`も両方とも当然ながら(そりゃそうだろって感じだけど) `e` とは一致しないんだよ。だから同じ処理をまた繰り返す羽目になる。つまり1) 右方向、それから2) 下方向、このどちらかで最大値を探す感じ。ただもうなんというか、その結果として出る値は毎度同じような光景で、「1」なんだよね…。ふぅ。
### lcss("ce", "e") と lcss("ce", "de") の探索
![]
今度はですね、またしても「c」は「e」と一致しない。あれ? もう少しドラマチックな展開になれば面白いんじゃ…とか思ったけど現実そんな甘くないわけで。それゆえに `lcss("ce", "e")` は赤枠内2つの数値、その中でより大きいものが選ばれる仕組みです。
![] 同様のパターンで、「lcss("ce", "de")」についてもやっぱり変わらず同じ手順を踏むことになるんですよ。本当繰り返しって時々疲れるよね。でも終わらせなきゃ話が進まないので戻ります。
### lcss("ce", "cde") の探索【注意事項】
…いや、「注意事項」って書いてあると身構えるけど、案外大したことなかったりする気もしちゃう。でもまあ慎重には進めたいところかな。このあたりまで来ると細部がおろそかになって迷子になりそうなので、一旦深呼吸して再確認したほうが良さげ。
記号合わせゲーム、斜めと横・縦どっちが大きい?
ああ、今また最初の文字が一致してるね。不思議とこういう偶然、何度も見てる気がするけど、ま、それはさておき——`lcss("ce", "cde")`について考えてみると、「1 + lcss("e", "de")」ってことになる。ん?なんか急に頭がぼーっとしてきたな…。でも要は表で言うなら、赤いマスに1を足して緑のマスへ反映するだけって話だ。
……ふと思ったけど、この作業、本当に地味なんだよね。で、次のいくつかのマスも同じように埋めていく必要がある。えっと、全部の緑色マスを見渡すとさ、不思議なことに最初の文字が一致しないケースばっかりじゃない?やれやれ… まあ、それぞれの場合、その緑色のマスには「右側の値」と「下側の値」のうち大きい方を設定するしかないんだろうな。
lcss("ace", "abcde")を求めるってなると、一瞬頭が混乱しそうになる。でも根気よく手順通り進めれば必ず辿り着けるはず、多分ね。
……ふと思ったけど、この作業、本当に地味なんだよね。で、次のいくつかのマスも同じように埋めていく必要がある。えっと、全部の緑色マスを見渡すとさ、不思議なことに最初の文字が一致しないケースばっかりじゃない?やれやれ… まあ、それぞれの場合、その緑色のマスには「右側の値」と「下側の値」のうち大きい方を設定するしかないんだろうな。
lcss("ace", "abcde")を求めるってなると、一瞬頭が混乱しそうになる。でも根気よく手順通り進めれば必ず辿り着けるはず、多分ね。

完成直前の迷子時間 実際に数字入れてみたら…?
緑色のマスの話だったよね、えっと、`lcss("ace", "abcde")`に着目してるんだ。最初の文字が一致しているから、そのセルには「1」と書いて、その上で斜め方向…つまり赤いマスの値を加えるってことになる。ま、実際やってみると分かりやすいかも。でも、ここで脇道それるけど昨日も似たようなこと考えてて、集中力すぐ飛ぶんだよなあ。うん、ごめん戻る。
残りの値をどう埋めるかというとさ、「r」は明らかに`abcde`には存在しないわけで…まあ、それならさっさと他のセルも処理できちゃうよね、とぼーっと思ったりする。さっきコンビニ行った時も同じくらい簡単な選択肢ばっかで逆に悩んだけど——いやいや今はこのテーブルだ。
さて全部テーブル埋め終わった後なんだけど、`race`と`abcde`間の最長共通部分列(Longest Common Subsequence)の長さを見るには、単純に一番最初の行・一番左側…そこを見るだけ。「3」って値が入っている、それが答えになるらしい。本当にこれ以上ある? 自分でも不安になったけど、大丈夫そう。
次はPythonコードを書く話だったかな…。時間計算量について言えばね、O(m*n)になる。結局全体m*n分テーブルを埋め尽くす感じだよ。でもこういう反復作業って妙に眠くなるから困る…とは言え、この原則は変わらないやつ。
残りの値をどう埋めるかというとさ、「r」は明らかに`abcde`には存在しないわけで…まあ、それならさっさと他のセルも処理できちゃうよね、とぼーっと思ったりする。さっきコンビニ行った時も同じくらい簡単な選択肢ばっかで逆に悩んだけど——いやいや今はこのテーブルだ。
さて全部テーブル埋め終わった後なんだけど、`race`と`abcde`間の最長共通部分列(Longest Common Subsequence)の長さを見るには、単純に一番最初の行・一番左側…そこを見るだけ。「3」って値が入っている、それが答えになるらしい。本当にこれ以上ある? 自分でも不安になったけど、大丈夫そう。
次はPythonコードを書く話だったかな…。時間計算量について言えばね、O(m*n)になる。結局全体m*n分テーブルを埋め尽くす感じだよ。でもこういう反復作業って妙に眠くなるから困る…とは言え、この原則は変わらないやつ。
最後に結果を見るだけ、それ以上は特になし
いや、どうだろう。ここまで読んでくれてる人がいるのか、ふと不安になる。ま、ともかく……内容が少しでも分かりやすかったら、それだけで嬉しいよ。こんな時に限って急にコーヒーが飲みたくなるけど、本筋に戻そう。
もし執筆活動を応援してもらえたら——あ、宣伝っぽいけど仕方ないね——**著書「101 Things I Never Knew About Python」**をぜひ手に取ってほしい。リンクはこちら(突然URL貼るの苦手なんだけど):[https://payhip.com/b/vywcf] まあ、押しつけじゃないから気が向いたらで大丈夫。
あとさ、このSubstackニュースレターにも実は登録できるんだ。[https://zlliu.substack.com/] こっちから参加できて、一週間ごとくらい?Pythonについて面白そうな話題を届けてるつもり。でも最近ネタ切れ気味だったりして…いや、ごめんまた脱線した。
ありがとう。本当に、小さなアクションひとつひとつが支えになってます。ああ、それだけは間違いなく言える。
**LinkedIn: [https://www.linkedin.com/company/the-python-rabbithole/]**
もし執筆活動を応援してもらえたら——あ、宣伝っぽいけど仕方ないね——**著書「101 Things I Never Knew About Python」**をぜひ手に取ってほしい。リンクはこちら(突然URL貼るの苦手なんだけど):[https://payhip.com/b/vywcf] まあ、押しつけじゃないから気が向いたらで大丈夫。
あとさ、このSubstackニュースレターにも実は登録できるんだ。[https://zlliu.substack.com/] こっちから参加できて、一週間ごとくらい?Pythonについて面白そうな話題を届けてるつもり。でも最近ネタ切れ気味だったりして…いや、ごめんまた脱線した。
- _この記事に50回拍手してくれたら泣いて喜ぶかもしれない_
- _感想やぼそっとしたコメントでも貰えると元気出ます_
- _何となく心に残った部分とかハイライトしてくれると助かります_
ありがとう。本当に、小さなアクションひとつひとつが支えになってます。ああ、それだけは間違いなく言える。
**LinkedIn: [https://www.linkedin.com/company/the-python-rabbithole/]**