8.4.3 Real-Time Deques

      2012-08-04 (初版)
      2012-08-14 (2版)
         @yuga
目次
•   狙い
•   実装
•   解析
•   Exercise 8.7
•   おまけ
狙い
• すべての操作が最悪実行時間O(1)であるDequeを作ります。
• 8.4.2で登場したBankersDequeを拡張して実現します。
実装
• BankersDqeueはAmortized Boundなデータ構造であるので、
  7章で学習した手順でWorst-Case Boundなデータ構造へと変
  換していきます。

  1.   Monolithic関数をIncremental関数に変換します
  2.   Dequeの各操作ごとにサスペンションを少しずつ実行するスケジュー
       ルを導入します
BankersDequeで使用したmonolithic関数
BankersDqueでは、フロントとリアの2つのStreamを、以下のように操作してローテーション
を行っていました。

         1.      長い方のStreamを全体の半分の長さだけ残す
                     – 残す方をtakeで取得
                     – 半分を超える部分をdropで作成

         2.      半分を超える部分をreverseして短い方のstreamと結合
                 (これで2つのstreamが同じ長さになる)
                 –    半分を超える部分をreverse
                 –    短い方のリストを前にしてreverseした結果と++

         Queueの場合はリアを空にしてフロントにすべて寄せていましたが、Dequeではフロントとリアの長さを同じ
         にします。

|f| < |r| の場合:

    val r’ = take (j, r)
    val f’ = f ++ reverse (drop (j, r))

ここに登場したtake/drop/reverseの各関数は、4章で定義されたものです。
Streamの実装(参考)
P36 の Figure 4.1 より                     サスペンションを1つ潰すが、
                                        新しいStreamのために構築したサスペンショ
                                        ンで、後続の処理を遅延させる
  fun lazy   ($NIL) ++ t = t
         |   ($CONS (x, s)) ++ t = $CONS (x, s ++ t)           incremental関数
  fun lazy   take (0, s) = $NIL
         |   take (n, $NIL) = $NIL
         |   take (n, $CONS (x, s)) = $CONS (x, take (n-1, s))

  fun lazy drop (n, s) =
        let fun drop’ (0, s) = s
              | drop’ (n, $NIL) = $NIL
              | drop’ (n, $CONS (x, s)) = drop’ (n-1, s)
        in drop’ (n, s) end
  fun lazy reverse s =                                       monolithic関数
        let fun reverse’ ($NIL, r) = r
              | reverse’ ($CONS (x, s), r) = reverse’ (s, $CONS (x, r))
        in reverse’ (s, $NIL) end

                                      既存のサスペンションをすべて潰しながら、
                                      新たなStreamを終端から構築する
monolithic関数からincremental関数へ

BankersDequeでのStream操作を分類します。
  – Incremental関数
     • take
     • ++
  – monolithic関数
     • reverse
     • drop


以上から、RealTimeDeque専用のdropとreverseを用意します。
  – rotateDrop
  – rotateRev
rotateDrop / rotateRevの戦略
•   Incremenetal関数への変換
    – Streamに対する処理を分割して実行し、毎回一定数(𝑐 > 1)ずつ処理を行う
      ようにします。

      ⇒ 一定数ずつ処理することで、1回の処理時間がO(1)になる。


•   スケジュールの導入
    – ローテーションの結果として新規作成されるStreamのデータ構築子$CONSを
      サスペンションとして、その内側に関数の呼び出しを配置して実行を遅延しま
      す。
    – Sectio7.2のRealTimeQueueと同様に、 Streamを先頭から順に開いていくこと
      でサスペンションを潰していく、スケジュール機構を構築します。
rotateDrop
1実行ごとに c ずつdropします。rotateDropの各回の呼び出しは、短い方の
Streamを再作成する$CONSの内側に配置します。

    fun rotateDrop (f, j, r)=
          if j < c then rotateRev (f, drop (j, r), $NIL)             ①
          else let val ($CONS (x, f’)) = f
               in $CONS (x, rotateDrop (f’, j – c, drop (c, r))) end ②




                                       ②

                                       ①            c=3 の場合
rotateRev
1実行ごとに c ずつreverseします。rotateDropの各回の呼び出しは、短い方
のStreamを再作成する$CONSの内側に配置します。

    fun rotateRev ($NIL, r, a) = reverse r ++ a
      | rotateRev ($CONS (x, f), r, a) =
          $CONS (x, rotateRev (f, drop (c, r), reverse (take (c, r)) ++ a))




                                                        c=3 の場合
