pfds 10.2.1 lists with efficient catenation
TRANSCRIPT
Copyright © 2012 yuga 1
10.2.1
Lists With Efficient Catenation
PFDS #11
@yuga
2012-11-03
Copyright © 2012 yuga 2
目次
Structural Abstractionを用いた1つめの実装例として、Catenable List を取り上げます。
定義: 何を作る
実装: どう作る
解析: どんなものなのか
Copyright © 2012 yuga 3
定義
何を作る
Copyright © 2012 yuga 4
実現する操作
以下の定義を実装したリストを作ります。
定義(CATENABLELISTシグネチャ):
= Output Restricted Queues + (++)
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
Copyright © 2012 yuga 5
実行時間はO(1)
CatenableListはすべての操作をO(1)時間で実現します。
通常のリスト同士の連結操作(++)はO(n)時間かかるのに対し、Catenable ListsではO(1)時間で可能になるのが特徴です。
Persistentな使い方をしても問題ないものにします。
Copyright © 2012 yuga 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 7
実装
どう作る
Copyright © 2012 yuga 8
実装のアイデア
効率の良い連結関数を作るため、リスト内部にqueueを設けて、その中に相手リストを格納します。
1
2 4
3
6
7 8
9
1
2 4
3
6
7 8
9
1
2 4
3
6
7 8
9
++ 連結
註: 空のqueueを省略しています
○はリストの要素
青枠はqueue
Copyright © 2012 yuga 9
実装のアイデア
Queueを新たに実装するのは本節の対象外です。以下の要件を満たしていれば何でも良いので、既存のものを利用します。
QUEUEシグネチャを満たしている
すべての操作が Worst-Case / Amortized 関係なくO(1)時間で実行可能
Persistentな使い方をしても問題ない
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
Copyright © 2012 yuga 10
PFDSに出てきたqueue
これまでに登場したqueueには以下のものがありました。
⇒ BatchedQueue以外なら良さそうです。
Section Name Cost Persistent
5.2 BatchedQueue O(n) worst-case time NG
6.3.2, 8.3
BankersQueue O(1) amortized time OK
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
Copyright © 2012 yuga 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 | (B (x, c), b) -> B (x, insert_c (b, c))
_c は ‘a c _b は ‘a b の関数
Copyright © 2012 yuga 12
実装: empty / isEmpty
最初に、データ構造として空リストだけ定義します。
ここではCatenableListの型を t とします。
emptyとisEmptyの実装はデータコンストラクタを見るだけです。
module CatenableList : CATENABLELIST = struct type ‘a t = E | … … end
let empty = E let isEmpty = function | E -> true | _ -> false
Copyright © 2012 yuga 13
実装: ++ rev.1
QUEUEシグネチャを実装したモジュールをQという名前で受け取ります。ここではqueueの型を t として、リストのデータ構造を定義します。
(++)は、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))
module CatenableList (Q : QUEUE) : CATENABLELIST = struct type ‘a t = E | C of ‘a * ‘a t Q.t … end
Copyright © 2012 yuga 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 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 16
図解: cons / snoc
consとsnocをそれぞれ3回繰り返した結果です。
E
1
2 3
cons
1
1
2
cons 2
3
1
cons 3
2
1
snoc 1
snoc
E
1
2
snoc
Copyright © 2012 yuga 17
実装: head
headは先頭を取り出すだけです。
exception Empty let head = function | E -> raise Empty | C (x, _) -> x
Copyright © 2012 yuga 18
実装: tail rev.1
tailは先頭をすてて、queueの中身をリスト状につなぎなおします。
exception Empty let link (C (x, q), ys) -> C (x, Q.snoc (q, ys)) let linkAll q = 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
linkは再掲
Copyright © 2012 yuga 19
図解: tail
tailによりqueueの中身がつなぎなおされる過程です。
最初にheadが取り除かれます。
5 6
7 8
9
1
5 6
7 8
9
2
3 4
2
3 4
Copyright © 2012 yuga 20
図解: tail
続いてqueueをほどいていきます。
5 6
7 8
9
5 6
7 8
9
2
3 4
2
3 4
Copyright © 2012 yuga 21
図解: tail
ほどき終わったらリスト状につなぎなおします。
5
6
7 8
9
5 6
7 8
9
2
3 4
2
3 4
Copyright © 2012 yuga 22
実装: tail rev.2
tailの完了です。
2
5 4
6
7 8
9
3
Copyright © 2012 yuga 23
実装: 同じデータを何度もtailしたとき
しかし、今の実装では、tailするたびに最悪O(n)回linkを実行することになり 、CatenableListをpersistentなデータ構造として使うことができません。
1
2 3 4 99 …
2
3
99
…
2
3
99
…
2
3
99
…
tail
tail tail
O(n)
O(n) O(n)
ならし解析が意味をなさない (参考: 5.6節 The Bad News)
Copyright © 2012 yuga 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
型: ‘a t Lazy.t (参考: 6章)
Copyright © 2012 yuga 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 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
Ocamlで実際に動かしてみたやつ: https://github.com/yuga/readpfds/blob/master/OCaml/catenableList.ml
Copyright © 2012 yuga 27
解析
どんなものなのか
Copyright © 2012 yuga 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 29
解析: Banker’s method
ならし解析にあたり、Banker’s methodを採用します。
tail 関数の実行コストはサスペンションとして負債(debits)にし
linkAll 関数が link 関数を呼ぶときの1番目の引数のノードに割り当てます。
++ 関数の実行コストもサスペンションとして負債(debits)にし、++ 関数が link 関数を呼ぶときの1番目の引数のノードに割り当てます。
各Nodeが tail 関数によって取り除かれるとき、そのノードに割り当てられたdebitsがすべて支払い済み(残サスペンション数=0)であるようにします。
Copyright © 2012 yuga 30
解析: 定義(ツリー)
CatenableListのデータ構造を、ノードによって構成されたツリーが、階層状になっているものと考えます。
0
1 2
3 4 5 6
𝑡
𝑡1
7 8
𝑡0 𝑡2 𝑡3
Copyright © 2012 yuga 31
解析: 定義(ノード識別子, degree, depth)
このツリーにラベルと関数を定義します。
0
1 3 2
5 6 7
4
9 10 11 12
8
𝑡 𝑡𝑗
1st node of t
2nd
4th node of t 𝑑𝑒𝑔𝑟𝑒𝑒𝑡 4 = 4 𝑑𝑒𝑝𝑡ℎ𝑡 4 = 1
0th node of 𝑡1
𝑑𝑒𝑔𝑟𝑒𝑒𝑡𝑗 0 = 4
𝑑𝑒𝑔𝑟𝑒𝑒𝑡 0 = 4
3rd
root (0th) node of t
1st node of 𝑡𝑗
𝑑𝑒𝑝𝑡ℎ𝑡 8 = 2
Copyright © 2012 yuga 32
解析: 各ノードに割り当てるdebitsの考え方
各ノードに割り当てるdebitsは以下のようになります。
ならし解析の目的はlinkAllのコストを配分すること
queueの中の子ノード数 (linkAllのコスト) = queueに含まれるサスペンション数 = そのノードに割り当てるdebits数
デビット数を表す関数を定義します。
𝑑𝑡 𝑖 = ツリー 𝑡 の 𝑖𝑡ℎ ノード上の𝑑𝑒𝑏𝑖𝑡𝑠数
𝐷𝑡 𝑖 = 𝑑𝑡(𝑗)𝑖𝑗=0 = ツリー 𝑡 のルートノードから 𝑖𝑡ℎノードまでの合計𝑑𝑒𝑏𝑖𝑡𝑠数
Copyright © 2012 yuga 33
解析: Debit Invariant #1
ある1つのノード上に割り当てられるdebits数の上限を、以下の不変式で表します。
あるノードのqueueに含まれるサスペンション数の上限は、 そのノードの出次数(out degree)
⇒ 𝑑𝑡 𝑖 ≤ 𝑑𝑒𝑔𝑟𝑒𝑒𝑡(𝑖) … (1)
ツリーの全ノードの出次数の合計は ノード数よりも1小さいので、 ⇒ 𝐷𝑡 ≤ |𝑡|
0th node <= 4 1st node <= 0 2nd node <= 3 7th node <= 2 8th node <= 2 12th node <= 0
0
1 7
12
2
8
Copyright © 2012 yuga 34
解析: Debit Invariant #2
あるノードが、全体のツリー t のルートノードとなり tail 関数によって取り除かれるまでに返済しなければならないdebits数の上限を、以下の不変式であらわします。
そのノードへのルートノードからのpath数 (= depth) + そのノードより先に tail 関数で取り除かれるノード数
0
1 7
12
2 0 + 0 = 0 if i = 0 1 + 1 = 2 if i = 1 2 + 1 = 3 if i = 2 … 7 + 1 = 8 if i = 7 8 + 2 = 10 if i = 8 12 + 2 = 14 if i = 12
8
⇒ 𝐷𝑡 𝑖 ≤ 𝑖 + 𝑑𝑒𝑝𝑡ℎ𝑡 𝑖
… (2) Left linear debit invariant
ルートノードは返済が 済んだ状態になる
Copyright © 2012 yuga 35
解析: 定理10.1
定理10.1
++ 関数と tail 関数は、それぞれ 1 debit、3 debits ずつ返済することでDebit Invariantを維持する。
Copyright © 2012 yuga 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 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 38
解析: tail 関数は定理10.1を満たしているか
tail 関数が定理10.1を満たすことを証明します。
ツリーtに tail 関数を適用してツリーt’を作るものとします(let t’ = tail t)。tのルートノードはm個の子ノードを持っています。tail 関数はtのルートノードを取り除いた後、その子ノードとしてqueueに格納されていたツリーt0からtm-1を右から左へリスト状につなぎます。
6 7 8
5 2 3 4
1
… … …
x
6 7 8
5
2 3 4
1
… … …
x
0
… …
𝑡
𝑡′
𝑡𝑗 𝑡𝑚−1 𝑡0
Copyright © 2012 yuga 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 の各ルートノードに割り当てます。
𝑡′𝑗
𝑡′𝑚−1 6 7 8
5
2 3 4
1
… … …
x
…
𝑡′0 debit数の上限に影響するデータ構造の変化
ツリーt0からツリーtm-2はそれぞれ1ずつ出次数が増加します。また、ツリーt1からtm-1は、それぞれ1からm-1だけdepthが増加します。
Copyright © 2012 yuga 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 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.