問題
検索窓のsrc を、dst に
- どこかに 1 文字を挿入する
- どこかの 1 文字を削除する
- どこかの 1 文字を
別の 文字に 置き換える
条件は
- 操作は
どれも 1 回と 数えます。 3 つの 操作を 好きな 順に、 何回でも 混ぜて使えます srcとdstの長さは、 どちらも 0 以上 500 以下です。 空の 文字列も来ます - 文字は
英小文字だけです
入力と出力の例
例 1
先頭の s を
入力
src = "spoon"
dst = "moon"出力
2例 2
前から 1 文字ずつ
入力
src = "stone"
dst = "tones"出力
2例 3
空の
入力
src = ""
dst = "web"出力
3面接での聞かれ方
面接では、
- 「2 つの
文字列を、 挿入、 削除、 置き換えで 同じに する 最小の 手数を 返してください」 - 書き終えると
「表の 記憶を 減らせますか?」と 聞かれます。 2 次元の 表から 1 行分 へ縮められるかが、 シニアとして 見られる ところです - 「どの
操作を したかも返せますか?」 「置き換えだけ 2 回と 数えたら?」と、 表の 中身を 変える 問いが 続く こともあります
文字ごとに
アルゴリズムのチートシートの「動的計画法 (DP)」に、この型の要点を表とコードでまとめています。
まず 考える手順
段 1
まずは
段 2
最後のsrc のdst のd[i][j] とd[i-1][j-1] です。
srcの最後の 文字を 置き換えた。 d[i-1][j-1]に 1 を足すsrcの最後の 文字を 削除した。 d[i-1][j]に 1 を足すdstの最後の 文字を 挿入した。 d[i][j-1]に 1 を足す
この 3 つのd[i][j] です。
段 3
表のd[0][j] はd[i][0] は i 文字をd[m][n] です。
例 1 の spoon から moon のsrc のdst の
| 空 | m | o | o | n | |
|---|---|---|---|---|---|
| 空 | 0 | 1 | 2 | 3 | 4 |
| s | 1 | 1 | 2 | 3 | 4 |
| p | 2 | 2 | 2 | 3 | 4 |
| o | 3 | 3 | 2 | 2 | 3 |
| o | 4 | 4 | 3 | 2 | 3 |
| n | 5 | 5 | 4 | 3 | 2 |
右下の 2 がo のspo) と、o のmo) がspo を mo にo どうしでsp を m に
段 4
表全体は (m + 1) × (n + 1) のrow だけをdiag という
段 5
似たspoon と moon ならoon が 3 文字なので、
解答
Python
Python
def edit_distance(src, dst):
n = len(dst)
row = list(range(n + 1))
for i, a in enumerate(src):
diag = row[0]
row[0] = i + 1
for j in range(n):
up = row[j + 1]
left = row[j]
c = diag
if a != dst[j]:
c += 1
c = min(c, up + 1)
c = min(c, left + 1)
row[j + 1] = c
diag = up
return row[n]JavaScript
JavaScript
function editDistance(
src, dst
) {
const n = dst.length;
const row = Array.from(
{ length: n + 1 },
(_, j) => j,
);
let i = 0;
for (const a of src) {
i++;
let diag = row[0];
row[0] = i;
let j = 0;
for (const b of dst) {
const up = row[j + 1];
const left = row[j];
let c = diag;
if (a !== b) c++;
c = Math.min(c, up + 1);
c = Math.min(c, left + 1);
row[j + 1] = c;
diag = up;
j++;
}
}
return row[n];
}Go
Go
func editDistance(
src, dst string,
) int {
m, n := len(src), len(dst)
row := make([]int, n+1)
for j := range row {
row[j] = j
}
for i := 0; i < m; i++ {
diag := row[0]
row[0] = i + 1
for j := 0; j < n; j++ {
up := row[j+1]
left := row[j]
c := diag
if src[i] != dst[j] {
c++
}
c = min(c, up+1)
c = min(c, left+1)
row[j+1] = c
diag = up
}
}
return row[n]
}row[j + 1] をrow[j] がdiag がc は
計算量
- 時間は O(m × n) です (m は
src、n は dstの長さ)。 表の 欄を 1 つずつ、 決まった 回数の 比べで 埋めます。 500 文字どうしなら 25 万欄です - 使う
記憶は O(n) です。 1 行分の rowと、変数が いく つかだけです - 2 次元の
表を そのまま 持つ 書き方は、 時間は 同じで 記憶が O(m × n) です。 短い ほうの 文字列を dstの側に 回せば、 記憶は O(min(m, n)) まで 減らせます。 手数は 向きを 入れ替えても 変わりません (挿入と 削除が 入れ替わるだけ)
よく ある間違い
- 左上の
値を 退かさずに 上書きする。 1 行で 書く とき、 row[j]を上書きした あとに 次の 欄で 左上を 読むと、 もう 今の 行の 値に なっています。 上書きの 前に diagに取っておきます - 表の
端を 0 で埋める。 d[0][j]とd[i][0]は 0 ではなく j と i です。1 行で 書く ときも、 行ごとに row[0] = i + 1を書き換え忘れると、 空の 文字列の 例で外れます - 同じ
文字でも 1 を足す。 最後の 文字が 同じなら 手数は 増えません。 置き換えの 1 は、 文字が 違う ときだけ足します - 前から
そろえる 欲張りで書く。 例 2 の stoneとtonesを位置ごとに 比べると 5 文字とも 違いますが、 答えは 2 です。 ずれを 直す削除と 挿入を 考える ために、 表が要ります - 再帰と
メモで 書いて 深さを 見落とす。 500 文字どうしだと、 再帰の 深さが 1000 段近くになり、 Python の 再帰の 上限に かかります。 表を 下から 埋める 書き方の 方が安全です
解答を動かした版 Python 3.12.14Node.js 22.23.4Go 1.24.6確認日 2026年10月30日
面接での 話し方の例
書き始める
「前から i 文字と j 文字を
そろえる 最小の 手数を 表にします。 最後の 文字が 同じなら左上のまま、 違えば 置き換えの 左上、 削除の 上、 挿入の 左の どれかに 1 を 足した 最小です。 表は O(m × n) 時間で 埋まりますが、 1 つの 欄に 要るのは 上と 左と 左上だけなので、 1 行分の 並びと 左上を 退かす変数 1 つで、 記憶は O(n) に できます。」
書き終えたら、
「例 1 の
spoonからmoonをたどります。 s を 見た 行では、 どの 欄も s を 消すか 置き換えるかで 1、 2、 3、 4 です。 sp まで 来ると、 m と そろえるのに 2 回です。 その あと o、 o、 n は moonの後ろの 3 文字と 同じなので、 左上の 値が そのまま 右下へ 運ばれ、 答えは 2 に なります。」
