re永続データ構造が分からない人のためのスライド
DESCRIPTION
Competitive Programming Advent Calendar 2012の12/01担当分の記事です。TRANSCRIPT
Re: 永続データ構造が分からない人のためのスライド
@qnighy Competitive Programing Advent Calendar 2012
このスライドについて
• Competitive Programming Advent Calendar 2012の12/1の担当記事です。
• Advent Calendarのサイトはこちら
経緯
経緯
経緯
経緯
経緯
経緯
• 被害者が何名か出ました
• 今日は少しだけ真面目に書きます
問題
• 三問やります
– Crayfish Scrivener (IOI 2012, day 1)
– L-th Number (UTPC 2011)
– Copy and Paste (JOI Spring 2012, day 4)
• 三問における使い方の違いみたいなのに焦点を当てていこうかと思っています
• ネタバレあり • 僕より詳しい人のツッコミが入る可能性に怯えています
永続データ構造
• 変更前のデータを扱えるデータ構造の総称
D1 D2 変更操作
普通のデータ構造
永続データ構造
• 変更前のデータを扱えるデータ構造の総称
D1
D2
変更操作
永続データ構造
D1
完全永続性
• 部分永続: 最新版のみを変更可能
D1 D2
D3 D4
完全永続性
• 完全永続: 最新版のみを変更可能
• ここでは完全永続データ構造のみ扱う
D1
D2
D4 D5
D3 D6
D7
D8
D9
2つの視点
• 履歴としての永続データ構造
• 値型としての永続データ構造
履歴としての永続データ構造
• 変更履歴を遡れる、という視点で見る
–昔使ったデータを再利用したい、というイメージ
値型としての永続データ構造
• 永続データ構造は元の値を保存する
–破壊的変更を隠せる
• 値型として見なせる
– 2つの値を演算して新しい値を作る場合も…
問題
• Crayfish Scrivener (IOI 2012, day 1)
• L-th Number (UTPC 2011)
• Copy and Paste (JOI Spring 2012, day 4)
問題
• Crayfish Scrivener (IOI 2012, day 1)
Crayfish Scrivener
• 問題文の入手先
• 概要
–エディタの動作をシミュレーションします
–入力操作: 末尾に1文字足します。
– undo操作: 直前のK個をもとに戻します
–入力・undoはそれぞれ1回の操作とみなします
• つまり、undoのundoができます
–文字取得: 現在のi番目の文字を教えて下さい
Crayfish Scrivener
• undoのundo???
Crayfish Scrivener
• 例: 1. Aを入力
0: (空)
1: A
Crayfish Scrivener
• 例: 1. Aを入力
2. Bを入力
0: (空)
1: A
2: AB
Crayfish Scrivener
• 例: 1. Aを入力
2. Bを入力
3. 1回分もとに戻す
0: (空)
1: A
2: AB
3: A
Crayfish Scrivener
• 例: 1. Aを入力
2. Bを入力
3. 1回分もとに戻す
4. Cを入力
0: (空)
1: A
2: AB
3: A
4: AC
Crayfish Scrivener
• 例: 1. Aを入力
2. Bを入力
3. 1回分もとに戻す
4. Cを入力
5. 2回分もとに戻す
0: (空)
1: A
2: AB
3: A
4: AC 5: AB
Crayfish Scrivener
• 例: 1. Aを入力
2. Bを入力
3. 1回分もとに戻す
4. Cを入力
5. 2回分もとに戻す
6. Dを入力
0: (空)
1: A
2: AB
3: A
4: AC 5: AB
6: ABD
Crayfish Scrivener
• 例: 1. Aを入力
2. Bを入力
3. 1回分もとに戻す
4. Cを入力
5. 2回分もとに戻す
6. Dを入力
• これを”作業履歴 ツリー”と呼ぶ ことにする
0: (空)
1: A
2: AB
3: A
4: AC 5: AB
6: ABD
Crayfish Scrivener
• undoのundo???
• →冷静に考えれば、単純にK個前の時の文字列をコピーしてくればよいことがわかります。
Crayfish Scrivener
• 入力操作(文字c)
1. S[t+1] ← S[t] + c
2. t++
• undo操作(K回)
1. S[t+1] ← S[t-K]
2. t++
• 読み出し操作
1. return S[t][i]
Crayfish Scrivener
• 素朴に実装すると二乗
• 文字列のコピーコストを減らしたい
Crayfish Scrivener
• 素朴に実装すると二乗
• 文字列のコピーコストを減らしたい
• 配列を永続化すれば良さそう
Crayfish Scrivener
• 素朴に実装すると二乗
• 文字列のコピーコストを減らしたい
• △ 配列を永続化すれば良さそう
Crayfish Scrivener
• 素朴に実装すると二乗
• 文字列のコピーコストを減らしたい
• △ 配列を永続化すれば良さそう
• ○ リストを永続化しよう!!
リスト = 永続stack
• リストへの追加: new Node(a, item)
• リストからの削除: a->next
• 先頭アイテムの取得: a->item
Crayfish Scrivener
• 例: 1. Aを入力
空
A
[0]
[1]
Crayfish Scrivener
• 例: 1. Aを入力
2. Bを入力
空
A
[0]
[1]
B [2]
Crayfish Scrivener
• 例: 1. Aを入力
2. Bを入力
3. 1回分もとに戻す
空
A
[0]
[1] [3]
B [2]
Crayfish Scrivener
• 例: 1. Aを入力
2. Bを入力
3. 1回分もとに戻す
4. Cを入力
空
A
[0]
[1] [3]
B [2] C [4]
Crayfish Scrivener
• 例: 1. Aを入力
2. Bを入力
3. 1回分もとに戻す
4. Cを入力
5. 2回分もとに戻す
空
A
[0]
[1] [3]
B [2] [5] C [4]
Crayfish Scrivener
• 例: 1. Aを入力
2. Bを入力
3. 1回分もとに戻す
4. Cを入力
5. 2回分もとに戻す
6. Dを入力
空
A
[0]
[1] [3]
B [2] [5] C [4]
D [6]
Crayfish Scrivener
• 永続リストを使って簡潔に表現できた!
• でもi番目の文字はどうやって見つけるのか?
• 次のページに答えを書きます。
Crayfish Scrivener
• 永続リストを使って簡潔に表現できた!
• でもi番目の文字はどうやって見つけるのか?
• 答え:
–普通のリストでは1文字前のリンクのみ保持している。
– 2^x文字前のリンクを全て持てば良い。
Crayfish Scrivener
• 例に戻ってみる 空
A
[0]
[1] [3]
B [2] [5] C [4]
D [6]
Crayfish Scrivener
• 例に戻ってみる
• 各時点でのデータは リストだった
空
A
B
D
Crayfish Scrivener
• 例に戻ってみる
• 各時点でのデータは リストだった
空
A
C
Crayfish Scrivener
• 例に戻ってみる
• 各時点でのデータは リストだった
• 全体を俯瞰すると 逆向きの木になっている ことがわかる
空
A
[0]
[1] [3]
B [2] [5] C [4]
D [6]
Crayfish Scrivener
• 例に戻ってみる
• 各時点でのデータは リストだった
• 全体を俯瞰すると 逆向きの木になっている ことがわかる
• これを「通時的構造」とでも 呼ぶことにしよう
空
A
[0]
[1] [3]
B [2] [5] C [4]
D [6]
Crayfish Scrivener (一般化)
• 一般にi文字目を書き換えるよう一般化した問題を考える
• →永続配列を実装する必要がある
• Wikipediaの「永続データ構造」の記事には永続配列としてVListが紹介さ
れているが、これは書き込みがランダムアクセスではないために配列とは呼べない。また、完全永続的でないので今回の問題には適さない。
永続配列
• 配列: map<int,T>として見なせる
• 永続な平衡二分木を作れば良さそう
永続配列
• 配列: map<int,T>として見なせる
• 永続な平衡二分木を作れば良さそう
• ただし、一般的な永続平衡二分木を実装する必要はない
–キーであるintのもつ性質を活用する
– これについては次のセクションで解説
問題
• Crayfish Scrivener (IOI 2012, day 1)
• L-th Number (UTPC 2011)
• Copy and Paste (JOI Spring 2012, day 4)
問題
• L-th Number (UTPC 2011)
L-th Number
• 問題文の入手先
• 概要
–無向木があります。頂点に数字が書かれています。
– クエリ(v,w,L)が順番に与えられます。
–頂点vと頂点wを結ぶ経路上の数字のうち、L番目に小さいものを答えて下さい。
L-th Number
• 解説PDFがあるので、ちゃんと解説を知りたい人はそっちを読んでください。
• 一応、次のページで軽く解説します。
L-th Number
• 無向木に根を与えて有向木にします。
• 頂点vから根までの経路上での数字の分布を木構造で管理します。(総和を計算するRMQを作ります。)
• 頂点vとwの間での分布は、その差分で得られます。
• 二分探索で答えが出ます。
L-th Number
• 無向木に根を与えて有向木にします。
• 頂点vから根までの経路上での数字の分布を木構造で管理します。(総和を計算するRMQを作ります。)
• 頂点vとwの間での分布は、その差分で得られます。
• 二分探索で答えが出ます。
L-th Number
• 「頂点vから根における分布」→「その子供wから根における分布」の計算
–分布を表すデータ構造に変更を加える
–でも元のデータも残したい、という問題
– →「履歴としての永続データ構造」と見なせる
L-th Number
• 作業履歴ツリーは例えば以下のようになる
• (公式解説スライドの例と同じもの)
2 空
2,5
2,4,5 2,5,8
2,5,8,9 2,5,7,8
L-th Number
• 永続リストの通時的構造は木だった
• 永続木の通時的構造は?
L-th Number
• 先ほどの例を使ってやってみよう
• (平衡のさせかたは適当)
2 空
2,5
2,4,5 2,5,8
2,5,8,9 2,5,7,8
L-th Number
• 頂点1
2 空
2,5
2,4,5 2,5,8
2,5,8,9 2,5,7,8
2
L-th Number
• 頂点3
2 空
2,5
2,4,5 2,5,8
2,5,8,9 2,5,7,8
2
5
L-th Number
• 頂点2
2 空
2,5
2,4,5 2,5,8
2,5,8,9 2,5,7,8
2
4
5
L-th Number
• 頂点4
2 空
2,5
2,4,5 2,5,8
2,5,8,9 2,5,7,8
2
5
8
L-th Number
• 頂点5
2 空
2,5
2,4,5 2,5,8
2,5,8,9 2,5,7,8
2
5
8
9
L-th Number
• 頂点6
2 空
2,5
2,4,5 2,5,8
2,5,8,9 2,5,7,8
2
5
7
8
L-th Number
• 通時的構造
2 空
2,5
2,4,5 2,5,8
2,5,8,9 2,5,7,8
2
4
5
5
7 8
9
8
8
L-th Number
• DAGになっていることが おわかり頂けただろうか
• (この例だと特殊なのでわかりにくいが…)
実装上のテクニック
• 実装上のテクニック
– Crayfish Scrivenerと併せて
• キーがintであることなどに注目すると、赤黒とかは必要ないことがわかる。
–木の形状は固定でよい
–データは葉にのみ持たせたほうが楽ちん
• ただし、L-th NumberはそもそもSegment Treeなので、生データを葉にのみ持たせるのは割と当然
実装上のテクニック
• L-th Numberの場合
• 「存在する数字の集合」ではなく、「ある範囲内に収まる数字はいくつあるか」で持っておく
• 木の形状を固定にすると良い点
–実装が簡単
–オーダーが落ちる(二分探索との併用なので、計算しつつ探索を下ることができれば速くなる)
実装上のテクニック
• 参考問題
– Apples (JOI Spring 2011 day4)
• 実は木の形状を固定できる例
• 遅延評価でメモリを節約する工夫が必要
– Bug Party (JOI Final 2011)
• 二分探索とツリー処理の合併で高速化できる例
ここまでの地図
• 履歴としての永続データ構造
–作業履歴が定義できて、それが木構造をなす
– リストの例: Crayfish Scrivener
• 通時的構造が木になることがわかった
–木構造の例: L-th Number
• 通時的構造がある種のDAGになることがわかった
• 値型としての永続データ構造
–??
問題
• Crayfish Scrivener (IOI 2012, day 1)
• L-th Number (UTPC 2011)
• Copy and Paste (JOI Spring 2012, day 4)
問題
• Copy and Paste (JOI Spring 2012, day 4)
Copy and Paste
• 問題文の入手先
• 概要
–エディタの動作をシミュレーションします
–初期入力: 初期文字列Sが与えられます。
– コピペ: ある範囲をある位置にコピー・挿入します
–溢れ: M文字より溢れたらトリミングします
–最終結果を出力してください
Copy and Paste
• これも、よくわかる解説スライドがあるので、もういいんじゃないかと思いますが………
• 一応、今回のテーマに関係する部分を適当に解説します
Copy and Paste
• 見た感じでヤバい問題 – これを見た感じでヤバいって思えないと大きな失態を演じることになるから気をつけた方がいい
• なんかこう、副作用を気にしないで好きなように切り貼りできる便利なのが欲しいよね
Copy and Paste
• 副作用を気にしないで?
–永続データ構造?
• 切ったり貼ったり出来る?
–平衡二分探索木?
• !!永続な平衡二分探索木を組もう!!
永続やるときの制約
• ならし解析はだいたい無理
–完全永続的なデータ構造では、同じ状態遷移を何回も繰り返せるので、ならし解析が壊れる
• 乱択アルゴリズムも難しいらしい
– こっちはよくわからないけどとにかく難しいらしい
Copy and Paste
• !!赤黒木を書こう!!
Copy and Paste
• 実装上のテクニックは公式解説にあるので省略 – 実は自分で実装してみていないから迂闊なこと言えないんだ、なんて言えない…
Copy and Paste
• この問題では、次のような操作があることに注意
–文字列の連結(c := a + b)
• 今までの例では、1つのデータを少しだけ加工して新しいデータを作っていた
• ここでは、2つのデータを併せて何かをする操作が出てくる
• =値型としての永続データ構造
Copy and Paste
• 「値型としての永続データ構造」の性質
–通時的構造はDAGになるが、先程のものとは違う性質を持っている
–それは、「頂点vからwへの遷移方法が2通り以上ある可能性がある」ということ
–そういう性質などもあって、こういう永続データ構造は扱いがめんどいようです。
Copy and Paste
• 本当はこれの実装上のテクニックとかも書く予定でしたが、もう眠いので止めます。
• ゴミ回収の必要とかがあるんですが、C++使いの人はshared_ptrがあるから
–はやく競プロでBoostが使えるようになるといいですね………………
突飛なまとめ
• 永続データ構造と一口にいってもその条件は何段階かある – 特に、insert/delete系の操作しか行わない場合と、mergeが必要な場面では実装の要求レベルがだいぶ変わってくる
• 永続データ構造を、通時的観点から俯瞰すると新たな視点が得られる(かも)
• つまり、「代数的データ型でデータ構造を構築する」という観点から永続データ構造を理解することも重要だが、結局は競技プログラミングにおける手法の一つとして使うのだから、手続き型的な観点から解体して、必要なテクニックだけ取り出せるようにすると良いよね
参考
• 著名データ構造の永続的な実装の例
• 配列 : 二分木による実装
• スタック : リストと等価
• ヒープ : Leftist Heapの永続的実装 – Binomial Heapの永続的実装もある
• キュー : Finger Tree, Implicit Queue
• マップ : 赤黒木系統、AVL系統
• DSU(Union Find) : Sylvain Conchon and Jean-Christophe Filliâtreによる論文
参考文献
• Chris Okasaki, 1999, Purely Functional Data Structures, Cambridge University Press, 232 Pages.
• Sylvain Conchon and Jean-Christophe Filliâtre, 2007, A persistent union-find data structure, ML ‘07 Proceedings of the 2007 workshop on ML, Pages 37-46, ACM.
• VList – Wikipedia, the free encyclopedia (2012/12/02閲覧) • 素集合データ構造 – Wikipedia (2012/12/02閲覧) • 永続データ構造 – Wikipedia (2012/12/02閲覧) • 2-3 フィンガーツリー – Wikipedia (2012/12/02閲覧) • Tasks | IOI 2012 (2012/12/02閲覧) • 東京大学プログラミングコンテスト2011 (2012/12/02閲覧) • 問題一覧 – 情報オリンピック 問題と解説 (2012/12/02閲覧)
fin
• ご清聴ありがとうございました