cp-library

競技プログラミング用 C++ ライブラリ

View the Project on GitHub qandori/cp-library

:heavy_check_mark: Trie

string/trie.hpp · GitHub で見る

共通ページ

文字列の重複付き集合を管理します。接頭辞ごとの個数、辞書順の順位・選択、LCP の集計に加え、各頂点に任意のデータを持たせて木 DP に使えます。

#include "lib/string/trie.hpp"

Trie<> trie;
int apple = trie.insert("apple", 2);
trie.insert("app");
trie.insert("bat");
assert(trie.size() == 4 && trie.distinct_size() == 3);
assert(trie.count("apple") == 2);
assert(trie.prefix_count("ap") == 3);
assert(trie.kth(1).value() == "apple"); // 0 始まり。重複も数える
assert(trie.order_of_key("bat") == 3);
assert(trie.lcp("apply") == 4);
assert(trie.lcp_sum("apply") == 11);   // 4+4+3+0
assert(trie.restore(apple) == "apple");
trie.erase("apple");                 // 1 個削除

Trie<Alphabet,First,Data> の既定値は英小文字 26 文字と追加データなしです。Trie<2,'A'> は A,B、Trie<10,'0'> は数字、Trie<256,0> は全 256 バイトに対応します。文字は [First,First+Alphabet) の範囲とし、辞書順は符号なしバイトの昇順です。空文字列も登録できます。

操作 内容・返り値 計算量
Trie() / clear() 空の Trie を作成/全ての頂点・登録・追加データを初期化 \(O(\sigma)\) / \(O(V+\sigma)\)
reserve(total_length) 文字数の合計を目安に頂点の領域を予約 \(O(V\sigma)\)
insert(s,copies=1) s を copies 個追加し、終端頂点を返す 償却 \(O(L+K\sigma)\)
erase(s,copies=1) 指定個数を削除。足りなければ変更せず false \(O(L)\)
count(s) / contains(s) 完全一致する個数/登録されているか \(O(L)\)
prefix_count(s) s を接頭辞に持つ登録文字列の個数 \(O(L)\)
size() / distinct_size() / empty() 重複込みの個数/異なる文字列数/未登録か \(O(1)\)
for_each_prefix(s,visit) s の接頭辞として登録された各終端頂点に visit(v)。短い順、重複はまとめて 1 回 \(O(L)\)
longest_prefix(s) s の接頭辞として登録された最長の文字列の頂点。なければ -1 \(O(L)\)
lcp(s) s と登録文字列の LCP 長の最大値。未登録なら 0 \(O(L)\)
lcp_sum(s) s と全登録文字列の LCP 長の総和。重複も数える \(O(L)\)
order_of_key(s) s より辞書順で小さい登録文字列の個数 \(O(L\sigma)\)
kth_node(k) 辞書順で 0 始まり k 番目の終端頂点。重複も数え、範囲外なら -1 \(O(H\sigma)\)
kth(k) 同じ選択結果を optional<string> で返す。範囲外なら nullopt \(O(H\sigma)\)
lower_bound(s) s 以上で最小の登録文字列を optional<string> で返す \(O((L+H)\sigma)\)

copies は正としてください。個数と LCP 長の総和は符号付きの int64_t です。個数・総和がその範囲を超える場合は std::overflow_error を送出します。追加する文字列は指定した文字範囲内とし、検索文字列には範囲外のバイトも使えます。NUL を含む文字列は string または長さを指定した string_view で渡してください。

頂点番号は根が 0、新しい接頭辞が作られた順に増えます。同じ文字列の追加では同じ終端頂点を返します。削除しても頂点と追加データは残り、再追加でも同じ番号を使います。clear() 以外では既存の頂点番号は変わりません。

頂点を扱う操作 内容・返り値 計算量
root / node_count() 根の頂点番号 0/根を含む頂点数 \(O(1)\)
ensure(s) 経路だけを作り、終端頂点を返す。登録数は増やさない 償却 \(O(L+K\sigma)\)
find(s) / walk(v,s) 根/頂点 v から s をたどった頂点。経路がなければ -1 \(O(L)\)
child(v,c) 文字 c に対応する子。存在しない、または範囲外なら -1 \(O(1)\)
children(v) 各文字の子の const array<int,Alphabet>&。添字は c-First、欠けた子は -1 \(O(1)\)
parent(v) / depth(v) / letter(v) 親/接頭辞の長さ/親からの文字。根の親は -1、根に letter は使えない \(O(1)\)
terminal_count(v) 頂点 v で終わる登録文字列の個数 \(O(1)\)
prefix_count(v) 頂点 v の部分木の登録文字列数。自身も含む \(O(1)\)
insert_node(v,copies=1) / erase_node(v,copies=1) 既存頂点の文字列を追加/削除。返り値は insert/erase と同じ \(O(d)\)
restore(v) 頂点が表す接頭辞を string で返す \(O(d)\)
data(v) 頂点の追加データを Data& で取得。const Trie では const Data& \(O(1)\)
for_each_ancestor(v,visit) v から根まで、下から順に visit(u) を呼ぶ。自身と根を含む \(O(d)\)

