コンテンツにスキップ

14.6   編集距離問題

編集距離は Levenshtein 距離とも呼ばれ、一方の文字列をもう一方に変換するために必要な最小の変更回数を表します。情報検索や自然言語処理で、二つの列の類似度を測るためによく使われます。

問題

二つの文字列 \(s\) と \(t\) が与えられます。\(s\) を \(t\) に変換するための最小の編集回数を返してください。

文字列には、文字を一つ挿入する、文字を一つ削除する、文字を任意の文字に置き換える、という 3 種類の編集操作を行えます。

図 14-27 のように、kitten を sitting に変換するには、置換 2 回と挿入 1 回の計 3 回が必要です。hello を algo に変換するには、置換 2 回と削除 1 回の計 3 回が必要です。

編集距離のデータ例

図 14-27   編集距離のデータ例

編集距離問題は、決定木モデルで自然に説明できます。文字列が木のノードに対応し、1 回の判断(編集操作)が辺に対応します。

図 14-28 のように、操作を制限しなければ各ノードから多くの辺が伸び、各辺が一つの操作に対応します。つまり、hello から algo への変換には、多くの経路が考えられます。

決定木の観点では、この問題の目標はノード hello とノード algo の間の最短経路を求めることです。

決定木モデルによる編集距離問題の表現

図 14-28   決定木モデルによる編集距離問題の表現

1.   動的計画法の考え方

第 1 段階:各回の選択を考えて状態を定義し、\(dp\) テーブルを作る

各回の選択は、文字列 \(s\) に編集操作を 1 回行うことです。

部分問題を作るには、編集するたびに問題を小さくしていきたいところです。文字列 \(s\) と \(t\) の長さをそれぞれ \(n\) と \(m\) とし、まず末尾の文字 \(s[n-1]\) と \(t[m-1]\) を考えます。

  • \(s[n-1]\) と \(t[m-1]\) が同じなら、それらを飛ばして \(s[n-2]\) と \(t[m-2]\) を考えられます。
  • \(s[n-1]\) と \(t[m-1]\) が異なるなら、\(s\) に編集(挿入、削除、置換)を 1 回行って末尾の文字を一致させ、それらを飛ばして、より小さな問題を考えます。

つまり、文字列 \(s\) に対する各回の判断(編集操作)によって、\(s\) と \(t\) のまだ対応させていない文字が変わります。そこで、現在考えている \(s\) の \(i\) 番目と \(t\) の \(j\) 番目の文字を状態とし、\([i, j]\) と表します。

状態 \([i, j]\) の部分問題は、\(s\) の先頭 \(i\) 文字を \(t\) の先頭 \(j\) 文字に変換するための最小編集回数です。

これで、\((n+1) \times (m+1)\) の二次元の \(dp\) テーブルが得られます。

第 2 段階:最適部分構造を見つけ、状態遷移式を導く

部分問題 \(dp[i, j]\) に対応する二つの文字列の末尾は、\(s[i-1]\) と \(t[j-1]\) です。編集操作によって、図 14-29 の 3 通りに分けられます。

  1. \(s[i-1]\) の後ろに \(t[j-1]\) を挿入すると、残る部分問題は \(dp[i, j-1]\) です。
  2. \(s[i-1]\) を削除すると、残る部分問題は \(dp[i-1, j]\) です。
  3. \(s[i-1]\) を \(t[j-1]\) に置換すると、残る部分問題は \(dp[i-1, j-1]\) です。

編集距離の状態遷移

図 14-29   編集距離の状態遷移

ここから最適部分構造が得られます。\(dp[i, j]\) の最小編集回数は、\(dp[i, j-1]\)、\(dp[i-1, j]\)、\(dp[i-1, j-1]\) の三つの最小編集回数のうち、最も小さいものに、今回の編集 \(1\) 回を加えた値です。状態遷移式は次のようになります。

\[ dp[i, j] = \min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1 \]

\(s[i-1]\) と \(t[j-1]\) が同じなら、その文字を編集する必要はありません。この場合の状態遷移式は次のとおりです。

