去年の12月に2つの文字列のレーベンシュタイン距離を求めるっていうのを、JavaScript と Haskell でやった。もともとは Python での実装を参考に2次元配列を使ってやってみたもので、JavaScript版はともかく Haskell版は2次元配列の更新に苦労した。
それが、今日ふと1次元配列でやってみたらどうだろうと思いついた。
つまりこうだ。(m + 1)行×(n + 1)列(m、n は比較する文字列2つの長さ)の各行をつなげた1次元配列を考えて、2次元配列の時の座標を表すタプル (i, j) で初期化しておく。0 < i, 0 < j のとき、(i, j) の値はひとつ左(i-1, j)、ひとつ上(i, j-1)、左上(i-1, j-1)から決まるから、これを1次元配列のインデックスに直すと次のようになる:
- ひとつ左: i * (n + 1) + j – 1
- ひとつ上: (i – 1) * (n + 1) + j
- 左上: (i – 1) * (n + 1) + j – 1
これをコードに落としこんでやるとこうなった:
module Main where
import System.Environment (getArgs)
levenshteinDistance :: String -> String -> Int
levenshteinDistance s1 s2 = last ld
where
ld = map f [(x, y) | x <- [0..m], y <- [0..n]]
m = length s1
n = length s2
f (0, 0) = 0
f (i, 0) = i
f (0, j) = j
f (i, j) = minimum [a, b, c]
where
a = ld !! (i * (n + 1) + j - 1) + 1
b = ld !! ((i - 1) * (n + 1) + j) + 1
c = ld !! ((i - 1) * (n + 1) + j - 1) + c'
c' = if s1 !! (i - 1) == s2 !! (j - 1) then
0
else
1
main :: IO ()
main = do
args <- getArgs
let s1 = args !! 0
let s2 = args !! 1
print $ levenshteinDistance s1 s2
結果はこうだ。
takatoh@apostrophe $ runhaskell ld2.hs apple play 4 takatoh@apostrophe $ runhaskell ld2.hs perl pearl 1
OK、うまくいった。
だけど、上のコードはまだ2次元配列の意識を引きずっている。もっと単純にできるはずだ。1次元配列のインデックスを x とすると:
- ひとつ左: x – 1
- ひとつ上: x – (n + 1)
- 左上: x – (n + 1) – 1
となる。これで一般部については2次元配列を気にしなくて良くなった。ただし問題がある。いちばん上の行(第0行)といちばん左の列(第0列)だ。少し考えて、x を (n + 1) で割った商と余りを使えばいいと気がついた。コードにするとこう:
module Main where
import System.Environment (getArgs)
levenshteinDistance :: String -> String -> Int
levenshteinDistance s1 s2 = last ld
where
ld = map f [0..((m + 1) * (n + 1) -1)]
m = length s1
n = length s2
f x | x < n + 1 = x
| x `rem` (n + 1) == 0 = x `div` (n + 1)
| otherwise = minimum [a, b, c]
where
a = ld !! (x - 1) + 1
b = ld !! (x - (n + 1)) + 1
c = ld !! (x - (n + 1) - 1) + c'
c' = if s1 !! i == s2 !! j then
0
else
1
i = x `div` (n + 1) - 1
j = x `rem` (n + 1) - 1
main :: IO ()
main = do
args <- getArgs
let s1 = args !! 0
let s2 = args !! 1
print $ levenshteinDistance s1 s2
1行だけだけど長くなってしまった。だけど考え方はシンプルのように思う。実行してみよう。
takatoh@apostrophe $ runhaskell ld3.hs apple play 4 takatoh@apostrophe $ runhaskell ld3.hs perl pearl 1
OK。