find・child・children は構造を参照する操作です。登録されているかは contains・terminal_count、その接頭辞の文字列が残っているかは prefix_count で判定してください。ensure だけで作った経路や、全て削除した後の経路も参照できます。

Data は初期化可能で、vector に格納できる型とします。各頂点の初期値は Data{} です。登録・削除で個数は自動更新しますが、追加データの計算は利用者が行います。コールバック内では data(v) を変更できます。走査中の文字列の追加・削除・clear() は行わないでください。data・children の参照は、頂点追加や reserve による再確保・clear()・代入・破棄で無効になります。

表の \(L\) は引数の文字列長、\(H\) は選ばれた文字列長、\(d\) は対象頂点の深さ、\(K\) は新しく作る頂点数、\(V\) は根を含む頂点数、\(\sigma\) は Alphabet です。Data の初期化・移動・破棄を定数時間としています。頂点数は INT_MAX 以下、保持する領域は \(O(V\sigma)\) です。再帰は使いません。

検証コード

コード

#ifndef TRIE_HPP
#define TRIE_HPP

#include <array>
#include <cassert>
#include <cstdint>
#include <limits>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>

namespace trie_detail {
struct Empty {};
}

template<int Alphabet = 26, int First = 'a', class Data = trie_detail::Empty>
class Trie {
	static_assert(0 < Alphabet && 0 <= First && First <= 255 && Alphabet <= 256 - First);
	struct Node {
		std::array<int, Alphabet> next;
		int parent, depth;
		char letter;
		std::int64_t count = 0, terminal = 0;
		[[no_unique_address]] Data data{};
		Node(int p = -1, int d = 0, char c = 0) : parent(p), depth(d), letter(c) { next.fill(-1); }
	};
	std::vector<Node> nodes_;
	int distinct_ = 0;
	static int symbol_(char c) { return int(static_cast<unsigned char>(c)) - First; }
	void check_([[maybe_unused]] int v) const { assert(0 <= v && v < node_count()); }
	void check_count_(std::int64_t copies) const {
		assert(copies > 0);
		if (copies > std::numeric_limits<std::int64_t>::max() - size()) throw std::overflow_error("Trie count");
	}
	int add_(int v, std::int64_t copies) {
		if (!nodes_[v].terminal) ++distinct_;
		nodes_[v].terminal += copies;
		for (int u = v; u != -1; u = nodes_[u].parent) nodes_[u].count += copies;
		return v;
	}
	template<class F>
	int walk_prefixes_(std::string_view s, F &&visit) const {
		int v = root;
		for (char c : s) {
			int u = child(v, c);
			if (u == -1 || !nodes_[u].count) break;
			v = u;
			visit(v);
		}
		return v;
	}

