2019/02/06 永続 Stack のやや丁寧な実装例を追加しました
2020/12/13 数式などを整えました
概要
あなたのライブラリに新しい彩りを与える永続データ構造の紹介です。
永続データ構造とは
永続データ構造とは、普段のデータ構造で更新をするときに更新前の状態のデータも保存しておけるデータ構造です。
唐突ですが、数列 に対するクエリを処理する問題を考えてみてください。
: に を加える
: " 番目のクエリ直後の" の和を出力
のクエリを一緒にソートすればいつもの Segment Tree で解くことが出来ます。
ではこれがオンラインクエリだったらどうでしょうか。
これは Segment Tree では容易には解くことが出来ません。
こういった「過去の状態が必要になる」状況で永続データ構造は効率的です。
永続 Stack
具体的に、基本的なデータ構造である Stack の永続化をしてみましょう。整数を要素とする Stack は次の 3 種類のクエリを処理します。
: 先頭要素にアクセス
: を先頭に追加
: 先頭要素を削除
これを永続化すると以下のようになります。
: 先頭要素にアクセス
: を先頭に追加した結果得られる Stack を作成
: 先頭要素を削除した結果得られる Stack を作成
※ の際、元のデータは書き換えられない
計算量を考慮しなければ、毎回 Stack の中身を全部コピーすればこれらのクエリは実現可能です。
しかしよく考えると、末尾のほとんどの要素は一切変化していません。これを利用して効率的に、具体的には計算量を のままで処理できるのです!
具体的にはどうすればよいのでしょうか。一般的に Stack は配列で実装すると思いますが、実は配列は永続化と相性が悪いので単方向連結リストを使います。
すると永続 Stack はリストの先頭ノードへのポインタそのものになります!各関数の処理を確認しましょう。
: ポインタから先頭ノードの値を読む
: 追加する値とポインタから新しくノードを作成し、それへのポインタを持つ Stack を作成
: ポインタから先頭ノードを読み、次のノードへのポインタを持つ Stack を作成
以上をコードにまとめると以下のようになります。(行儀や効率やらを投げ捨てています)
#include <iostream> struct Node { int value; Node *next; }; struct Stack { Node *head; int top() { return head->value; } Stack push(int x) { return Stack({ new Node({x, head}) }); } Stack pop() { return Stack({ head->next }); } }; int main() { Stack st0({nullptr}); Stack st1 = st0.push(1); Stack st2 = st1.push(2); Stack st3 = st2.pop(); std::cout << st1.top() << " " << st2.top() << " " << st3.top(); // 1 2 1 return 0; }
は や がされていますが、その後でも で先頭要素を取得可能なことがお分かりいただけるでしょうか。これこそが永続たる所以です。
実装例
上記の実装はメモリの解法処理を行っていないためメモリリークを起こすあまりよろしくない実装例です。
もう少しちゃんとした実装は以下を参考にしてください。
まとめ
永続データ構造は過去の状態の保存が可能で、様々なデータ構造に対してその永続版が提案されています。現在コンテストでお目にかかることは稀ですが、その理論は大変美しく技巧的です。データ構造やライブラリ整備が好きな人は是非調べてみてください。