0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeScriptで編集距離を実装する:面接トランスクリプトの回答差分をO(min(m,n))メモリで測る

0
Posted at

背景:面接後のトランスクリプトを振り返るという課題は、AI面接トランスクリプトレビューをきっかけに整理しました。本稿では製品の話ではなく、2つの文字列の修正量を測る実装だけを扱います。

TypeScriptで編集距離を実装する:面接トランスクリプトの回答差分をO(min(m,n))メモリで測る

面接で実際に話した回答と、振り返り後に書き直した回答は、同じ主旨でも少しずつ違います。「どれだけ直したか」を数えるだけなら、編集距離(Levenshtein distance)が使えます。

この記事で実装する関数は、挿入・削除・置換をそれぞれ1回として最小回数を返します。動的計画法の表全体は保持せず、短い方の文字列に対応する2行だけを持つため、メモリ量は O(min(m, n)) です。

  • 距離が 0: 文字列は同一です。
  • 距離が小さい: 表現の微修正である可能性があります。
  • 距離が大きい: 構成や具体例が大きく変わっています。

編集距離は回答の「良し悪し」を採点する値ではありません。変更量を機械的に見つけ、見直す場所を絞るための値です。

入力から差分率までの流れ

問題を操作に分解する

たとえば 結果を共有しました を 結果をすぐ共有しました に変えるとき、必要なのは すぐ の挿入です。編集距離では次の3操作を数えます。

操作 例
挿入 結果を共有しました → 結果を**すぐ**共有しました
削除 私は**主に**設計しました → 私は設計しました
置換 結果 → 成果

文字列 left の先頭 i 文字と right の先頭 j 文字の距離を dp[i][j] とすると、最後の文字が同じかどうかで次のように決まります。

dp[i][j] = min(
  dp[i - 1][j] + 1,                  // left 側の1文字を削除
  dp[i][j - 1] + 1,                  // right 側の1文字を挿入
  dp[i - 1][j - 1] + 置換コスト      // 同じなら0、異なれば1
)

ここで重要なのは、dp[i][j] が参照するのは「上」「左」「左上」だけだという点です。前の行と現在の行があれば、表全体を残す必要はありません。

各セルが参照する3方向

TypeScript実装

Array.from を使うと、UTF-16のコードユニットではなくコードポイント単位で走査できます。短い方を列に固定しているので、入力順が逆でも必要なバッファ量は増えません。

export function levenshtein(left: string, right: string): number {
  let row = Array.from(left);
  let column = Array.from(right);

  // バッファは常に短い方に合わせる。
  if (row.length < column.length) [row, column] = [column, row];

  let previous = new Uint32Array(column.length + 1);
  let current = new Uint32Array(column.length + 1);

  // 空文字列から column の先頭 j 文字へは、j 回の挿入が必要。
  for (let j = 0; j <= column.length; j++) previous[j] = j;

  for (let i = 1; i <= row.length; i++) {
    // row の先頭 i 文字から空文字列へは、i 回の削除が必要。
    current[0] = i;

    for (let j = 1; j <= column.length; j++) {
      const replace =
        previous[j - 1] + Number(row[i - 1] !== column[j - 1]);
      const remove = previous[j] + 1;
      const insert = current[j - 1] + 1;

      current[j] = Math.min(replace, remove, insert);
    }

    [previous, current] = [current, previous];
  }

  return previous[column.length];
}

実行時間は O(m × n) です。一般的な面接回答のような数百文字程度なら十分に軽く、保存する数値は短い方の長さに比例します。

差分を長さで正規化する

100文字の回答で距離10と、10文字の回答で距離10は同じ重さではありません。比較画面などでは、距離を最大長で割った差分率も一緒に出すと扱いやすくなります。

export function similarity(left: string, right: string): number {
  const length = Math.max(Array.from(left).length, Array.from(right).length);
  return length === 0 ? 1 : 1 - levenshtein(left, right) / length;
}

この値は0から1の範囲です。たとえば 結果 と 成果 の編集距離は1、類似度は 1 - 1 / 2 = 0.5 です。閾値を決める場合は、実際のデータを見てから決めます。一般的な「0.8以上なら同じ」といった値をそのまま採用すると、短文で誤判定しやすくなります。

動作確認

次のケースをBunで実行しました。空文字列、同一文字列、挿入、置換、日本語、入力順を反転した場合を含めています。

import { strict as assert } from "node:assert";

assert.equal(levenshtein("", ""), 0);
assert.equal(levenshtein("STAR", "STAR"), 0);
assert.equal(levenshtein("STAR", "SSTAR"), 1);
assert.equal(levenshtein("結果", "成果"), 1);
assert.equal(levenshtein("私は設計しました", "私は設計をしました"), 1);
assert.equal(levenshtein("kitten", "sitting"), 3);
assert.equal(levenshtein("sitting", "kitten"), 3);
assert.equal(similarity("", ""), 1);
assert.equal(similarity("結果", "成果"), 0.5);

console.log("9 assertions passed");

出力は次のとおりでした。

9 assertions passed

実務での注意点

文字単位と見た目の1文字は一致しないことがある

Array.from はコードポイント単位です。結合文字、絵文字の修飾子、複数コードポイントからなる書記素クラスタを「見た目の1文字」として扱いたい場合は、分割処理を Intl.Segmenter に差し替えます。編集距離のDP部分はそのまま再利用できます。

const segmenter = new Intl.Segmenter("ja", { granularity: "grapheme" });

function graphemes(text: string): string[] {
  return [...segmenter.segment(text)].map(({ segment }) => segment);
}

距離だけでは、何を直したか分からない

2行だけのDPは距離を省メモリで求めるための実装です。実際の挿入・削除・置換箇所までUIに表示するには、全表を保持して逆向きにたどる方法、または Myers diff のような別アルゴリズムを選びます。まず変更量だけで十分か、差分表示まで必要かを分けて設計するのが安全です。

まとめ

  • 編集距離は、2つの回答テキストの最小修正回数を測れます。
  • 参照するセルは3つだけなので、DPの2行で O(min(m, n)) メモリにできます。
  • 距離は品質スコアではなく、見直し対象を見つけるための指標です。
  • 表示単位や差分の復元が必要なら、入力の分割方法とアルゴリズムを追加で選びます。

技術面接では、DPの漸化式だけで終わらず、「なぜ2行で足りるか」「差分箇所を復元するには何が変わるか」まで説明できると、設計上のトレードオフを具体的に伝えられます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?