pfds 10.2.1 lists with efficient catenation

41
Copyright © 2012 yuga 1 10.2.1 Lists With Efficient Catenation PFDS #11 @yuga 2012-11-03

Upload: shohei-murayama

Post on 12-Jun-2015

308 views

Category:

Technology


1 download

TRANSCRIPT

Page 1: PFDS 10.2.1 lists with efficient catenation

Copyright © 2012 yuga 1

10.2.1

Lists With Efficient Catenation

PFDS #11

@yuga

2012-11-03

Page 2: PFDS 10.2.1 lists with efficient catenation

Copyright © 2012 yuga 2

目次

Structural Abstractionを用いた1つめの実装例として、Catenable List を取り上げます。

定義: 何を作る

実装: どう作る

解析: どんなものなのか

Page 3: PFDS 10.2.1 lists with efficient catenation

Copyright © 2012 yuga 3

定義

何を作る

Page 4: PFDS 10.2.1 lists with efficient catenation

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

Page 5: PFDS 10.2.1 lists with efficient catenation

Copyright © 2012 yuga 5

実行時間はO(1)

CatenableListはすべての操作をO(1)時間で実現します。

通常のリスト同士の連結操作(++)はO(n)時間かかるのに対し、Catenable ListsではO(1)時間で可能になるのが特徴です。

Persistentな使い方をしても問題ないものにします。

Page 6: PFDS 10.2.1 lists with efficient catenation

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なリストより実装が簡単になっています。

Page 7: PFDS 10.2.1 lists with efficient catenation

Copyright © 2012 yuga 7

実装

どう作る

Page 8: PFDS 10.2.1 lists with efficient catenation

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

Page 9: PFDS 10.2.1 lists with efficient catenation

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

Page 10: PFDS 10.2.1 lists with efficient catenation

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

Page 11: PFDS 10.2.1 lists with efficient catenation

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 の関数

Page 12: PFDS 10.2.1 lists with efficient catenation

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

Page 13: PFDS 10.2.1 lists with efficient catenation

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

Page 14: PFDS 10.2.1 lists with efficient catenation

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

Page 15: PFDS 10.2.1 lists with efficient catenation

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)

Page 16: PFDS 10.2.1 lists with efficient catenation

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

Page 17: PFDS 10.2.1 lists with efficient catenation

Copyright © 2012 yuga 17

実装: head

headは先頭を取り出すだけです。

exception Empty let head = function | E -> raise Empty | C (x, _) -> x

Page 18: PFDS 10.2.1 lists with efficient catenation

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は再掲

Page 19: PFDS 10.2.1 lists with efficient catenation

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

Page 20: PFDS 10.2.1 lists with efficient catenation

Copyright © 2012 yuga 20

図解: tail

続いてqueueをほどいていきます。

5 6

7 8

9

5 6

7 8

9

2

3 4

2

3 4

Page 21: PFDS 10.2.1 lists with efficient catenation

Copyright © 2012 yuga 21

図解: tail

ほどき終わったらリスト状につなぎなおします。

5

6

7 8

9

5 6

7 8

9

2

3 4

2

3 4

Page 22: PFDS 10.2.1 lists with efficient catenation

Copyright © 2012 yuga 22

実装: tail rev.2

tailの完了です。

2

5 4

6

7 8

9

3

Page 23: PFDS 10.2.1 lists with efficient catenation

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)

Page 24: PFDS 10.2.1 lists with efficient catenation

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章)

Page 25: PFDS 10.2.1 lists with efficient catenation

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

Page 26: PFDS 10.2.1 lists with efficient catenation

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

Page 27: PFDS 10.2.1 lists with efficient catenation

Copyright © 2012 yuga 27

解析

どんなものなのか

Page 28: PFDS 10.2.1 lists with efficient catenation

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つに注目して行います。

Page 29: PFDS 10.2.1 lists with efficient catenation

Copyright © 2012 yuga 29

解析: Banker’s method

ならし解析にあたり、Banker’s methodを採用します。

tail 関数の実行コストはサスペンションとして負債(debits)にし

linkAll 関数が link 関数を呼ぶときの1番目の引数のノードに割り当てます。

++ 関数の実行コストもサスペンションとして負債(debits)にし、++ 関数が link 関数を呼ぶときの1番目の引数のノードに割り当てます。

各Nodeが tail 関数によって取り除かれるとき、そのノードに割り当てられたdebitsがすべて支払い済み(残サスペンション数=0)であるようにします。

Page 30: PFDS 10.2.1 lists with efficient catenation

Copyright © 2012 yuga 30

解析: 定義(ツリー)

CatenableListのデータ構造を、ノードによって構成されたツリーが、階層状になっているものと考えます。

0

1 2

3 4 5 6

𝑡

𝑡1

7 8

𝑡0 𝑡2 𝑡3

Page 31: PFDS 10.2.1 lists with efficient catenation

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

Page 32: PFDS 10.2.1 lists with efficient catenation

Copyright © 2012 yuga 32

解析: 各ノードに割り当てるdebitsの考え方

各ノードに割り当てるdebitsは以下のようになります。

ならし解析の目的はlinkAllのコストを配分すること

queueの中の子ノード数 (linkAllのコスト) = queueに含まれるサスペンション数 = そのノードに割り当てるdebits数

デビット数を表す関数を定義します。

𝑑𝑡 𝑖 = ツリー 𝑡 の 𝑖𝑡ℎ ノード上の𝑑𝑒𝑏𝑖𝑡𝑠数

𝐷𝑡 𝑖 = 𝑑𝑡(𝑗)𝑖𝑗=0 = ツリー 𝑡 のルートノードから 𝑖𝑡ℎノードまでの合計𝑑𝑒𝑏𝑖𝑡𝑠数

Page 33: PFDS 10.2.1 lists with efficient catenation

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

Page 34: PFDS 10.2.1 lists with efficient catenation

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

ルートノードは返済が 済んだ状態になる

Page 35: PFDS 10.2.1 lists with efficient catenation

Copyright © 2012 yuga 35

解析: 定理10.1

定理10.1

++ 関数と tail 関数は、それぞれ 1 debit、3 debits ずつ返済することでDebit Invariantを維持する。

Page 36: PFDS 10.2.1 lists with efficient catenation

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増加します。

Page 37: PFDS 10.2.1 lists with efficient catenation

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を満たす」ことを証明できました。

Page 38: PFDS 10.2.1 lists with efficient catenation

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

Page 39: PFDS 10.2.1 lists with efficient catenation

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が増加します。

Page 40: PFDS 10.2.1 lists with efficient catenation

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を満たす」ことを証明できました。

Page 41: PFDS 10.2.1 lists with efficient catenation

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.