トライ木(Trie木) の解説と実装【接頭辞(prefix) を利用したデータ構造】
Trie木は、効率的な検索(retrieval)のために使われるデータ構造です。文字列などの先頭部分(接頭辞: prefix)の共通部分を共有して保存することで、\(O(M)\) での検索を可能にします。(文字列の長さを\(M\) としました。)
以下は、「fire・firearm・firework・fireman・algo」をトライ木に保存した時の状態を表しています。

トライ木の仕組み
基本は木
複数の頂点から構成された順序付きの木になっています。効率的な検索のために
- 1つの頂点が一つの文字を表す
- 文字列の先頭部分(prefix)が共通する場合は、なるべく無駄が無いように共有する
というようになっています。
頂点
トライ木は、一つの頂点(ノード)が一文字を表現した木となっています。必要に応じて様々な情報を頂点が持たせると実装しやすいです。基本としては、
- その頂点の子は何か
- その頂点で末端となる文字列はあるか
- その頂点はどんな文字列を表現しているか
などが必要になるでしょう。
トライ木の機能
基本的な機能としては以下の2つです。
- 挿入(insert):文字列をトライ木に保存する
- 検索(search):文字列がトライ木に保存されているか確認する
文字列の挿入
挿入時は、文字列を一文字ずつ確認して、Trie 木の頂点を1つずつたどっていきます。もしまだ頂点が存在しなければ、新しく作成することになります。


文字列の検索
検索時も、文字列を一文字ずつ確認して、Trie 木の頂点を1つずつたどっていきます。もし頂点が存在しなければ、その文字列は Trie 木に挿入されていなかったということになります。
最後の頂点にたどり着いたら、その頂点で終了する文字列があるかを確認する必要があります。例えば、"firework" を挿入したなら、true を返すのは “firework" を検索した時で、"firewo" などを検索した時には false を返してもらうためです。


C++での実装例
ベース
実装方法は様々なものがありますが、頂点の集合を vector などに保存しておくことにして実装していくことにします。C++では、以下のように構造体などで実装すると変更がしやすいです。
template <int char_size, int base>
struct Trie {
struct Node { // 頂点を
vector<int> next; // 子の頂点番号を格納。存在しなければ-1
vector<int> accept; // 末端がこの頂点になる文字列の str_id を保存
int c; // base からの間隔をint型で表現したもの
int common; // いくつの文字列がこの頂点を共有しているか
Node(int c_) : c(c_), common(0) {
next.assign(char_size, -1);
}
};
vector<Node> nodes; // trie 木本体
int root; // 根
Trie() : root(0) {
// 初期化。はじめは根しか無い
nodes.push_back(Node(root));
}
/*
その他のメソッド(insert や search) などを実装する
*/
}
※ 頂点は子の頂点番号を保持することにしていますが、子の頂点への参照を保持させるような実装もあります。その場合は、少し実装が複雑になりますが、頂点の集合を vector で保持しておく必要などはありません。
※ 文字の種類は char_size に限定されているとして、その 0 番目に当たるものを base としています。例えば、Trie<26, 'a’> であれば、アルファベット小文字26種類からなる文字列のみを考えることになります。このようにすることで、「’a’ を数字の 0」「’c’を数字の2」などと簡単に対応させることができます。
単語の挿入

一文字ずつ頂点が存在するか確認していきましょう。存在しない場合には、頂点を新しく追加する必要があります。
最後の頂点には、単語の終端だと分かるように単語の番号を加えています。
void insert(const string &word, int word_id) { // 単語:str と 単語の番号: str_id
int node_id = 0;
for (int i = 0; i < (int)word.size(); i++) {
int c = (int)(word[i] - base);
int &next_id = nodes[node_id].next[c];
if (next_id == -1) { // 次の頂点が存在しなければ追加
next_id = (int)nodes.size();
nodes.push_back(Node(c));
}
++nodes[node_id].common;
node_id = next_id;
}
++nodes[node_id].common;
nodes[node_id].accept.push_back(word_id); // 単語の終端なので、頂点に番号を入れておく
}
void insert(const string &word) {
insert(word, nodes[0].common);
}
単語の検索