\[ dp[i, j] = dp[i-1, j-1] \]

第 3 段階:境界条件と状態遷移の順序を決める

両方の文字列が空なら編集回数は \(0\)、すなわち \(dp[0, 0] = 0\) です。\(s\) が空で \(t\) が空でないなら、最小編集回数は \(t\) の長さなので、先頭行は \(dp[0, j] = j\) です。\(s\) が空でなく \(t\) が空なら、最小編集回数は \(s\) の長さなので、先頭列は \(dp[i, 0] = i\) です。

状態遷移式から、解 \(dp[i, j]\) は左、上、左上の解に依存します。したがって、二重ループで \(dp\) テーブル全体を順方向に走査すれば十分です。

2.   コードの実装

edit_distance.py
def edit_distance_dp(s: str, t: str) -> int:
    """編集距離:動的計画法"""
    n, m = len(s), len(t)
    dp = [[0] * (m + 1) for _ in range(n + 1)]
    # 状態遷移:先頭行と先頭列
    for i in range(1, n + 1):
        dp[i][0] = i
    for j in range(1, m + 1):
        dp[0][j] = j
    # 状態遷移:残りの行と列
    for i in range(1, n + 1):
        for j in range(1, m + 1):
            if s[i - 1] == t[j - 1]:
                # 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[i][j] = dp[i - 1][j - 1]
            else:
                # 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1
    return dp[n][m]
