編集距離

編集距離、一般には「Levenstein Distance」と言うらしいです。二つの文字列について、片方を「削除、挿入」をそれぞれ何回することによってもう片方と同じものにできるか、という数字です。よく、二つの文字列の類似度を測る基準として用いられます。

この編集距離を求める方法には、ベタな方法、O(ND)法、O(NP)法というものが代表的なアルゴリズムらしいです。ベタな方法はそのまま。O(ND)法は「n回の編集で一致できるか?」という観点で無駄を省いたもの。O(NP)法は「p回損をすると一致できるか」という観点でO(ND)法を改良したもの。ということらしい。

参考


そこで、dtl開発者の方のソースを参考に、Rubyに落とし込んでみた。Moduleにしているので、Requireして「EditDistance.onp(arg1,arg2)」で編集距離が帰ってきます。arg1,arg2はArrayを渡します。またその要素は「==」演算子が使え無ければなりません。なお往々にして「複数の文の中から最小の編集距離を求めたい」ということがあるので、枝刈りできるように制限引数limitをつけています。これを指定した場合、編集距離がその数以上になった場合、計算を打ち切ります。

module EditDistance

  @@N = 0
  @@M = 0
  @@ary1 = nil
  @@ary2 = nil

  def onp(ary1, ary2, limit = nil)
    if ary1.size > ary2.size then 
      @@ary1, @@ary2 = ary2, ary1 
    else
      @@ary1, @@ary2 = ary1, ary2
    end

    @@M = @@ary1.size
    @@N = @@ary2.size
    offset = @@M + 1
    delta  = @@N - @@M
    size   = @@M + @@N + 3

    limit = @@M*2 + delta if limit == nil

    fp = Array.new(size)
    fp.fill(-1)

    p = -1
    k = 0

    until fp[delta + offset] >= @@N do
      p += 1
      return limit if 2*p+delta >= limit #limit以上で打ち切り

      k = -p
      while k < delta do
        fp[k + offset] = fp[k - 1 + offset] + 1 > fp[k + 1 + offset] ? 
        snake(k, fp[k - 1 + offset] + 1) : 
        snake(k, fp[k + 1 + offset])

        k += 1
      end
      
      k = delta + p
      while k > delta do
        fp[k + offset] = fp[k - 1 + offset] + 1 > fp[k + 1 + offset] ? 
        snake(k, fp[k - 1 + offset] + 1) : 
        snake(k, fp[k + 1 + offset])

        k -= 1
      end
      
      fp[delta + offset] = fp[delta - 1 + offset] + 1 > fp[delta + 1 + offset] ? 
      snake(delta, fp[delta - 1 + offset] + 1) : 
      snake(delta, fp[delta + 1 + offset])
    end
        
    return delta + 2 * p
  end
  module_function :onp

  def snake (k, y)
    x = y - k
    while x < @@M && y < @@N && @@ary1[x] == @@ary2[y] do
      x += 1
      y += 1
    end

    return y
  end
  module_function :snake

end


上記を「edit_distance.rb」として保存し、メインスクリプトがあるファイルのあるフォルダに置いた場合、サンプルはこんな感じになります。

require "edit_distance.rb"
str1 = "abc"
str2 = "ac"

p EditDistance.onp(str1.unpack("C*"), str2.unpack("C*"))
#===> 2