問題
文字列 $S$(長さ $M$)と文字列 $T$(長さ $N$)が与えられる。$S$ を $T$ に一致させるために必要な、挿入・削除・置換(それぞれコスト1)の最小回数、すなわち 編集距離(レーベンシュタイン距離) を求めよ。
通常のDPでは $O(NM)$ かかるが、$M \le 60$ なら $S$ の各文字位置をビット列の1ビットに対応させ、1列分の更新を整数のビット演算だけで行う Myers' Bit-Vector Algorithm により全体 $O(N)$ で計算できる。
入力形式
S
T
制約
$1 \le M = |S| \le 60$
$1 \le N = |T| \le 2 \times 10^5$
$S, T$ は英小文字のみ
入出力例
入力例1
kitten
sitting
出力例1
3
入力例2
abcde
xxabcdexx
出力例2
4
概念図
ヒント(段階的開示)
ヒント1: 方向性
各 $j$ ごとに、直前の列から現在の列を計算する際の「1つ上のセルとの差分(+1, 0, -1)」のパターンには規則性がある。この差分パターンをビット列として持てば、1列全体の更新をまとめて処理できないか考えよ。
ヒント2: アプローチ
列方向の差分を「+1のビット集合
Ph」「−1のビット集合 Mh」、行方向を Pv, Mv として表現する。文字 $c$ ごとに前計算したビットマスク Peq[c]($S$のどの位置と一致するか)を使うと、1文字読むたびの更新式がすべてビット演算の組み合わせで書ける。行末のビットが Ph に立てば score += 1、Mh に立てば score -= 1。ヒント3: 誘導(コード骨格)
def myers_edit_distance(s, t):
m = len(s)
MASK = (1 << m) - 1
peq = {}
for i, ch in enumerate(s):
peq[ch] = peq.get(ch, 0) | (1 << i)
Pv = MASK
Mv = 0
score = m
topbit = 1 << (m - 1)
for ch in t:
eq = peq.get(ch, 0)
# Xv, Xh, Ph, Mh をビット演算で計算し、score を更新
...
return score
~(NOT)演算はPythonでは符号なし固定長ビットとして扱えないため、MASK ^ x で代用する。
模範解答 (Python)
import sys
def myers_edit_distance(s, t):
m = len(s)
if m == 0:
return len(t)
MASK = (1 << m) - 1
peq = {}
for i, ch in enumerate(s):
peq[ch] = peq.get(ch, 0) | (1 << i)
Pv = MASK
Mv = 0
score = m
topbit = 1 << (m - 1)
for ch in t:
eq = peq.get(ch, 0)
Xv = (eq | Mv) & MASK
Xh = ((((eq & Pv) + Pv) & MASK) ^ Pv) | eq
Xh &= MASK
Ph = (Mv | (MASK ^ (Xh | Pv))) & MASK
Mh = Pv & Xh & MASK
if Ph & topbit:
score += 1
elif Mh & topbit:
score -= 1
Ph = ((Ph << 1) | 1) & MASK
Mh = (Mh << 1) & MASK
Pv = (Mh | (MASK ^ (Xv | Ph))) & MASK
Mv = Ph & Xv & MASK
return score
def solve():
data = sys.stdin.read().split()
s, t = data[0], data[1]
print(myers_edit_distance(s, t))
solve()
計算量: $M \le 64$ なら1ワードに収まるため $O(N)$。$M > 64$ の場合はブロック分割で $O(N\lceil M/64\rceil)$。
Step-by-Step 解説
1前処理 Peq
$S$の各文字位置に対応するビットマスク
$S$の各文字位置に対応するビットマスク
Peq[c]を文字ごとに事前計算する。2初期状態
dp[i][0]=iに対応させ、Pvを全ビット1、Mvを0、score=mに初期化する。31文字ごとの更新式
Xh=(((eq&Pv)+Pv)^Pv)|eqの加算演算が核心で、一致が続く区間の右端を一括検出する(Myers, 1999)。4scoreの更新と行方向への反映
Ph/Mhの末尾ビットでscoreを増減し、シフトして次列用のPv,Mvを作る。5計算量とワード分割
$M\le64$なら1ワードで$O(N)$。$M>64$はブロック間キャリー伝播が必要で$O(N\lceil M/64\rceil)$。
$M\le64$なら1ワードで$O(N)$。$M>64$はブロック間キャリー伝播が必要で$O(N\lceil M/64\rceil)$。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
~xをそのまま使う | Pythonの~は無限桁2の補数を返し有限幅の式が壊れる | MASK ^ xで$M$ビット幅のNOTを表現 |
Phシフト後に最下位ビットへの1立てを忘れる | dp[0][j]=jの境界条件が反映されない | Ph = ((Ph << 1) | 1) & MASK |
Xhの加算後にMASKでマスクし忘れる | 多倍長intの繰り上がりが以降の演算を汚染 | 加算・シフトのたびに& MASKで切り詰める |
| $M>64$にも単一ワード実装を使う | 単一ワード実装はワード幅を前提 | ワード分割しブロックごとにキャリーを伝播 |
次のステップ
- 発展: $M>64$対応の複数ワード版、または自由開始版(近似文字列検索・agrep)
- 次回予告: MST検証アルゴリズム(全方位木DPによる最大辺クエリ・Komlós法の実用実装)