単語を1文字ずつ確認していき、次の頂点が存在しなければ終了します。
全ての頂点があったとしても、最後の頂点が受理状態か確認する必要があります。
// 単語とprefixの検索
bool search(const string &word, bool prefix = false) {
int node_id = 0;
for (int i = 0; i < (int)word.size(); i++) {
int c = (int)(word[i] - base);
int &next_id = nodes[node_id].next[c];
if (next_id == -1) { // 次の頂点が存在しなければ終了
return false;
}
node_id = next_id;
}
return nodes[node_id].accept.size() > 0; // 最後の頂点が受理状態か確認
}
その他の機能
いくつかあると便利な機能を加えておきます。プログラミングコンテストなどでは、これらの実装例そのままで解ける問題は少ないですが、トライ木を利用するための元になります。
指定した prefix を持つ文字列があるか検索
search のコードを一部変更することで、単語の検索だけではなく、指定した接頭辞(prefix) を持つ単語が存在するか判定することができます。終端の頂点が受理状態であろうと無かろうと true を返すようにすれば良いです。
例えば、「fireman」を挿入していれば、「firem」を接頭辞として持つことになります。
// 単語とprefixの検索
bool search(const string &word, bool prefix = false) {
int node_id = 0;
for (int i = 0; i < (int)word.size(); i++) {
int c = (int)(word[i] - base);
int &next_id = nodes[node_id].next[c];
if (next_id == -1) { // 次の頂点が存在しなければ終了
return false;
}
node_id = next_id;
}
return (prefix) ? true : nodes[node_id].accept.size() > 0; // 最後の頂点が受理状態か確認
}
// prefix を持つ単語が存在するかの検索
bool start_with(const string &prefix) {
return search(prefix, true);
}
挿入した単語の総数
全ての頂点に、その頂点を共有している単語の数を記録してあるので、根(root) の頂点について確認すれば、挿入した単語の総数を確認することができます。
// 挿入した単語の数
int count() const {
return (nodes[0].common);
}
頂点の総数
prefix を共有することで、いくつか頂点を節約できているはずです。この頂点は vector で管理していたので、簡単に求めることができます。
// Trie木のノード数
int size() const {
return ((int)nodes.size());
}
C++ での実装まとめ
上述の全ての機能を盛り込んだ実装のまとめです。
#include <bits/stdc++.h>
using namespace std;
/* Trie 木: 文字の種類(char_size)、int型で0に対応する文字(base)
insert(word): 単語 word を Trie 木に挿入する
search(word): 単語 word が Trie 木にあるか判定する
start_with(prefix): prefix が一致する単語が Trie 木にあるか判定する
count(): 挿入した単語の数を返す
size(): Trie 木の頂点数を返す
計算量:insert, search ともに O(M)(Mは単語の長さ)
*/
template <int char_size, int base>
struct Trie {
struct Node { // 頂点を表す構造体
vector<int> next; // 子の頂点番号を格納。存在しなければ-1
vector<int> accept; // 末端がこの頂点になる単語の word_id を保存
int c; // base からの間隔をint型で表現したもの
int common; // いくつの単語がこの頂点を共有しているか
Node(int c_) : c(c_), common(0) {
next.assign(char_size, -1);
}
};
vector<Node> nodes; // trie 木本体
int root;
Trie() : root(0) {
nodes.push_back(Node(root));
}
// 単語の挿入
void insert(const string &word, int word_id) {
int node_id = 0;
for (int i = 0; i < (int)word.size(); i++) {
int c = (int)(word[i] - base);
int &next_id = nodes[node_id].next[c];
if (next_id == -1) { // 次の頂点が存在しなければ追加
next_id = (int)nodes.size();
nodes.push_back(Node(c));
}
++nodes[node_id].common;
node_id = next_id;
}
++nodes[node_id].common;
nodes[node_id].accept.push_back(word_id);
}
void insert(const string &word) {
insert(word, nodes[0].common);
}
// 単語とprefixの検索
bool search(const string &word, bool prefix = false) {
int node_id = 0;
for (int i = 0; i < (int)word.size(); i++) {
int c = (int)(word[i] - base);
int &next_id = nodes[node_id].next[c];
if (next_id == -1) { // 次の頂点が存在しなければ終了
return false;
}
node_id = next_id;
}
return (prefix) ? true : nodes[node_id].accept.size() > 0;
}
// prefix を持つ単語が存在するかの検索
bool start_with(const string &prefix) {
return search(prefix, true);
}
// 挿入した単語の数
int count() const {
return (nodes[0].common);
}
// Trie木のノード数
int size() const {
return ((int)nodes.size());
}
};
int main() {
Trie<26, 'A'> trie;
trie.insert("FIRE");
cout << trie.search("FIRE") << endl; // 1 を返す
cout << trie.search("FI") << endl; // 0 を返す
trie.insert("FIREMAN");
trie.insert("FIREARM");
trie.insert("FIREWORK");
trie.insert("ALGO");
return 0;
}
練習問題
- Implement Trie (Prefix Tree) – LeetCode:単純な Trie 木を実装するだけの問題です
- Google Kick Start 2020 Round A Bundling :Trie 木を上手く利用すると簡単に解くことができます
ディスカッション
コメント一覧
まだ、コメントがありません