ローテーション後のフロントとリア
新たに作成されたStreamに内包されて処理が遅延しています。
サスペンションはフロントとリアの両方に作成されます。

                      フロント                    リア

               rotateRev が作成

   rotateDrop が作成               ++ が作成     take が作成




          drop         drop        ++         take
       rotateDrop      take
                     reverse                           Streamの$CONSで
                        ++
                                                       遅延している処理
                    rotateRev

                                ++ や take 自身が再帰して作成す
                                るので、他の処理を内包できない
                                (専用のを作れば別だろうけど)
スケジュールの導入
ローテーション実行中にrotateDrop関数と rotateRev関数が作成したサスペンションを、
次回のローテーション開始までに全て実行しなければなりません。

このためにスケジュールを導入します。
  type ‘a queue = int * ‘a Stream * ‘a Stream   (* front *)
                * int * ‘a Stream * ‘a Stream   (* rear *)

          データサイズ           データ           スケジュール

ローテーションで新しいフロントとリアができるので、そのままスケジュールとします。
  let j = (lenf + lenr) / 2; let i = lenf + lenr –j;
  let r' = S.take (j, r); let f' = rotateDrop (f, j, r)
  in (i, f', f', j, r', r')


Streamを順に開いていくことでサスペンションを実行します。
  fun exec1 ($CONS (x, s)) = s
    | exec1 s = s
解析
短い方のStreamもとに作成する$CONSで rotateDrop と rotateRev を遅延させ
ることができるように、Balance Invariantを設定します。

•   Balance Invariant:
           𝑓 ≤ 𝑐× 𝑟 +1        𝑟 ≤ 𝑐 × 𝑓 + 1 (BankersDequeと同じ)


•   上記から、ローテーション開始時のフロントとリアの長さ:
                  短い方 = 𝑚
                  長い方 = 𝑐 × 𝑚 + 𝑘 + 1       𝑤ℎ𝑒𝑟𝑒 1 ≤ 𝑘 ≤ 𝑐

    例)
     c=3の場合、以下のような状態と操作でローテーションが開始する
        k=1: |f|=5, |r|=16 のときの snoc、|f|=6, |r|=17 のときの tail
        k=2: |f|=6, |r|=18 のときの tail
        k=3: |f|=6, |r|=19 のときの tail
解析
定数 c の範囲を求めます。

•   定数 c の範囲
    – 定数 j を長い方から短い方へ移動する長さとする。

    – rotateRev 開始時に以下が成立する。
         長い方 ≥ 𝑐 × 短い方 + 1 + 𝑘 − 𝑗 𝑚𝑜𝑑 𝑐

    –   長い方 ≥ 𝑐 × 短い方 , 1 ≤ 𝑘 ≤ 𝑐, 𝑐 > 1 なので、
         1 + 𝑘 − 𝑗 𝑚𝑜𝑑 𝑐 ≥ 0
         1 + 1 − 𝑐 − 1 ≥ 0 (k の最小値と j mod c の最大値を代入)
         𝑐≤3

    –   𝑐 = 2 または 𝑐 = 3
解析
•   ローテーション後のフロントとリアの長さ:
                                           𝑓 + |𝑟|
               ローテーション前に短かった方 =
                                             2
                                           𝑓 + |𝑟|
               ローテーション前に長かった方 =
                                             2

•   次回ローテション実行までの最短操作回数
                                           1
    – Init / tail のどちらかを連続実行します(cons / snoc よりも 倍の回数ですむ)。
                                           𝑐
    – ローテーション後のフロント、リアの長さをともに n とすると

                 𝑐 × 𝑛 − 最短操作回数 + 2 ≤ 𝑛
                               𝑛−2
                 𝑛 − 最短操作回数 ≤
                                𝑐
                               𝑛−2   𝑛+2   𝑛
                 最短操作回数 ≥ 𝑛 −      =     >
                                𝑐      𝑐   𝑐
      𝑛+2                                        𝑛
         の項が最大になるのは𝑐 = 2のときで、init / tail を最短で約 回実行すると、次のロー
       𝑐                                      2
      テーションが発生します。
Exercise 8.7
•   ローテーションで作成されるサスペンション数は、フロントとリアともにStream
    の長さと一致します。

