Contenu connexe
Similaire à PFDS 10.2.1 lists with efficient catenation
Similaire à PFDS 10.2.1 lists with efficient catenation (20)
PFDS 10.2.1 lists with efficient catenation
- 1. PFDS #11
10.2.1
Lists With Efficient Catenation
@yuga
2012-11-03
Copyright © 2012 yuga 1
- 2. 目次
Structural Abstractionを用いた1つめの実装例として、
Catenable List を取り上げます。
定義: 何を作る
実装: どう作る
解析: どんなものなのか
Copyright © 2012 yuga 2
- 3. 定義
何を作る
Copyright © 2012 yuga 3
- 4. 実現する操作
以下の定義を実装したリストを作ります。
定義(CATENABLELISTシグネチャ):
module type CATENABLELIST = sig
type ‘a t
val empty : ‘a t
val isEmpty : ‘a t -> bool
val cons : ‘a * ‘a t -> ‘a t
val snoc : ‘a t * ‘a -> ‘a t
val (++) : ‘a t -> ‘a t -> ‘a t
val head : ‘a t -> ‘a
val tail : ‘a t -> ‘a t
end
= Output Restricted Queues + (++)
Copyright © 2012 yuga 4
- 5. 実行時間はO(1)
CatenableListはすべての操作をO(1)時間で実現します。
通常のリスト同士の連結操作(++)はO(n)時間かかるのに対し、
Catenable ListsではO(1)時間で可能になるのが特徴です。
Persistentな使い方をしても問題ないものにします。
Copyright © 2012 yuga 5
- 6. 実行時間はAmortized time
でも、ごめんなさい。
Worst-Case timeではなくて、Amortized timeです。
Worst-CaseなCatenable Listsは11章に出てくるRecursive
Slow-Downというテクニックを使って実現できます。
– Persistent Lists with Catenation via Recursive Slow-Down
– こっちの方が先行して世に登場
今回扱うCatenable Listsは後発ですが、Worst-Caseなリスト
より実装が簡単になっています。
Copyright © 2012 yuga 6
- 7. 実装
どう作る
Copyright © 2012 yuga 7
- 8. 実装のアイデア
効率の良い連結関数を作るため、リスト内部にqueueを設け
て、その中に相手リストを格納します。
1 6
連結
2 4
++ 7 8
○はリストの
要素
3 9
青枠は
queue
1 6 1
2 4 7 8 2 4 6
3 9 3 7 8
註: 空のqueueを省略しています 9
Copyright © 2012 yuga 8
- 9. 実装のアイデア
Queueを新たに実装するのは本節の対象外です。以下の要件
を満たしていれば何でも良いので、既存のものを利用します。
QUEUEシグネチャを満たしている
module type QUEUE = sig
type ‘a t
val empty : ‘a t
val isEmpty : ‘a t -> bool
val snoc : ‘a t * ‘a -> ‘a t
val head : ‘a t -> ‘a
val tail : ‘a t -> ‘a t
end
すべての操作が Worst-Case / Amortized 関係なくO(1)時間で実行可能
Persistentな使い方をしても問題ない
Copyright © 2012 yuga 9
- 10. PFDSに出てきたqueue
これまでに登場したqueueには以下のものがありました。
Section Name Cost Persistent
5.2 BatchedQueue O(n) worst-case time NG
6.3.2, BankersQueue O(1) amortized time OK
8.3
6.4.2 PhysicistsQueue O(1) amortized time OK
7.2 RealTimeQueue O(1) worst-case time OK
8.3 HoodMelvilleQueue O(1) worst-case time OK
10.1.3 BootStrappedQueue O(1) amortized time OK
⇒ BatchedQueue以外なら良さそうです。
Copyright © 2012 yuga 10
- 11. Structural Abstractionテンプレートを使って実装開始
10.2節に登場したテンプレートを参考に進めます。
‘a c : Primitive type Queue
’a b : Bootstrapped type CatenableList
type ‘a b = E
| B of ‘a * ‘a b c
let unit_b x = B (x, empty_b)
let insert_b = function
| (x, E) -> B (x, empty_c)
| (x, B (y, c)) -> B (x, insert_c (unit_b y, c))
let join_b = function
| (b, E) -> b
| (E, b) -> b _c は ‘a c
| (B (x, c), b) -> B (x, insert_c (b, c)) _b は ‘a b の関数
Copyright © 2012 yuga 11
- 12. 実装: empty / isEmpty
最初に、データ構造として空リストだけ定義します。
ここではCatenableListの型を t とします。
module CatenableList : CATENABLELIST = struct
type ‘a t = E
| …
…
end
emptyとisEmptyの実装はデータコンストラクタを見るだけです。
let empty = E
let isEmpty = function
| E -> true
| _ -> false
Copyright © 2012 yuga 12
- 13. 実装: ++ rev.1
QUEUEシグネチャを実装したモジュールをQという名前で受け取
ります。ここではqueueの型を t として、リストのデータ構造を定
義します。
module CatenableList (Q : QUEUE) : CATENABLELIST = struct
type ‘a t = E
| C of ‘a * ‘a t Q.t
…
end
(++)は、1つめのリストのqueueに2つめのリストを格納します。
let (++) xs ys = match (xs, ys) with
| (E, ys) -> ys
| (xs, E) -> xs
| (C (x, q), ys) -> C (x, Q.snoc (q, ys))
Copyright © 2012 yuga 13
- 14. 実装: ++ rev.2
4行目は、あとで他の関数からも利用するので、ヘルパー関数link
にくくりだします。
module CatenableList (Q : QUEUE) : CATENABLELIST = struct
type ‘a t = E
| C of ‘a * ‘a t Q.t
let link (C (x, q), ys) -> C (x, Q.snoc (q, ys))
let (++) xs ys = match (xs, ys) with
| (E, ys) -> ys
| (xs, E) -> xs
| (xs, ys) -> link (xs, ys)
…
end
Copyright © 2012 yuga 14
- 15. 実装: cons / snoc
consとsnocは、さきほど実装した(++)を使えば簡単です。
let cons (x, xs) = C (x, Q.empty) ++ xs
let snoc (xs, x) = xs ++ C (x, Q.empty)
Copyright © 2012 yuga 15
- 16. 図解: cons / snoc
consとsnocをそれぞれ3回繰り返した結果です。
3
cons cons 2 cons
1
2
E 1
1
1 2 3
snoc snoc snoc
1
1 1
E
2 2 3
Copyright © 2012 yuga 16
- 17. 実装: head
headは先頭を取り出すだけです。
exception Empty
let head = function
| E -> raise Empty
| C (x, _) -> x
Copyright © 2012 yuga 17
- 18. 実装: tail rev.1
tailは先頭をすてて、queueの中身をリスト状につなぎなおします。
exception Empty
let link (C (x, q), ys) -> C (x, Q.snoc (q, ys))
let linkAll q = linkは再掲
let x = Q.head q in
let q’ = Q.tail q in
if Q. isEmpty q’ then x else link (t, linkAll q’)
let tail = function
| E -> raise Empty
| C (_, q) -> if Q.isEmpty q then q else linkAll q
Copyright © 2012 yuga 18
- 19. 図解: tail
tailによりqueueの中身がつなぎなおされる過程です。
最初にheadが取り除かれます。
1
2 5 6 2 5 6
3 4 7 8 3 4 7 8
9 9
Copyright © 2012 yuga 19
- 20. 図解: tail
続いてqueueをほどいていきます。
2 5 6 2 5 6
3 4 7 8 3 4 7 8
9 9
Copyright © 2012 yuga 20
- 21. 図解: tail
ほどき終わったらリスト状につなぎなおします。
5
2 5 6 2 6
3 4 7 8 3 4 7 8
9 9
Copyright © 2012 yuga 21
- 22. 実装: tail rev.2
tailの完了です。
2
3 4 5
6
7 8
9
Copyright © 2012 yuga 22
- 23. 実装: 同じデータを何度もtailしたとき
しかし、今の実装では、tailするたびに最悪O(n)回linkを実行
することになり 、CatenableListをpersistentなデータ構造と
して使うことができません。
2
1 tail
O(n) 3
2 3 4 … 99
…
99
2 2
3 3
… …
ならし解析が意味をなさない tail tail
(参考: 5.6節 The Bad News) O(n) O(n)
99 99
Copyright © 2012 yuga 23
- 24. 実装: tail rev.2
そこで、linkAllの再帰実行を遅延データに包んで、先頭要素から
queueをほどく処理を、順次必要になるまで遅延させます。
これによりtailがincremental関数になります。
exception Empty
let link (C (x, q), ys) -> C (x, Q.snoc (q, ys))
let linkAll q =
let lazy t = Q.head q in
let q’ = Q.tail q in
if Q. isEmpty q’ then t else link (t, lazy (linkAll q’))
let tail = function
| E -> raise Empty
| C (_, q) -> if Q.isEmpty q then q else linkAll q
型: ‘a t
(参考: 6章) 型: ‘a t Lazy.t
Copyright © 2012 yuga 24
- 25. 実装: ++ rev.3
その結果、queueに格納するデータ型が変化するので修正します。
module CatenableList (Q : QUEUE) : CATENABLELIST = struct
type ‘a t = E
| C of ‘a * ‘a t Lazy.t Q.t
let link (C (x, q), ys) -> C (x, Q.snoc (q, ys))
let (++) xs ys = match (xs, ys) with
| (E, ys) -> ys
| (xs, E) -> xs
| (xs, ys) -> link (xs, lazy ys)
…
end
Copyright © 2012 yuga 25
- 26. 実装: 完成
module CatenableList (Q : QUEUE) : CATENABLELIST = struct
type ‘a t = E
| C of ‘a * ‘a t Lazy.t Q.t
exception Empty
let empty = E
let isEmpty = function
| E -> true
| _ -> false
let link (C (x, q), ys) -> C (x, Q.snoc (q, ys))
let (++) xs ys = match (xs, ys) with
| (E, ys) -> ys
| (xs, E) -> xs
| (xs, ys) -> link (xs, lazy ys)
let cons (x, xs) = C (x, Q.empty) ++ xs
let snoc (xs, x) = xs ++ C (x, Q.empty)
let head = function
| E -> raise Empty
| C (x, _) -> x
let linkAll q =
let lazy t = Q.head q in
let q’ = Q.tail q in
if Q. isEmpty q’ then t else link (t, lazy (linkAll q’))
let tail = function
| E -> raise Empty
| C (_, q) -> if Q.isEmpty q then q else linkAll q
end
Copyright © 2012 yuga Ocamlで実際に動かしてみたやつ: https://github.com/yuga/readpfds/blob/master/OCaml/catenableList.ml 26
- 27. 解析
どんなものなのか
Copyright © 2012 yuga 27
- 28. 解析: ならし解析
実行コストの考え方:
++ / cons / snoc / head 関数の実行コストは、実装からあき
らかにO(1) worst-case timeです。
tail関数のO(n) worst-case timeな実行コストを、他の関数と
の間でならして、CatenableListのすべての操作がO(1)
amortized timeであることを証明します。
CatenableListのデータ構造に影響を与えるのは ++ / cons /
snoc / tail 関数ですが、cons / snoc 関数は ++ 関数に依存し
ています。ならし解析は++ 関数と tail 関数の2つに注目して
行います。
Copyright © 2012 yuga 28
- 29. 解析: Banker’s method
ならし解析にあたり、Banker’s methodを採用します。
tail 関数の実行コストはサスペンションとして負債(debits)にし
linkAll 関数が link 関数を呼ぶときの1番目の引数のノードに割り当
てます。
++ 関数の実行コストもサスペンションとして負債(debits)にし、
++ 関数が link 関数を呼ぶときの1番目の引数のノードに割り当て
ます。
各Nodeが tail 関数によって取り除かれるとき、そのノードに
割り当てられたdebitsがすべて支払い済み(残サスペンション
数=0)であるようにします。
Copyright © 2012 yuga 29
- 30. 解析: 定義(ツリー)
CatenableListのデータ構造を、ノードによって構成され
たツリーが、階層状になっているものと考えます。
0
1 2 7 8
𝑡0 𝑡2 𝑡3
3 4 5 6
𝑡
𝑡1
Copyright © 2012 yuga 30
- 31. 解析: 定義(ノード識別子, degree, depth)
このツリーにラベルと関数を定義します。
0
root (0 th) node of t
𝑑𝑒𝑔𝑟𝑒𝑒 𝑡 0 = 4
4th node of t
1 2 3 4 𝑑𝑒𝑔𝑟𝑒𝑒 𝑡 4 = 4
1st 2nd 3 rd 𝑑𝑒𝑝𝑡ℎ 𝑡 4 = 1
node of t 0 th node of 𝑡1
𝑑𝑒𝑔𝑟𝑒𝑒 𝑡 𝑗 0 = 4
5 6 7 8
1st 𝑑𝑒𝑝𝑡ℎ 𝑡 8 = 2
node of 𝑡 𝑗
9 10 11 12
𝑡 𝑡𝑗
Copyright © 2012 yuga 31
- 32. 解析: 各ノードに割り当てるdebitsの考え方
各ノードに割り当てるdebitsは以下のようになります。
ならし解析の目的はlinkAllのコストを配分すること
queueの中の子ノード数 (linkAllのコスト)
= queueに含まれるサスペンション数
= そのノードに割り当てるdebits数
デビット数を表す関数を定義します。
𝑑 𝑡 𝑖 = ツリー 𝑡 の 𝑖 𝑡ℎ ノード上の𝑑𝑒𝑏𝑖𝑡𝑠数
𝑖
𝐷𝑡 𝑖 = 𝑗=0 𝑑 𝑡 (𝑗) = ツリー 𝑡 のルートノードから 𝑖 𝑡ℎ ノードまでの合計𝑑𝑒𝑏𝑖𝑡𝑠数
Copyright © 2012 yuga 32
- 33. 解析: Debit Invariant #1
ある1つのノード上に割り当てられるdebits数の上限を、
以下の不変式で表します。
あるノードのqueueに含まれるサスペンション数の上限は、
そのノードの出次数(out degree)
⇒ 𝑑 𝑡 𝑖 ≤ 𝑑𝑒𝑔𝑟𝑒𝑒 𝑡 (𝑖) … (1)
0
ツリーの全ノードの出次数の合計は
ノード数よりも1小さいので、
0th node <= 4 1 2 7
1st node <= 0
⇒ 𝐷 𝑡 ≤ |𝑡| 2nd
7th
node
node
<=
<=
3
2
8th node <= 2
12th node <= 0 8 12
Copyright © 2012 yuga 33
- 34. 解析: Debit Invariant #2
あるノードが、全体のツリー t のルートノードとなり
tail 関数によって取り除かれるまでに返済しなければなら
ないdebits数の上限を、以下の不変式であらわします。
そのノードへのルートノードからのpath数 (= depth)
+ そのノードより先に tail 関数で取り除かれるノード数
⇒ 𝐷 𝑡 𝑖 ≤ 𝑖 + 𝑑𝑒𝑝𝑡ℎ 𝑡 𝑖 0
… (2) Left linear debit invariant
0 + 0 = 0 if i = 0 1 2 7
1 + 1 = 2 if i = 1
ルートノードは返済が
2 + 1 = 3 if i = 2
済んだ状態になる
…
7 + 1 = 8 if i = 7
8 + 2 = 10 if i = 8 8 12
12 + 2 = 14 if i = 12
Copyright © 2012 yuga 34
- 35. 解析: 定理10.1
定理10.1
++ 関数と tail 関数は、それぞれ 1 debit、3 debits ず
つ返済することでDebit Invariantを維持する。
Copyright © 2012 yuga 35
- 36. 解析: ++ 関数は定理10.1を満たしているか
++ 関数が定理10.1を満たすことを証明します。
++ 関数は、2つのツリー𝑡1 と𝑡2 を連結することで新たなツリー𝑡を作るものとします。ツリー𝑡の
ノード数を|𝑡| 、 𝑡1 を|𝑡1 | とします。当然ながら𝑡1 とt2はそれぞれ不変式(1)と(2)を満たしています。
++ 関数の実行の結果、𝑡1 のルートノードの子としてt2のルートノードが加わり新しいツリーtがう
まれます。
新規に発生するdebitとしては、t2のルートノードを格納するサスペンションが作られた結果、𝑡の
ルートノード(元𝑡1 のルートノード)に割り当てられるdebitが1増加します。
debit数の上限に影響するデータ構造の変化としては、𝑡1 のルートノードの出次数が1増え、𝑡2 の各
ノードのインデックスが|𝑡1 |増加しdepthも1増加します。
Copyright © 2012 yuga 36
- 37. 解析: ++ 関数は定理10.1を満たしているか
まず新規debitについて考えます。不変式(1)によると、tの総出次数=𝑡1 の総出次数+𝑡2 の総出次数 +
1であるので、このdebitの返済は必要ありません。しかし不変式(2)よれば、ルートノードはdebitを
持てないため、すぐに1 debit返済する必要があります。
次にデータ構造の変化によるdebit数の上限の変化です。ルートノードの出次数増加は、不変式(1)に
よればdebitの許容数を増やすものなので、既存のdebitに対する影響はありません。不変式(2)につ
いては、𝑡1 に含まれていた任意のノードiは連結による影響を受けないため、i < |𝑡1 | に対し、
𝐷 𝑡 𝑖 = 𝐷 𝑡1 𝑖 ≤ 𝑖 + 𝑑𝑒𝑝𝑡ℎ 𝑡1 𝑖 = 𝑑𝑒𝑝𝑡ℎ 𝑡 (𝑖)です。ツリーt2に含まれていた任意のノード𝑖は𝑡の中でイ
ンデックスが|𝑡1 |増加し、またdepthが1増加するので、
𝐷 𝑡 𝑡1 + 𝑖 = 𝐷 𝑡1 + 𝐷 𝑡2 𝑖
≤ 𝑡1 + 𝐷 𝑡2 𝑖
≤ 𝑡1 + 𝑖 + 𝑑𝑒𝑝𝑡ℎ 𝑡2 𝑖
= 𝑡1 + 𝑖 + 𝑑𝑒𝑝𝑡ℎ 𝑡 𝑡1 + 𝑖 − 1
< 𝑡1 + 𝑖 + 𝑑𝑒𝑝𝑡ℎ 𝑡 𝑡1 + 𝑖
となりこちらも不変式を維持しています。
以上から「++関数は1 debitの返済でDebit Invariantを維持し定理10.1を満たす」ことを証明でき
ました。
Copyright © 2012 yuga 37
- 38. 解析: tail 関数は定理10.1を満たしているか
tail 関数が定理10.1を満たすことを証明します。
ツリーtに tail 関数を適用してツリーt’を作るものとします(let t’ = tail t)。tのルートノードはm
個の子ノードを持っています。tail 関数はtのルートノードを取り除いた後、その子ノードとして
queueに格納されていたツリーt0からtm-1を右から左へリスト状につなぎます。
𝑡′ 1
𝑡
0
2 3 4 5
𝑡0 𝑡𝑗 𝑡 𝑚−1
1 5 … x …
6 7 8
x
2 3 4 6 7 8 … … …
… … …
Copyright © 2012 yuga 38
- 39. 解析: tail 関数は定理10.1を満たしているか
新規に発生するdebit
ツリーt’jを、ツリーtjからtm-1までをリンクした部分結果とします。したがってツリーt’=t’0となり
ます。一番外側を除いたすべてのリンクはサスペンションを作ります。一番外側が除かれるのは、
tail 関数からの linkAll 関数の呼び出しは遅延されてないからです。link関数の実行だけに注目して大
雑把に式にすると、
let tail = link (tj, lazy (link (tj+1, lazy (link (tm-2, lazy (tm-1))))))
となっています。このようにサスペンションの作成によってもたらされるdebitsを、ツリーtj ただし
0 < j <= m-1 の各ルートノードに割り当てます。
𝑡′0 𝑡′ 𝑗
debit数の上限に影響するデータ構造の変化
1 5
ツリーt0からツリーtm-2はそれぞれ1ずつ出次数が増
加します。また、ツリーt1からtm-1は、それぞれ1か
らm-1だけdepthが増加します。
…
2 3 4 6 7 8
x
𝑡′ 𝑚−1
… … …
Copyright © 2012 yuga 39
- 40. 解析: tail 関数は定理10.1を満たしているか
まず新規debitについて考えます。ツリーt1からtm-2までは、それぞれリンクによって出次数が1増
加しているので、新規debitを1割り当てても不変式(1)を維持していますが、tm-1についてはリンク
を行わないので出次数が変化していません。したがって、tm-1に割り当てられる予定だった1 debit
はすぐに返済する必要があります。
次にデータ構造の変化によるdebit数の上限の変化です。ツリーt0からtm-2までの出次数増加は、不
変式(1)によればdebitの許容数を増やすものなので、既存のdebitに対する影響はありません。不変
式(2)については、tjの中に含まれるtのi番目のノードをとりあげます。不変式(2)から
Dt(i)<=i+deptht(i)であることがわかっています。これがtailによって、どのように各項の値がどの
ように変化するかを見ます。tのルートノードが取り除かれるので、iは1減少します。tjの各ノードの
depthはj-1増加します。一方でtjの各ノードのDt(i)は新規debitにより累積debitがj増加します。し
たがって、
Dt’(i-1)=Dt(i)+j<=i+deptht(i)+j=i+(deptht’(i-1)-(j-1))+j=(i-1)+deptht’(i-1)+2
となり、2 debitsを返済すれば不変式(2)を維持できます。よって tail 関数が返済すべきdebitは合計
3となります。
以上から「tail 関数は3 debitの返済でDebit Invariantを維持し定理10.1を満たす」ことを証明でき
ました。
Copyright © 2012 yuga 40
- 41. 参考文献
• Chris Okasaki, “10.2.1 Lists With Efficient Catenation”, Purely
Functional Data Structures, Cambridge University Press (1999)
• Chris Okasaki, “Amortization, lazy evaluation, and persistence: Lists
with catenation via lazy linking”, In IEEE Symposium on Foundations
of Computer Science, pages 646-654, October 1995
• Haim Kaplan and Robert E. Tarjan, “Persistent lists with catenation via
recursive slow-down”, In ACM Symposium on Theory of Computing,
pages 93-102, May 1995.
Copyright © 2012 yuga 41