Eliminating Amortizations PurelyFunctionalDataStrctures 輪講 第6回 2006/06/23 いなば ここまでの流れ (1) Imperative Queue (Ch2) リスト末尾を指すポインタをもっておきsnoc時に書換 1 2 3 ・ 4 ・ Persistent でない head, tail, snoc の最悪実行時間が O(1) ここまでの流れ (2) Functional Queue (Ch5) Front側とRear側の2つのリストで表現 |Front| == 0 になったら Rear を reverse 1 2 5 4 ・ 3 ・ Persistent である 最悪実行時間は、reverseが発生するとき O(n) 償却実行時間は (persistencyを必要としない使い方なら) O(1) ここまでの流れ (3) Functional Queue via Lazy Evaluation (Ch6) Front側とRear側の2つのリストで表現 |Front|+1 == |Rear| になったら遅延 reverse 1 2 5 4 ・ 3 ・ Persistent である 最悪実行時間は、reverseが発生するとき O(n) 償却実行時間は (persistencyを必要とする使い方でも) O(1) 今日の話 Functional Real-Time Queue (Ch7) ? Persistent である 最悪実行時間は、O(1) 償却実行時間も当然、 O(1) Real-Time Queues: 基本的考え 6章の遅延評価を使ったQueueでは 「借金」 = 「suspensionをほどく計算のコスト」 “reverse” の suspension に仮想的な「借金」を割当 借金額 =非共有コスト = そのsuspensionをforceした時実行される計算時間 = reverse時点のRearの長さ 実際にsuspensionがforceされるに至るまでの各演 算に、仮想的に借金を定数額ずつ「返済」させる reveseは|Front| = |Rear| のとき行われるので、 |Front| 回 tail した後にforceされる。よって、1回のtailで 1ずつ借金を返済 Real-Time Queues: 基本的考え Real-Time Queue では 「借金」 = 「suspensionをほどく計算のコスト」 各 tail 演算で、実際に「借金」を「返済」する つまり、tailのたびに、reverseのsuspensionを 少しずつ(定数時間ずつ)実行しておく Real-Time Queues OCaml で実装してみました MyRTQ.ml ポイント 「|Front|+1 == |Rear| になったらreverse」になってるか 全てのsuspensionは定数時間でforce可能か Amortization除去 6章のQueueとの差 Rearの逆転処理 f @ rev r rotate f r [] rev は incremental でない(forceすると全体を計算してし まう)ので、「実際に少しずつ借金を返す」ができない incremental。少しずつ計算できる。 Schedule, exec 「実際に少しずつ計算」するための「次やる計算」のリ ストと、それを実際に少し実行する処理 Amortization除去 一般的なステップ (1) Monolithic な suspension を、工夫して incremental なものに置き換える ① 各 suspension の「内部コスト」を、許される計算量 制限の範囲におさえる 例 : @rev → rotate, 内部コスト=O(1) 内部コスト(internal cost) suspension の内部コストとは、「依存している他の全ての suspension が既に評価済みで値をO(1)で取り出せる」と 仮定した時にかかる計算コストのこと (「suspendされている計算の非共有コスト」と同義?) Amortization除去 一般的なステップ (2) 各suspensionが評価されるよりも前に、その suspensionが依存する他のsuspensionが評 価済みとなることを保証する ② 例 : schedule変数 Exercise 7.1 6章の @rev を rotate に置き換えると(Schedulingを 行わなくても)、snoc, head, tail の最悪実行時間は O(n) から O(log n) に減る。これを示せ Ans Scheduleが関与したのはrotateの第一引数のforce。 このsuspensionの依存の深さは、連続でsnocし続け た時最大になるが、たかだかO(log n) Exercise 7.2 Real-time Queue の要素数を求めたい。 ただし、|f| + |r| で求めるよりも、|r| と |s| を使う方が効率がよい。 求め方と効率について考察せよ Ans. |s| = 前回のrotate時の要素数 – tail回数 – snoc回数 |r| = snoc回数 ∴要素数 =前回のrotate時の要素数 - tail回数 + snoc回数 = |s| + 2 |r| |f|+|r| > |r|+|s| なのでその分だけ速い? それだけだろうか? 今日の話その2 Binomial Heaps (Ch3) Binomial Heaps (Ch5) Physicist’s Method で、 persistencyを必要としない使い方な ら insert : 償却 O(1) が示せる Lazy Binomial Heaps (Ch6) insert, deleteMin, findMin, merge : 最悪 O(log n) リストを遅延リストに変えれば、persistency を使う場合でも insert : 償却 O(1) にできる Amortizationの除去 (Ch7) insert を 最悪 O(1) にできる Binomial Heaps: おさらい 自然数の「2進数表現」に類似したデータ構造 サイズ 2a1+2a2+...+2an のHeapは、 [サイズ2a1の木、サイズ2a2の木、…、サイズ2anの木] のリストで表現 Binomial Heaps: おさらい type tree = T of int * Elt.t * tree list type heap = tree list insert e insTree t | [] | hd::tl h = insTree (T(0,e,[])) h h = match h with -> [t] -> if rank t < rank hd then t::h else insTree (link t hd) tl やるべきこと 「少しずつ実行したいsuspension」 が monolithicなら incremental に書き換える ① ② 内部コストが欲しい計算量におさまるように monolithic な insTree を incremental, O(1) に Scheduling insert e insTree t | [] | hd::tl h = insTree (T(0,e,[])) h h = match h with -> [t] -> if rank t < rank hd then t::h else insTree (link t hd) tl 1: Incremental 化 もっと2進数に近い表現を type tree = T of Elt.t * tree list type bit = Zero | One of tree type heap = bit list [, ] [ ,, , ] 0 0 1: Incremental 化 もっと2進数に近い表現を insTree t h = match h with | [] -> [One t] | Zero::tl -> (One t) :: tl | (One hd)::tl -> Zero :: insTree (link t hd) tl 1: Incremental 化 もっと2進数に近い表現を Lazy版 insTree t h = match force h with | SNil -> lazy (SCons(One t, lazy(SNil))) | SCons(Zero, tl) -> lazy (SCons(One t, tl)) | SCons(One hd, tl) -> lazy (SCons(Zero, insTree (link t hd) tl)) 2: Scheduling 「insTreeの作るsuspensionは全て、 評価される前にその依存するsuspensionが評 価済みになっているようにする」 insTree t h = match force h with | SNil -> lazy (SCons(One t, lazy(SNil))) | SCons(Zero, tl) -> lazy (SCons(One t, tl)) | SCons(One hd, tl) -> lazy (SCons(Zero, insTree (link t hd) tl)) 2: Scheduling insert のみで考える(他の演算はあとで) 1 0 0 0 1 1 0 0 1 0 1 0 1 0 1 1 1 1 1 0 1 0 0 1 0 0 1 1 1 0 1 1 0 1 1 0 1 1 0 1 1 1 1 1 1 1 1 1 1 作られるsuspension 左図の黄色の部分 満たすべき条件 「ある bit が0に評価さ れる時点までには、そ の右上のマスの bit は 評価済み」 2: Scheduling insert のみで考える(他の演算はあとで) 1 0 0 0 1 1 0 0 1 0 1 0 1 0 1 1 1 1 1 0 1 0 0 1 0 0 1 1 1 0 1 1 0 1 1 0 1 1 0 1 1 1 1 1 1 1 1 1 1 type schd = bit stream list type heap = bit stream * schd let exec (bs::bsl) = match force bs with | SCons(Zero, t) -> t::bsl | SCons(One _,_) -> bsl let insert x (bs,sc) = let bs = insTree ... bs in (bs, exec (exec (bs::sc))) 2: Scheduling Q: なぜ2回execするのか A: 6章での解析によれば、 Amortized Costは2だったので。 2: Scheduling (Thm7.1) Schedule 1回のinsertにつき2回execすれば 常に次が満たされる 0 0 0 0 0 0 1 1 0 0 0 0 0 0 1 0 0 0 0 1 0 1 … 0 0 … 0 0 0 1 0 1 0 1 0 0 0 1 0 1 0 1 Schedule内で未評価 rangeに重なりはない 隣り合う未評価range の間には必ず評価済 のZeroが1個はある 最左の未評価rangeの 左には必ず評価済の Zeroが2個はある 現在のHeap 2: Scheduling (Thm7.1) 証明 帰納法による 初期状態 (empty) は条件を満たす → 自明 条件を満たす状態で insert を1回実行しても条件を 満たす insert すると最左のZeroがOneに変わる 場合分け(4通り) [1] が入る ケースB 証明:場合分け4通り [1] が入る ケースA 0 0 1 0 0 insert 1 1 insert 1 0 1 0 0 0 1 0 0 1 1 0 1 1 0 0 1 1 0 0 0 1 1 0 0 1 0 0 1 0 1 exec × 2 0 1 0 insert 1 exec × 2 0 1 [0,1] が入 る ケース 0 insert 1 exec × 2 1 exec × 2 1 1 0 0 1 0 1 [0,...,0,1] が入る ケース その他の演算 deleteMin, findMin, merge O(log n) で十分なので、全ての未消化 schedule を 実行して、普通のリストとして操作すればよい insert での解析から、未消化 schedule はたかだか O(log n) 個である MyBinHeap2.ml まとめ “Lazy” “Amortized” “Persistent” データ構造を、 “Lazy” “WorstCase” “Persistent” に変える手法 ① ② ③ 遅延評価する部分を incremental にする 小分けにした suspension を “Schedule” として管理 各演算ごとに、解析された償却計算量に従った回数分 Schedule を実行
© Copyright 2024 ExpyDoc