  public:
	static constexpr int root = 0;
	Trie() { nodes_.emplace_back(); }
	std::int64_t size() const { return nodes_[root].count; }
	bool empty() const { return size() == 0; }
	int distinct_size() const { return distinct_; }
	int node_count() const { return static_cast<int>(nodes_.size()); }
	void reserve(int total_length) {
		assert(0 <= total_length && total_length < std::numeric_limits<int>::max());
		nodes_.reserve(std::size_t(total_length) + 1);
	}
	void clear() { nodes_.clear(), nodes_.emplace_back(); distinct_ = 0; }
	int ensure(std::string_view s) {
		assert(s.size() < std::size_t(std::numeric_limits<int>::max()));
		int v = root;
		for (char c : s) {
			int x = symbol_(c);
			assert(0 <= x && x < Alphabet);
			int u = nodes_[v].next[x];
			if (u == -1) {
				assert(node_count() < std::numeric_limits<int>::max());
				u = node_count();
				nodes_.emplace_back(v, nodes_[v].depth + 1, c);
				nodes_[v].next[x] = u;
			}
			v = u;
		}
		return v;
	}
	int insert(std::string_view s, std::int64_t copies = 1) {
		check_count_(copies);
		return add_(ensure(s), copies);
	}
	int insert_node(int v, std::int64_t copies = 1) {
		check_(v), check_count_(copies);
		return add_(v, copies);
	}
	bool erase_node(int v, std::int64_t copies = 1) {
		check_(v), assert(copies > 0);
		if (nodes_[v].terminal < copies) return false;
		nodes_[v].terminal -= copies;
		if (!nodes_[v].terminal) --distinct_;
		for (int u = v; u != -1; u = nodes_[u].parent) nodes_[u].count -= copies;
		return true;
	}
	bool erase(std::string_view s, std::int64_t copies = 1) {
		assert(copies > 0);
		int v = find(s);
		return v != -1 && erase_node(v, copies);
	}
	int parent(int v) const { check_(v); return nodes_[v].parent; }
	int depth(int v) const { check_(v); return nodes_[v].depth; }
	char letter(int v) const { check_(v); assert(v != root); return nodes_[v].letter; }
	const std::array<int, Alphabet> &children(int v) const { check_(v); return nodes_[v].next; }
	int child(int v, char c) const {
		check_(v);
		int x = symbol_(c);
		return 0 <= x && x < Alphabet ? nodes_[v].next[x] : -1;
	}
	std::int64_t terminal_count(int v) const { check_(v); return nodes_[v].terminal; }
	std::int64_t prefix_count(int v) const { check_(v); return nodes_[v].count; }
	Data &data(int v) { check_(v); return nodes_[v].data; }
	const Data &data(int v) const { check_(v); return nodes_[v].data; }
	int walk(int v, std::string_view s) const {
		check_(v);
		for (char c : s) { v = child(v, c); if (v == -1) break; }
		return v;
	}
	int find(std::string_view s) const { return walk(root, s); }
	std::int64_t count(std::string_view s) const { int v = find(s); return v == -1 ? 0 : nodes_[v].terminal; }
	bool contains(std::string_view s) const { return count(s) != 0; }
	std::int64_t prefix_count(std::string_view s) const { int v = find(s); return v == -1 ? 0 : nodes_[v].count; }
	std::string restore(int v) const {
		check_(v);
		std::string s(nodes_[v].depth, '\0');
		for (int i = int(s.size()); i > 0; v = nodes_[v].parent) s[--i] = nodes_[v].letter;
		return s;
	}
	template<class F>
	void for_each_ancestor(int v, F &&visit) const {
		check_(v);
		for (; v != -1; v = nodes_[v].parent) visit(v);
	}
	template<class F>
	void for_each_prefix(std::string_view s, F &&visit) const {
		if (nodes_[root].terminal) visit(root);
		walk_prefixes_(s, [&](int v) {
			if (nodes_[v].terminal) visit(v);
		});
	}
	int longest_prefix(std::string_view s) const {
		int v = -1;
		for_each_prefix(s, [&](int u) { v = u; });
		return v;
	}
	int lcp(std::string_view s) const {
		return nodes_[walk_prefixes_(s, [](int) {})].depth;
	}
	std::int64_t lcp_sum(std::string_view s) const {
		std::int64_t sum = 0;
		walk_prefixes_(s, [&](int v) {
			if (nodes_[v].count > std::numeric_limits<std::int64_t>::max() - sum) throw std::overflow_error("Trie LCP sum");
			sum += nodes_[v].count;
		});
		return sum;
	}
	std::int64_t order_of_key(std::string_view s) const {
		int v = root;
		std::int64_t rank = 0;
		for (char c : s) {
			rank += nodes_[v].terminal;
			int x = symbol_(c);
			for (int j = 0; j < Alphabet && j < x; ++j) {
				int u = nodes_[v].next[j];
				if (u != -1) rank += nodes_[u].count;
			}
			if (x < 0 || x >= Alphabet) break;
			v = nodes_[v].next[x];
			if (v == -1) break;
		}
		return rank;
	}
	int kth_node(std::int64_t k) const {
		if (k < 0 || k >= size()) return -1;
		int v = root;
		while (true) {
			if (k < nodes_[v].terminal) return v;
			k -= nodes_[v].terminal;
			for (int u : nodes_[v].next) if (u != -1) {
				if (k < nodes_[u].count) { v = u; break; }
				k -= nodes_[u].count;
			}
		}
	}
	std::optional<std::string> kth(std::int64_t k) const {
		int v = kth_node(k);
		return v == -1 ? std::nullopt : std::optional<std::string>(restore(v));
	}
	std::optional<std::string> lower_bound(std::string_view s) const { return kth(order_of_key(s)); }
};

#endif
Back to top page