•   Debits Invariantを以下のように設定します。
    – ローテーション開始時の 𝑐 × 短い方 + 1 + 𝑘 ≥ 長い方 𝑤ℎ𝑒𝑟𝑒 1 ≤ 𝑘 ≤ 𝑐 から、
       一方の𝑆𝑡𝑟𝑒𝑎𝑚の未評価サスペンション数 ≤ 𝑐 × 短い方 + 2 − 長い方

      さらに次回ローテション実行までの最短操作回数の考察から c = 2 を代入して、
      一方のStreamの未評価サスペンション数 ≤ 2 × 短い方 + 2 − 長い方


•   以上から
    – ローテーション実行直後、短い方と長い方の長さは高々1違うだけなので、Debits Invariantを
      満たしている。
    – Dequeから削除するとき、短い方が1短くなっても、2つのStreamのいずれも、サスペンション
      を2減らせばDebits Invariantを維持できる。
    – Dequeに追加するとき、長い方が1短くなっても、2つのStreamのいずれも、サスペンションを1
      減らせばDebits Invariantを維持できる。
おまけ: RealTimeDequeの動作例
例) Dequeの操作とローテーション

   1. c = 3, |f| = 3, |r| = 9 の RealTimeDeque q1 に対して
       •   snoc q1 x
           ⇒ |f| = 3, |r| = 10
       •   snoc (snoc q1 x) y
           ⇒ |f| = 3, |r| = 11
           ⇒ ローテーション
           ⇒ |f| = 7, |r| = 7

   2. c=3, |f| = 7, |r| = 18 の RealTimeDeque q2 に対して
       •   tail q2
           ⇒ |f| = 6, |r| = 18
       •   tail (tail q2)
           ⇒ |f| = 5, |r| = 18
           ⇒ ローテーション
           ⇒ |f| = 12, |r| = 11


OCamlによるサンプルコード:
   https://gist.github.com/3271021
おまけ: Global Rebuildingとの違い
だいたいこんな感じだと思います。

•   Global Rebuildingには必要だったローテーション用のコピーが、Lazy Rebuildingで
    は不要。
•   Global Rebuildingを使用したHood MelvilleQueueでは、ローテーションに必要な処
    理が比較的均等に分散していたが、Lazy Rebuildingを使用したRealTimeDequeで
    は、短い方のStreamを再作成する$CONSだけを使ってdropやreverse遅延させて
    いるので、サスペンションの実行コストに偏りがある。
参考文献
•   Chris Okasaki, “8.4.3 Real-Time Deque”, Purely Functional Data Structures,
    Cambridge University Press (1999)
•   Chris Okasaki, “Simple and efficient purely functional queues and deques”, Journal
    of Functional Programming, 5(4):583-592, October 1995

