読者です 読者をやめる 読者になる 読者になる

セカイノカタチ

世界のカタチを探求するブログ。関数型言語に興味があり、HaskellやScalaを勉強中。最近はカメラの話題も多め

マーブルワーズ

「関数プログラミング 珠玉のアルゴリズムデザイン」をScalaで実装してみる 第4章

Haskell Scala プログラミング 関数型 技術書

前回: 「関数プログラミング 珠玉のアルゴリズムデザイン」をScalaで実装してみる 第3章 その2 - セカイノカタチ

さて、時間が開いてしまいましたが、第4章行きたいと思います。

関数プログラミング 珠玉のアルゴリズムデザイン

関数プログラミング 珠玉のアルゴリズムデザイン

この本のコードをScalaに置き換えていっているシリーズの第4弾です。

選択問題

タイトルからしてやヴぁいですが、内容は二つの集合を合わせた時にk番目に小さい要素を取り出すというものです。選択公理は関係なかったですね(ほっ)。

前提をまとめます。

  • 集合Xと集合Yは互いに素な有限集合
  • XとYの和集合の大きさはkより大きい
  • XYともに整列されたリストと配列について考える

詳しくは、本を読んでもらうとして、リストであればO(|X|+|Y|)ステップの計算時間になって、配列であればO(log|X|+log|Y|)ステップまで低減できるとのことです。

考え方

一番単純な方法はXY両方のリストの先頭から順に探索することで、答えを探します。

smallest :: Ord a => Int -> ([a],[a]) -> a
smallest k (xs, ys) = union (xs,ys) !! k

union (xs,[]) = xs
union ([],ys) = ys
union (x:xs, y:ys) | x < y = x : union (xs, y:ys)
                   | x < y = y : union (x:xs, ys)

Scalaで実装します。

def smallest[A:Ordering](k: Int, pair: (List[A],List[A])):A = {
  def union(pair:(List[A], List[A])): List[A] = {
    pair match {
      case (xs, Nil) => xs
      case (Nil, ys) => ys
      case (x::xs, y::ys) => if (implicitly[Ordering[A]].lt(x,y)) x :: union (xs, y::ys)
                             else y :: union (x::xs, ys)
    }
  }
  union(pair)(k)
}

簡単ですね。

分割統治

しかし、こんな方法でこの本が満足するはずがありません(log|X|になってないし)。

ここから伝家の宝刀分割統治が始まります。

まず、手始めにXとYをこのように分割して考えます。

xs ++ [a] ++ ys, us ++ [b] ++ vs

これは何かというと、両方の集合ととある点aとbでそれぞれ分割するということです。

ただし、aとbはa<bとなるように選択します(実際にはa>bが双対なので適当に選択してロジックを入れ替えます)。

こうして分割されたリストは、以下の様な感じになります。

ごちゃごちゃしていますが、色々冷静に考えるとkが、|xs ++ [a] ++ us|より小さければ、zone2(ys2)の可能性は無いと断定できるということです。

もし、kが|xs ++ [a] ++ us|より大きかった場合、逆にこんな感じになって、zone2(us1)の可能性はなくなります。

このように、2分割では無いですが、4分割して、そのうちの1つづつ削っていくイメージで答えに迫っていくというロジックになります。

コードは、こんな感じです。

smallest :: Ord a => Int -> ([a],[a]) -> a
smallest k ([],ws) = ws !! k
smallest k (zs,[]) = zs !! k
smallest k (zs,ws) =
  case (a < b,k <= p + q) of
    (True,True)   -> smallest k (zs,us)
    (True,False)  -> smallest (k-p-1) (ys,ws)
    (False,True)  -> smallest k (xs,ws)
    (False,False) -> smallest (k-q-1) (zs,vs)
  where p = (length zs) `div` 2
        q = (length ws) `div` 2
        (xs,a:ys) = splitAt p zs
        (us,b:vs) = splitAt q ws

かなり複雑になりましたね。(^^;

Scalaにします。

def smallest[A: Ordering](k: Int, pair: (List[A],List[A])):A = {
  pair match {
    case (Nil, ws) => ws(k)
    case (zs, Nil) => zs(k)
    case (zs, ws) =>
      val p = zs.length / 2
      val q = ws.length / 2
      val (xs,a::ys) = zs.splitAt(p)
      val (us,b::vs) = ws.splitAt(q)
      (implicitly[Ordering[A]].lt(a,b), k <= p + q) match {
        case (true,true)   => smallest(k, (zs,us))
        case (true,false)  => smallest(k-p-1, (ys,ws))
        case (false,true)  => smallest(k, (xs,ws))
        case (false,false) => smallest(k-q-1, (zs, vs))
      }
  }
}

可読性は・・・変わらないですね。

このバージョンでも、リストを走査する(!!)が線形時間となるため効率は良くなりません。

配列を使うと定数時間の(!)を使えるため、効率が対数時間に改善されます。

smallest' :: Ord a => Int -> (Array Int a, Array Int a) -> a
smallest' k (xa,ya) = search k (0, m + 1) (0, n + 1)
  where
    (0,m) = bounds xa
    (0,n) = bounds ya
    search k (lx, rx) (ly, ry)
      | lx == rx = ya ! (k + ly)
      | ly == ry = xa ! (k + lx)
  | otherwise =
      case (xa ! mx < ya ! my, k <= mx - lx + my - ly) of
        (True,True)   -> search k (lx,rx) (ly,my)
        (True,False)  -> search (k - (mx - lx) - 1) (mx + 1, rx) (ly, ry)
        (False,True)  -> search k (lx, mx) (ly, ry)
        (False,False) -> search (k - (my - ly) - 1) (lx,rx) (my + 1, ry)
      where mx = (lx + rx) `div` 2
            my = (ly + ry) `div` 2

更に複雑化したような。変わらないような。(^^;

Scala化します。

def search[A: Ordering](pair: (Vector[A],Vector[A]), k:Int, pairX:(Int,Int), pairY:(Int,Int)): A = {
  val (xa,ya) = pair
  val (lx,rx) = pairX
  val (ly,ry) = pairY
  val mx = (lx + rx) / 2
  val my = (ly + ry) / 2
  if (lx == rx) ya(k + ly)
  else if (ly == ry) xa(k + lx)
  else (implicitly[Ordering[A]].lt(xa(mx), ya(my)), k <= mx - lx + my - ly) match {
    case (true, true)    => search(pair, k, (lx,rx), (ly,my))
    case (true, false)   => search(pair, k - (mx - lx) - 1, (mx + 1, rx), (ly, ry))
    case (false, true)   => search(pair, k, (lx, mx), (ly, ry))
    case (false , false) => search(pair, k - (my - ly) - 1, (lx,rx), (my + 1, ry))
  }
}

def smallest2[A: Ordering](k: Int, pair: (Vector[A],Vector[A])):A = {
  val (xa,ya) = pair
  search(pair, k, (0, xa.length), (0, ya.length))
}

やっぱり、複雑です。

序文を引用すると「最後に出来たプログラムが珠玉なのではなく、最後のプログラムを導いた運算こそが珠玉なのである」との事なので、プログラム事態には価値は無いです(言い過ぎ)。

というか、それを言われてしまうとこの記事自体が・・・・。(^^;

おわりに

今回は、SPECIAL DAY Scala Hack-a-son 143th in KABUKIZA - Scala Meetup Group in Tokyo | Doorkeeperのネタとして実装をしました。

ホントは、2章分ぐらい進める予定だったのですが、難しいですね。(^^;

ともかく、記事が書けて良かったです。

おしまい。