edit_distance.cpp
/* 編集距離:動的計画法 */
int editDistanceDP(string s, string t) {
    int n = s.length(), m = t.length();
    vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
    // 状態遷移:先頭行と先頭列
    for (int i = 1; i <= n; i++) {
        dp[i][0] = i;
    }
    for (int j = 1; j <= m; j++) {
        dp[0][j] = j;
    }
    // 状態遷移:残りの行と列
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (s[i - 1] == t[j - 1]) {
                // 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
            }
        }
    }
    return dp[n][m];
}
edit_distance.java
/* 編集距離:動的計画法 */
int editDistanceDP(String s, String t) {
    int n = s.length(), m = t.length();
    int[][] dp = new int[n + 1][m + 1];
    // 状態遷移:先頭行と先頭列
    for (int i = 1; i <= n; i++) {
        dp[i][0] = i;
    }
    for (int j = 1; j <= m; j++) {
        dp[0][j] = j;
    }
    // 状態遷移:残りの行と列
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (s.charAt(i - 1) == t.charAt(j - 1)) {
                // 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
            }
        }
    }
    return dp[n][m];
}
edit_distance.cs
/* 編集距離:動的計画法 */
int EditDistanceDP(string s, string t) {
    int n = s.Length, m = t.Length;
    int[,] dp = new int[n + 1, m + 1];
    // 状態遷移:先頭行と先頭列
    for (int i = 1; i <= n; i++) {
        dp[i, 0] = i;
    }
    for (int j = 1; j <= m; j++) {
        dp[0, j] = j;
    }
    // 状態遷移:残りの行と列
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (s[i - 1] == t[j - 1]) {
                // 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[i, j] = dp[i - 1, j - 1];
            } else {
                // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[i, j] = Math.Min(Math.Min(dp[i, j - 1], dp[i - 1, j]), dp[i - 1, j - 1]) + 1;
            }
        }
    }
    return dp[n, m];
}
edit_distance.go
/* 編集距離:動的計画法 */
func editDistanceDP(s string, t string) int {
    n := len(s)
    m := len(t)
    dp := make([][]int, n+1)
    for i := 0; i <= n; i++ {
        dp[i] = make([]int, m+1)
    }
    // 状態遷移:先頭行と先頭列
    for i := 1; i <= n; i++ {
        dp[i][0] = i
    }
    for j := 1; j <= m; j++ {
        dp[0][j] = j
    }
    // 状態遷移:残りの行と列
    for i := 1; i <= n; i++ {
        for j := 1; j <= m; j++ {
            if s[i-1] == t[j-1] {
                // 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[i][j] = dp[i-1][j-1]
            } else {
                // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[i][j] = MinInt(MinInt(dp[i][j-1], dp[i-1][j]), dp[i-1][j-1]) + 1
            }
        }
    }
    return dp[n][m]
}
edit_distance.swift
/* 編集距離:動的計画法 */
func editDistanceDP(s: String, t: String) -> Int {
    let n = s.utf8CString.count
    let m = t.utf8CString.count
    var dp = Array(repeating: Array(repeating: 0, count: m + 1), count: n + 1)
    // 状態遷移:先頭行と先頭列
    for i in 1 ... n {
        dp[i][0] = i
    }
    for j in 1 ... m {
        dp[0][j] = j
    }
    // 状態遷移:残りの行と列
    for i in 1 ... n {
        for j in 1 ... m {
            if s.utf8CString[i - 1] == t.utf8CString[j - 1] {
                // 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[i][j] = dp[i - 1][j - 1]
            } else {
                // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1
            }
        }
    }
    return dp[n][m]
}
edit_distance.js
/* 編集距離:動的計画法 */
function editDistanceDP(s, t) {
    const n = s.length,
        m = t.length;
    const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
    // 状態遷移:先頭行と先頭列
    for (let i = 1; i <= n; i++) {
        dp[i][0] = i;
    }
    for (let j = 1; j <= m; j++) {
        dp[0][j] = j;
    }
    // 状態遷移:残りの行と列
    for (let i = 1; i <= n; i++) {
        for (let j = 1; j <= m; j++) {
            if (s.charAt(i - 1) === t.charAt(j - 1)) {
                // 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[i][j] =
                    Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;
            }
        }
    }
    return dp[n][m];
}
edit_distance.ts
/* 編集距離:動的計画法 */
function editDistanceDP(s: string, t: string): number {
    const n = s.length,
        m = t.length;
    const dp = Array.from({ length: n + 1 }, () =>
        Array.from({ length: m + 1 }, () => 0)
    );
    // 状態遷移:先頭行と先頭列
    for (let i = 1; i <= n; i++) {
        dp[i][0] = i;
    }
    for (let j = 1; j <= m; j++) {
        dp[0][j] = j;
    }
    // 状態遷移:残りの行と列
    for (let i = 1; i <= n; i++) {
        for (let j = 1; j <= m; j++) {
            if (s.charAt(i - 1) === t.charAt(j - 1)) {
                // 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[i][j] =
                    Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;
            }
        }
    }
    return dp[n][m];
}
edit_distance.dart
/* 編集距離:動的計画法 */
int editDistanceDP(String s, String t) {
  int n = s.length, m = t.length;
  List<List<int>> dp = List.generate(n + 1, (_) => List.filled(m + 1, 0));
  // 状態遷移:先頭行と先頭列
  for (int i = 1; i <= n; i++) {
    dp[i][0] = i;
  }
  for (int j = 1; j <= m; j++) {
    dp[0][j] = j;
  }
  // 状態遷移:残りの行と列
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
      if (s[i - 1] == t[j - 1]) {
        // 2 つの文字が等しい場合、その 2 文字をスキップする
        dp[i][j] = dp[i - 1][j - 1];
      } else {
        // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
        dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
      }
    }
  }
  return dp[n][m];
}
edit_distance.rs
/* 編集距離:動的計画法 */
fn edit_distance_dp(s: &str, t: &str) -> i32 {
    let (n, m) = (s.len(), t.len());
    let mut dp = vec![vec![0; m + 1]; n + 1];
    // 状態遷移:先頭行と先頭列
    for i in 1..=n {
        dp[i][0] = i as i32;
    }
    for j in 1..m {
        dp[0][j] = j as i32;
    }
    // 状態遷移:残りの行と列
    for i in 1..=n {
        for j in 1..=m {
            if s.chars().nth(i - 1) == t.chars().nth(j - 1) {
                // 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[i][j] =
                    std::cmp::min(std::cmp::min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
            }
        }
    }
    dp[n][m]
}
edit_distance.c
/* 編集距離:動的計画法 */
int editDistanceDP(char *s, char *t, int n, int m) {
    int **dp = malloc((n + 1) * sizeof(int *));
    for (int i = 0; i <= n; i++) {
        dp[i] = calloc(m + 1, sizeof(int));
    }
    // 状態遷移:先頭行と先頭列
    for (int i = 1; i <= n; i++) {
        dp[i][0] = i;
    }
    for (int j = 1; j <= m; j++) {
        dp[0][j] = j;
    }
    // 状態遷移:残りの行と列
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (s[i - 1] == t[j - 1]) {
                // 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[i][j] = myMin(myMin(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
            }
        }
    }
    int res = dp[n][m];
    // メモリを解放する
    for (int i = 0; i <= n; i++) {
        free(dp[i]);
    }
    return res;
}
edit_distance.kt
/* 編集距離:動的計画法 */
fun editDistanceDP(s: String, t: String): Int {
    val n = s.length
    val m = t.length
    val dp = Array(n + 1) { IntArray(m + 1) }
    // 状態遷移:先頭行と先頭列
    for (i in 1..n) {
        dp[i][0] = i
    }
    for (j in 1..m) {
        dp[0][j] = j
    }
    // 状態遷移:残りの行と列
    for (i in 1..n) {
        for (j in 1..m) {
            if (s[i - 1] == t[j - 1]) {
                // 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[i][j] = dp[i - 1][j - 1]
            } else {
                // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1
            }
        }
    }
    return dp[n][m]
}
edit_distance.rb
### 編集距離:動的計画法 ###
def edit_distance_dp(s, t)
  n, m = s.length, t.length
  dp = Array.new(n + 1) { Array.new(m + 1, 0) }
  # 状態遷移:先頭行と先頭列
  (1...(n + 1)).each { |i| dp[i][0] = i }
  (1...(m + 1)).each { |j| dp[0][j] = j }
  # 状態遷移:残りの行と列
  for i in 1...(n + 1)
    for j in 1...(m +1)
      if s[i - 1] == t[j - 1]
        # 2 つの文字が等しい場合、その 2 文字をスキップする
        dp[i][j] = dp[i - 1][j - 1]
      else
        # 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
        dp[i][j] = [dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]].min + 1
      end
    end
  end
  dp[n][m]
