Dynamic Segment Tree の ACL 風実装
インターフェースを ACL ( AtCoder Library ) に寄せた Dynamic Segment Tree を書きました。
Dynamic Segment Tree の概要
そこらに生えている多くのセグ木は、事前に適切なサイズの配列を確保した上で、二分ヒープ的 index で親や子に移動するような実装となっていますね。そういったセグ木で、例えば以下のようなクエリは捌けるでしょうか。
すベて で初期化された長さ の数列 があります。
以下で説明されるクエリを 回、オンラインで処理してください。クエリは 2 種類あります。クエリ 1: が与えられるので、 に を加算する。
クエリ 2: が与えられるので、 を報告する。ただし である。
注目すべきは の制約です。二分ヒープ的な index で管理する、いわゆる通常のセグ木で処理しようとすると、一般的なメモリ制限では MLE となります。また、オンライン処理なので、クエリ先読みした上での座標圧縮もできません。
ところで、セグ木の一回のクエリ処理でアクセスするノードの数は なので、実は上述のクエリを処理するためには のノードがあれば十分足りてしまいます。(本問の最大ケースでも 程度です。)
つまり、必要になったときに初めてそのノードを作るような実装にすれば、MLE することなく上述のクエリも処理できてしまいます。こういった実装の工夫がなされたセグ木のことを Dynamic Segment Tree と呼んでいます。(呼称については controversial です。私は「必要な部分だけ作るセグ木」という呼び方が好きですが、これはこれで簡潔ではありませんし、Codeforces のブログ等でも「Dynamic Segment Tree」という呼称が用いられているため、検索でのヒットのし易さ等も考慮して本記事では「Dynamic Segment Tree」で統一します。)
以下のリンク先の説明が簡潔にまとまっていておすすめです。
scrapbox.io
Dynamic Segment Tree の長所・短所
長所
- 実際に必要となるノード数が十分小さければ、大きなサイズの数列に対するクエリも、先読みや座標圧縮をすることなく処理することができる。
短所
- 通常のセグ木と比較してメモリアクセスが遅い。
- 一点取得の時間計算量は、通常のセグ木では であるのに対して、Dynamic Segment Tree では となる。
- 基本的には初期化は単位元でしか行えない。(工夫すれば、単位元以外の値での初期化もできます。)
実装についての簡単な説明
ポインタを用いて実装します。多少書き慣れていないと少し理解するのが難しいかもしれません。ポインタを用いてデータ構造を書いた経験がない方は、まずは AOJ ALDS1 #8 を書いてみるのがおすすめです。
Dynamic Segment Tree はポインタを用いて書く関係上、全ての処理がトップダウン的になります。再帰セグ木に近い構造です。
まずは抽象化はしていない、シンプルな一点更新区間和取得 Dynamic Segment Tree の実装を見ていきましょう。主に通常のセグ木と異なる部分について、コード中にコメントを入れています。
#include <cstddef> class dynamic_segtree { public: // 初め、 root は nullptr となっている dynamic_segtree(size_t n) : n(n), root(nullptr) {} void set(size_t p, int x) { set(root, 0, n, p, x); } int get(size_t p) { return get(root, 0, n, p); } int prod(size_t l, size_t r) { return prod(root, 0, n, l, r); } private: // 各 node に、値と左右の子のポインタを持たせる struct node { int value; node* left; node* right; // 初め、左右の子は nullptr となっている node(int value) : value(value), left(nullptr), right(nullptr) {} }; size_t n; // セグ木の根のポインタを管理する node* root; void set(node*& t, size_t a, size_t b, size_t p, int x) { // そのノードがまだ作られていなかったら作る if (!t) t = new node(0); // 区間幅が 1、つまりセグ木の葉にたどり着いたら値を更新 if (b - a == 1) { t->value = x; return; } // それ以外の場合は左か右の子に進む size_t c = (a + b) >> 1; if (p < c) set(t->left, a, c, p, x); else set(t->right, c, b, p, x); // ノードの値の更新(左右の子が作られていない可能性があることに注意) t->value = 0; if (t->left) t->value += t->left->value; if (t->right) t->value += t->right->value; } // set とほぼ同じ int get(node*& t, size_t a, size_t b, size_t p) { if (!t) return 0; if (b - a == 1) return t->value; size_t c = (a + b) >> 1; if (p < c) return get(t->left, a, c, p); else return get(t->right, c, b, p); } int prod(node*& t, size_t a, size_t b, size_t l, size_t r) { // ノードが作られていない、もしくは区間の重なりがなければ、単位元を返す if (!t || b <= l || r <= a) return 0; // クエリ区間がノード区間を包含していれば、そのノードの持つ値を返す if (l <= a && b <= r) return t->value; // それ以外の場合は、左右の子に進む size_t c = (a + b) >> 1; return prod(t->left, a, c, l, r) + prod(t->right, c, b, l, r); } };
再帰セグ木をご存知でしたら理解は容易かと思います。private 内void set(t, a, b, p, x)
の一番初め、アクセスした時にそのノードがまだなかったら作る行が、Dynamic Segment Tree の肝中の肝です。
基本的な実装方針としては以上の通りなのですが、下記の実装コードでは、void set(t, a, b, p, x)
のノード作成時に、 が成り立つまでノードを作成し続けるのではなく、子が一つだけで葉まで続く部分は省略してノードの作成数を押さえる工夫を加えています。これにより、元々は一点更新クエリ毎に新しく作成されるノード数が だったものを高々1個まで減らすことができるため、一点更新クエリの回数を 回として、空間計算量を から に改善することができます。
この工夫についての詳細は kazuma8128 さんのブログをご覧ください。 kazuma8128.hatenablog.com
実装コード
抽象化して、さらにstd::unique_ptr
を使いより安全なメモリ確保を行うようにしたものです。public の部分を見るとわかるように、ACL の segtree と使い方はほぼ同じです。みんな使ってね。
※ 2021/3/28
ある区間を単位元に戻す(いらないノードを削除する)関数 reset
と
セグ木上の二分探索 max_right
, min_left
を追加しました。
#include <cassert> #include <cstddef> #include <memory> #include <utility> template <class S, S (*op)(S, S), S (*e)()> class dynamic_segtree { public: dynamic_segtree(size_t n) : n(n), root(nullptr) {} void set(size_t p, S x) { assert(p < n); set(root, 0, n, p, x); } S get(size_t p) const { assert(p < n); return get(root, 0, n, p); } S prod(size_t l, size_t r) const { assert(l <= r && r <= n); return prod(root, 0, n, l, r); } S all_prod() const { return root ? root->product : e(); } void reset(size_t l, size_t r) { assert(l <= r && r <= n); return reset(root, 0, n, l, r); } template <bool (*f)(S)> size_t max_right(size_t l) const { return max_right(l, [](S x) { return f(x); }); } template <class F> size_t max_right(size_t l, const F& f) const { assert(l <= n); S product = e(); assert(f(product)); return max_right(root, 0, n, l, f, product); } template <bool (*f)(S)> size_t min_left(size_t r) const { return min_left(r, [](S x) { return f(x); }); } template <class F> size_t min_left(size_t r, const F& f) const { assert(r <= n); S product = e(); assert(f(product)); return min_left(root, 0, n, r, f, product); } private: struct node; using node_ptr = std::unique_ptr<node>; struct node { size_t index; S value, product; node_ptr left, right; node(size_t index, S value) : index(index), value(value), product(value), left(nullptr), right(nullptr) {} void update() { product = op(op(left ? left->product : e(), value), right ? right->product : e()); } }; const size_t n; node_ptr root; void set(node_ptr& t, size_t a, size_t b, size_t p, S x) const { if (!t) { t = std::make_unique<node>(p, x); return; } if (t->index == p) { t->value = x; t->update(); return; } size_t c = (a + b) >> 1; if (p < c) { if (t->index < p) std::swap(t->index, p), std::swap(t->value, x); set(t->left, a, c, p, x); } else { if (p < t->index) std::swap(p, t->index), std::swap(x, t->value); set(t->right, c, b, p, x); } t->update(); } S get(const node_ptr& t, size_t a, size_t b, size_t p) const { if (!t) return e(); if (t->index == p) return t->value; size_t c = (a + b) >> 1; if (p < c) return get(t->left, a, c, p); else return get(t->right, c, b, p); } S prod(const node_ptr& t, size_t a, size_t b, size_t l, size_t r) const { if (!t || b <= l || r <= a) return e(); if (l <= a && b <= r) return t->product; size_t c = (a + b) >> 1; S result = prod(t->left, a, c, l, r); if (l <= t->index && t->index < r) result = op(result, t->value); return op(result, prod(t->right, c, b, l, r)); } void reset(node_ptr& t, size_t a, size_t b, size_t l, size_t r) const { if (!t || b <= l || r <= a) return; if (l <= a && b <= r) { t.reset(); return; } size_t c = (a + b) >> 1; reset(t->left, a, c, l, r); reset(t->right, c, b, l, r); t->update(); } template <class F> size_t max_right(const node_ptr& t, size_t a, size_t b, size_t l, const F& f, S& product) const { if (!t || b <= l) return n; if (f(op(product, t->product))) { product = op(product, t->product); return n; } size_t c = (a + b) >> 1; size_t result = max_right(t->left, a, c, l, f, product); if (result != n) return result; if (l <= t->index) { product = op(product, t->value); if (!f(product)) return t->index; } return max_right(t->right, c, b, l, f, product); } template <class F> size_t min_left(const node_ptr& t, size_t a, size_t b, size_t r, const F& f, S& product) const { if (!t || r <= a) return 0; if (f(op(t->product, product))) { product = op(t->product, product); return 0; } size_t c = (a + b) >> 1; size_t result = min_left(t->right, c, b, r, f, product); if (result != 0) return result; if (t->index < r) { product = op(t->value, product); if (!f(product)) return t->index + 1; } return min_left(t->left, a, c, r, f, product); } };
実行時間比較
の 3 つの実行時間の比較を行いました。
実行時間 ( 10回平均と95%信頼区間 ) | |
---|---|
1. Dynamic Segment Tree | 75.0 ± 3.0 ms |
2. Segtree ( ACL ) | 55.3 ± 1.3 ms |
3. Fenwick Tree ( ACL ) | 50.3 ± 0.5 ms |
本記事の Dynamic Segment Tree は他の 2 つに比べると多少遅いですが、クエリをオンラインで処理できるという点ではえらいですね。
問題例
- AtCoder Regular Contest 008 D - タコヤキオイシクナール
の関数列に対するクエリを扱えると座標圧縮をせずにすむので少し楽になります。 atcoder.jp
(Dynamic Segment Tree が使いたくなる他の問題例も募集中です!)
コード貼り付けただけのブログしかあげられない人生
— Lorent (@lorent_kyopro) 2021年3月11日