PFDS 8.4.3 Real-Time Deques

  • 1.
    8.4.3 Real-Time Deques 2012-08-04 (初版) 2012-08-14 (2版) @yuga
  • 2.
    目次 • 狙い • 実装 • 解析 • Exercise 8.7 • おまけ
  • 3.
  • 4.
    実装 • BankersDqeueはAmortized Boundなデータ構造であるので、 7章で学習した手順でWorst-Case Boundなデータ構造へと変 換していきます。 1. Monolithic関数をIncremental関数に変換します 2. Dequeの各操作ごとにサスペンションを少しずつ実行するスケジュー ルを導入します
  • 5.
    BankersDequeで使用したmonolithic関数 BankersDqueでは、フロントとリアの2つのStreamを、以下のように操作してローテーション を行っていました。 1. 長い方のStreamを全体の半分の長さだけ残す – 残す方をtakeで取得 – 半分を超える部分をdropで作成 2. 半分を超える部分をreverseして短い方のstreamと結合 (これで2つのstreamが同じ長さになる) – 半分を超える部分をreverse – 短い方のリストを前にしてreverseした結果と++ Queueの場合はリアを空にしてフロントにすべて寄せていましたが、Dequeではフロントとリアの長さを同じ にします。 |f| < |r| の場合: val r’ = take (j, r) val f’ = f ++ reverse (drop (j, r)) ここに登場したtake/drop/reverseの各関数は、4章で定義されたものです。
  • 6.
    Streamの実装(参考) P36 の Figure4.1 より サスペンションを1つ潰すが、 新しいStreamのために構築したサスペンショ ンで、後続の処理を遅延させる fun lazy ($NIL) ++ t = t | ($CONS (x, s)) ++ t = $CONS (x, s ++ t) incremental関数 fun lazy take (0, s) = $NIL | take (n, $NIL) = $NIL | take (n, $CONS (x, s)) = $CONS (x, take (n-1, s)) fun lazy drop (n, s) = let fun drop’ (0, s) = s | drop’ (n, $NIL) = $NIL | drop’ (n, $CONS (x, s)) = drop’ (n-1, s) in drop’ (n, s) end fun lazy reverse s = monolithic関数 let fun reverse’ ($NIL, r) = r | reverse’ ($CONS (x, s), r) = reverse’ (s, $CONS (x, r)) in reverse’ (s, $NIL) end 既存のサスペンションをすべて潰しながら、 新たなStreamを終端から構築する
  • 7.
    monolithic関数からincremental関数へ BankersDequeでのStream操作を分類します。 –Incremental関数 • take • ++ – monolithic関数 • reverse • drop 以上から、RealTimeDeque専用のdropとreverseを用意します。 – rotateDrop – rotateRev
  • 8.
    rotateDrop / rotateRevの戦略 • Incremenetal関数への変換 – Streamに対する処理を分割して実行し、毎回一定数(𝑐 > 1)ずつ処理を行う ようにします。 ⇒ 一定数ずつ処理することで、1回の処理時間がO(1)になる。 • スケジュールの導入 – ローテーションの結果として新規作成されるStreamのデータ構築子$CONSを サスペンションとして、その内側に関数の呼び出しを配置して実行を遅延しま す。 – Sectio7.2のRealTimeQueueと同様に、 Streamを先頭から順に開いていくこと でサスペンションを潰していく、スケジュール機構を構築します。
  • 9.
    rotateDrop 1実行ごとに c ずつdropします。rotateDropの各回の呼び出しは、短い方の Streamを再作成する$CONSの内側に配置します。 fun rotateDrop (f, j, r)= if j < c then rotateRev (f, drop (j, r), $NIL) ① else let val ($CONS (x, f’)) = f in $CONS (x, rotateDrop (f’, j – c, drop (c, r))) end ② ② ① c=3 の場合
  • 10.
    rotateRev 1実行ごとに c ずつreverseします。rotateDropの各回の呼び出しは、短い方 のStreamを再作成する$CONSの内側に配置します。 fun rotateRev ($NIL, r, a) = reverse r ++ a | rotateRev ($CONS (x, f), r, a) = $CONS (x, rotateRev (f, drop (c, r), reverse (take (c, r)) ++ a)) c=3 の場合
  • 11.
    ローテーション後のフロントとリア 新たに作成されたStreamに内包されて処理が遅延しています。 サスペンションはフロントとリアの両方に作成されます。 フロント リア rotateRev が作成 rotateDrop が作成 ++ が作成 take が作成 drop drop ++ take rotateDrop take reverse Streamの$CONSで ++ 遅延している処理 rotateRev ++ や take 自身が再帰して作成す るので、他の処理を内包できない (専用のを作れば別だろうけど)
  • 12.
    スケジュールの導入 ローテーション実行中にrotateDrop関数と rotateRev関数が作成したサスペンションを、 次回のローテーション開始までに全て実行しなければなりません。 このためにスケジュールを導入します。 type ‘a queue = int * ‘a Stream * ‘a Stream (* front *) * int * ‘a Stream * ‘a Stream (* rear *) データサイズ データ スケジュール ローテーションで新しいフロントとリアができるので、そのままスケジュールとします。 let j = (lenf + lenr) / 2; let i = lenf + lenr –j; let r' = S.take (j, r); let f' = rotateDrop (f, j, r) in (i, f', f', j, r', r') Streamを順に開いていくことでサスペンションを実行します。 fun exec1 ($CONS (x, s)) = s | exec1 s = s
  • 13.
    解析 短い方のStreamもとに作成する$CONSで rotateDrop とrotateRev を遅延させ ることができるように、Balance Invariantを設定します。 • Balance Invariant: 𝑓 ≤ 𝑐× 𝑟 +1 𝑟 ≤ 𝑐 × 𝑓 + 1 (BankersDequeと同じ) • 上記から、ローテーション開始時のフロントとリアの長さ: 短い方 = 𝑚 長い方 = 𝑐 × 𝑚 + 𝑘 + 1 𝑤ℎ𝑒𝑟𝑒 1 ≤ 𝑘 ≤ 𝑐 例) c=3の場合、以下のような状態と操作でローテーションが開始する k=1: |f|=5, |r|=16 のときの snoc、|f|=6, |r|=17 のときの tail k=2: |f|=6, |r|=18 のときの tail k=3: |f|=6, |r|=19 のときの tail
  • 14.
    解析 定数 c の範囲を求めます。 • 定数 c の範囲 – 定数 j を長い方から短い方へ移動する長さとする。 – rotateRev 開始時に以下が成立する。 長い方 ≥ 𝑐 × 短い方 + 1 + 𝑘 − 𝑗 𝑚𝑜𝑑 𝑐 – 長い方 ≥ 𝑐 × 短い方 , 1 ≤ 𝑘 ≤ 𝑐, 𝑐 > 1 なので、 1 + 𝑘 − 𝑗 𝑚𝑜𝑑 𝑐 ≥ 0 1 + 1 − 𝑐 − 1 ≥ 0 (k の最小値と j mod c の最大値を代入) 𝑐≤3 – 𝑐 = 2 または 𝑐 = 3
  • 15.
    解析 • ローテーション後のフロントとリアの長さ: 𝑓 + |𝑟| ローテーション前に短かった方 = 2 𝑓 + |𝑟| ローテーション前に長かった方 = 2 • 次回ローテション実行までの最短操作回数 1 – Init / tail のどちらかを連続実行します(cons / snoc よりも 倍の回数ですむ)。 𝑐 – ローテーション後のフロント、リアの長さをともに n とすると 𝑐 × 𝑛 − 最短操作回数 + 2 ≤ 𝑛 𝑛−2 𝑛 − 最短操作回数 ≤ 𝑐 𝑛−2 𝑛+2 𝑛 最短操作回数 ≥ 𝑛 − = > 𝑐 𝑐 𝑐 𝑛+2 𝑛 の項が最大になるのは𝑐 = 2のときで、init / tail を最短で約 回実行すると、次のロー 𝑐 2 テーションが発生します。
  • 16.
    Exercise 8.7 • ローテーションで作成されるサスペンション数は、フロントとリアともにStream の長さと一致します。 • Debits Invariantを以下のように設定します。 – ローテーション開始時の 𝑐 × 短い方 + 1 + 𝑘 ≥ 長い方 𝑤ℎ𝑒𝑟𝑒 1 ≤ 𝑘 ≤ 𝑐 から、 一方の𝑆𝑡𝑟𝑒𝑎𝑚の未評価サスペンション数 ≤ 𝑐 × 短い方 + 2 − 長い方 さらに次回ローテション実行までの最短操作回数の考察から c = 2 を代入して、 一方のStreamの未評価サスペンション数 ≤ 2 × 短い方 + 2 − 長い方 • 以上から – ローテーション実行直後、短い方と長い方の長さは高々1違うだけなので、Debits Invariantを 満たしている。 – Dequeから削除するとき、短い方が1短くなっても、2つのStreamのいずれも、サスペンション を2減らせばDebits Invariantを維持できる。 – Dequeに追加するとき、長い方が1短くなっても、2つのStreamのいずれも、サスペンションを1 減らせばDebits Invariantを維持できる。
  • 17.
    おまけ: RealTimeDequeの動作例 例) Dequeの操作とローテーション 1. c = 3, |f| = 3, |r| = 9 の RealTimeDeque q1 に対して • snoc q1 x ⇒ |f| = 3, |r| = 10 • snoc (snoc q1 x) y ⇒ |f| = 3, |r| = 11 ⇒ ローテーション ⇒ |f| = 7, |r| = 7 2. c=3, |f| = 7, |r| = 18 の RealTimeDeque q2 に対して • tail q2 ⇒ |f| = 6, |r| = 18 • tail (tail q2) ⇒ |f| = 5, |r| = 18 ⇒ ローテーション ⇒ |f| = 12, |r| = 11 OCamlによるサンプルコード: https://gist.github.com/3271021
  • 18.
    おまけ: Global Rebuildingとの違い だいたいこんな感じだと思います。 • Global Rebuildingには必要だったローテーション用のコピーが、Lazy Rebuildingで は不要。 • Global Rebuildingを使用したHood MelvilleQueueでは、ローテーションに必要な処 理が比較的均等に分散していたが、Lazy Rebuildingを使用したRealTimeDequeで は、短い方のStreamを再作成する$CONSだけを使ってdropやreverse遅延させて いるので、サスペンションの実行コストに偏りがある。
  • 19.
    参考文献 • Chris Okasaki, “8.4.3 Real-Time Deque”, Purely Functional Data Structures, Cambridge University Press (1999) • Chris Okasaki, “Simple and efficient purely functional queues and deques”, Journal of Functional Programming, 5(4):583-592, October 1995