end
実行の可視化

図 14-30 のように、編集距離問題の状態遷移はナップサック問題によく似ており、どちらも二次元の表を埋める過程と捉えられます。

編集距離を動的計画法で求める流れ

編集距離を動的計画法で求める流れ:手順 2

編集距離を動的計画法で求める流れ:手順 3

編集距離を動的計画法で求める流れ:手順 4

編集距離を動的計画法で求める流れ:手順 5

編集距離を動的計画法で求める流れ:手順 6

編集距離を動的計画法で求める流れ:手順 7

編集距離を動的計画法で求める流れ:手順 8

編集距離を動的計画法で求める流れ:手順 9

編集距離を動的計画法で求める流れ:手順 10

編集距離を動的計画法で求める流れ:手順 11

編集距離を動的計画法で求める流れ:手順 12

編集距離を動的計画法で求める流れ:手順 13

編集距離を動的計画法で求める流れ:手順 14

編集距離を動的計画法で求める流れ:手順 15

図 14-30   編集距離を動的計画法で求める流れ

3.   空間の最適化

\(dp[i,j]\) は、上の \(dp[i-1, j]\)、左の \(dp[i, j-1]\)、左上の \(dp[i-1, j-1]\) から遷移します。順方向に走査すると左上の \(dp[i-1, j-1]\) が失われ、逆方向では \(dp[i, j-1]\) を先に計算できません。そのため、どちらの順序もそのままでは使えません。

そこで、変数 leftup に左上の解 \(dp[i-1, j-1]\) を一時保存すれば、左と上の解だけを考えればよくなります。これは個数制限なしナップサック問題と同じ状況なので、順方向に走査できます。コードを次に示します。

edit_distance.py
def edit_distance_dp_comp(s: str, t: str) -> int:
    """編集距離:空間を最適化した動的計画法"""
    n, m = len(s), len(t)
    dp = [0] * (m + 1)
    # 状態遷移:先頭行
    for j in range(1, m + 1):
        dp[j] = j
    # 状態遷移:残りの行
    for i in range(1, n + 1):
        # 状態遷移:先頭列
        leftup = dp[0]  # dp[i-1, j-1] を一時保存する
        dp[0] += 1
        # 状態遷移:残りの列
        for j in range(1, m + 1):
            temp = dp[j]
            if s[i - 1] == t[j - 1]:
                # 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[j] = leftup
            else:
                # 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[j] = min(dp[j - 1], dp[j], leftup) + 1
            leftup = temp  # 次の反復の dp[i-1, j-1] に更新する
    return dp[m]
edit_distance.cpp
/* 編集距離:空間を最適化した動的計画法 */
int editDistanceDPComp(string s, string t) {
    int n = s.length(), m = t.length();
    vector<int> dp(m + 1, 0);
    // 状態遷移:先頭行
    for (int j = 1; j <= m; j++) {
        dp[j] = j;
    }
    // 状態遷移:残りの行
    for (int i = 1; i <= n; i++) {
        // 状態遷移:先頭列
        int leftup = dp[0]; // dp[i-1, j-1] を一時保存する
        dp[0] = i;
        // 状態遷移:残りの列
        for (int j = 1; j <= m; j++) {
            int temp = dp[j];
            if (s[i - 1] == t[j - 1]) {
                // 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[j] = leftup;
            } else {
                // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;
            }
            leftup = temp; // 次の反復の dp[i-1, j-1] に更新する
        }
    }
    return dp[m];
}
edit_distance.java
/* 編集距離:空間を最適化した動的計画法 */
int editDistanceDPComp(String s, String t) {
    int n = s.length(), m = t.length();
    int[] dp = new int[m + 1];
    // 状態遷移:先頭行
    for (int j = 1; j <= m; j++) {
        dp[j] = j;
    }
    // 状態遷移:残りの行
    for (int i = 1; i <= n; i++) {
        // 状態遷移:先頭列
        int leftup = dp[0]; // dp[i-1, j-1] を一時保存する
        dp[0] = i;
        // 状態遷移:残りの列
        for (int j = 1; j <= m; j++) {
            int temp = dp[j];
            if (s.charAt(i - 1) == t.charAt(j - 1)) {
                // 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[j] = leftup;
            } else {
                // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftup) + 1;
            }
            leftup = temp; // 次の反復の dp[i-1, j-1] に更新する
        }
    }
    return dp[m];
}
edit_distance.cs
/* 編集距離:空間を最適化した動的計画法 */
int EditDistanceDPComp(string s, string t) {
    int n = s.Length, m = t.Length;
    int[] dp = new int[m + 1];
    // 状態遷移:先頭行
    for (int j = 1; j <= m; j++) {
        dp[j] = j;
    }
    // 状態遷移:残りの行
    for (int i = 1; i <= n; i++) {
        // 状態遷移:先頭列
        int leftup = dp[0]; // dp[i-1, j-1] を一時保存する
        dp[0] = i;
        // 状態遷移:残りの列
        for (int j = 1; j <= m; j++) {
            int temp = dp[j];
            if (s[i - 1] == t[j - 1]) {
                // 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[j] = leftup;
            } else {
                // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[j] = Math.Min(Math.Min(dp[j - 1], dp[j]), leftup) + 1;
            }
            leftup = temp; // 次の反復の dp[i-1, j-1] に更新する
        }
    }
    return dp[m];
}
edit_distance.go
/* 編集距離:空間を最適化した動的計画法 */
func editDistanceDPComp(s string, t string) int {
    n := len(s)
    m := len(t)
    dp := make([]int, m+1)
    // 状態遷移:先頭行
    for j := 1; j <= m; j++ {
        dp[j] = j
    }
    // 状態遷移:残りの行
    for i := 1; i <= n; i++ {
        // 状態遷移:先頭列
        leftUp := dp[0] // dp[i-1, j-1] を一時保存する
        dp[0] = i
        // 状態遷移:残りの列
        for j := 1; j <= m; j++ {
            temp := dp[j]
            if s[i-1] == t[j-1] {
                // 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[j] = leftUp
            } else {
                // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[j] = MinInt(MinInt(dp[j-1], dp[j]), leftUp) + 1
            }
            leftUp = temp // 次の反復の dp[i-1, j-1] に更新する
        }
    }
    return dp[m]
}
edit_distance.swift
/* 編集距離:空間を最適化した動的計画法 */
func editDistanceDPComp(s: String, t: String) -> Int {
    let n = s.utf8CString.count
    let m = t.utf8CString.count
    var dp = Array(repeating: 0, count: m + 1)
    // 状態遷移:先頭行
    for j in 1 ... m {
        dp[j] = j
    }
    // 状態遷移:残りの行
    for i in 1 ... n {
        // 状態遷移:先頭列
        var leftup = dp[0] // dp[i-1, j-1] を一時保存する
        dp[0] = i
        // 状態遷移:残りの列
        for j in 1 ... m {
            let temp = dp[j]
            if s.utf8CString[i - 1] == t.utf8CString[j - 1] {
                // 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[j] = leftup
            } else {
                // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1
            }
            leftup = temp // 次の反復の dp[i-1, j-1] に更新する
        }
    }
    return dp[m]
}
edit_distance.js
/* 編集距離:空間を最適化した動的計画法 */
function editDistanceDPComp(s, t) {
    const n = s.length,
        m = t.length;
    const dp = new Array(m + 1).fill(0);
    // 状態遷移:先頭行
    for (let j = 1; j <= m; j++) {
        dp[j] = j;
    }
    // 状態遷移:残りの行
    for (let i = 1; i <= n; i++) {
        // 状態遷移:先頭列
        let leftup = dp[0]; // dp[i-1, j-1] を一時保存する
        dp[0] = i;
        // 状態遷移:残りの列
        for (let j = 1; j <= m; j++) {
            const temp = dp[j];
            if (s.charAt(i - 1) === t.charAt(j - 1)) {
                // 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[j] = leftup;
            } else {
                // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;
            }
            leftup = temp; // 次の反復の dp[i-1, j-1] に更新する
        }
    }
    return dp[m];
}
edit_distance.ts
/* 編集距離:空間を最適化した動的計画法 */
function editDistanceDPComp(s: string, t: string): number {
    const n = s.length,
        m = t.length;
    const dp = new Array(m + 1).fill(0);
    // 状態遷移:先頭行
    for (let j = 1; j <= m; j++) {
        dp[j] = j;
    }
    // 状態遷移:残りの行
    for (let i = 1; i <= n; i++) {
        // 状態遷移:先頭列
        let leftup = dp[0]; // dp[i-1, j-1] を一時保存する
        dp[0] = i;
        // 状態遷移:残りの列
        for (let j = 1; j <= m; j++) {
            const temp = dp[j];
            if (s.charAt(i - 1) === t.charAt(j - 1)) {
                // 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[j] = leftup;
            } else {
                // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;
            }
            leftup = temp; // 次の反復の dp[i-1, j-1] に更新する
        }
    }
    return dp[m];
}
edit_distance.dart
/* 編集距離:空間を最適化した動的計画法 */
int editDistanceDPComp(String s, String t) {
  int n = s.length, m = t.length;
  List<int> dp = List.filled(m + 1, 0);
  // 状態遷移:先頭行
  for (int j = 1; j <= m; j++) {
    dp[j] = j;
  }
  // 状態遷移:残りの行
  for (int i = 1; i <= n; i++) {
    // 状態遷移:先頭列
    int leftup = dp[0]; // dp[i-1, j-1] を一時保存する
    dp[0] = i;
    // 状態遷移:残りの列
    for (int j = 1; j <= m; j++) {
      int temp = dp[j];
      if (s[i - 1] == t[j - 1]) {
        // 2 つの文字が等しい場合、その 2 文字をスキップする
        dp[j] = leftup;
      } else {
        // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
        dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;
      }
      leftup = temp; // 次の反復の dp[i-1, j-1] に更新する
    }
  }
  return dp[m];
}
edit_distance.rs
/* 編集距離:空間を最適化した動的計画法 */
fn edit_distance_dp_comp(s: &str, t: &str) -> i32 {
    let (n, m) = (s.len(), t.len());
    let mut dp = vec![0; m + 1];
    // 状態遷移:先頭行
    for j in 1..m {
        dp[j] = j as i32;
    }
    // 状態遷移:残りの行
    for i in 1..=n {
        // 状態遷移:先頭列
        let mut leftup = dp[0]; // dp[i-1, j-1] を一時保存する
        dp[0] = i as i32;
        // 状態遷移:残りの列
        for j in 1..=m {
            let temp = dp[j];
            if s.chars().nth(i - 1) == t.chars().nth(j - 1) {
                // 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[j] = leftup;
            } else {
                // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[j] = std::cmp::min(std::cmp::min(dp[j - 1], dp[j]), leftup) + 1;
            }
            leftup = temp; // 次の反復の dp[i-1, j-1] に更新する
        }
    }
    dp[m]
}
edit_distance.c
/* 編集距離:空間を最適化した動的計画法 */
int editDistanceDPComp(char *s, char *t, int n, int m) {
    int *dp = calloc(m + 1, sizeof(int));
    // 状態遷移:先頭行
    for (int j = 1; j <= m; j++) {
        dp[j] = j;
    }
    // 状態遷移:残りの行
    for (int i = 1; i <= n; i++) {
        // 状態遷移:先頭列
        int leftup = dp[0]; // dp[i-1, j-1] を一時保存する
        dp[0] = i;
        // 状態遷移:残りの列
        for (int j = 1; j <= m; j++) {
            int temp = dp[j];
            if (s[i - 1] == t[j - 1]) {
                // 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[j] = leftup;
            } else {
                // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[j] = myMin(myMin(dp[j - 1], dp[j]), leftup) + 1;
            }
            leftup = temp; // 次の反復の dp[i-1, j-1] に更新する
        }
    }
    int res = dp[m];
    // メモリを解放する
    free(dp);
    return res;
}
edit_distance.kt
/* 編集距離:空間を最適化した動的計画法 */
fun editDistanceDPComp(s: String, t: String): Int {
    val n = s.length
    val m = t.length
    val dp = IntArray(m + 1)
    // 状態遷移:先頭行
    for (j in 1..m) {
        dp[j] = j
    }
    // 状態遷移:残りの行
    for (i in 1..n) {
        // 状態遷移:先頭列
        var leftup = dp[0] // dp[i-1, j-1] を一時保存する
        dp[0] = i
        // 状態遷移:残りの列
        for (j in 1..m) {
            val temp = dp[j]
            if (s[i - 1] == t[j - 1]) {
                // 2 つの文字が等しい場合、その 2 文字をスキップする
                dp[j] = leftup
            } else {
                // 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1
            }
            leftup = temp // 次の反復の dp[i-1, j-1] に更新する
        }
    }
    return dp[m]
}
edit_distance.rb
### 編集距離:空間を最適化した動的計画法 ###
def edit_distance_dp_comp(s, t)
  n, m = s.length, t.length
  dp = Array.new(m + 1, 0)
  # 状態遷移:先頭行
  (1...(m + 1)).each { |j| dp[j] = j }
  # 状態遷移:残りの行
  for i in 1...(n + 1)
    # 状態遷移:先頭列
    leftup = dp.first # dp[i-1, j-1] を一時保存する
    dp[0] += 1
    # 状態遷移:残りの列
    for j in 1...(m + 1)
      temp = dp[j]
      if s[i - 1] == t[j - 1]
        # 2 つの文字が等しい場合、その 2 文字をスキップする
        dp[j] = leftup
      else
        # 最小編集回数 = 挿入、削除、置換の 3 操作での最小編集回数 + 1
        dp[j] = [dp[j - 1], dp[j], leftup].min + 1
      end
      leftup = temp # 次の反復の dp[i-1, j-1] に更新する
    end
  end
  dp[m]
end
実行の可視化

コメント欄で、考えや質問、ご提案をお寄せください。