<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2025-06-29T11:55:24+00:00</updated><id>/feed.xml</id><title type="html">s-yata.jp</title><subtitle>I&apos;m on leave.</subtitle><author><name>矢田 晋 / Susumu Yata</name></author><entry><title type="html">久しぶりに LOUDS trie を実装して気付いたこと</title><link href="/2019/11/08/louds-trie.html" rel="alternate" type="text/html" title="久しぶりに LOUDS trie を実装して気付いたこと" /><published>2019-11-08T00:00:00+00:00</published><updated>2019-11-08T00:00:00+00:00</updated><id>/2019/11/08/louds-trie</id><content type="html" xml:base="/2019/11/08/louds-trie.html"><![CDATA[<h2 id="概要">概要</h2>

<p>LOUDS を用いた trie をいくつか実装してみて，その中で気付いたことをまとめてみることにしました．
LOUDS およびに LOUDS を用いた trie については <a href="https://takeda25.hatenablog.jp/archive/category/LOUDS">LOUDS カテゴリーの記事一覧 - アスペ日記</a> に詳しい説明があるため，ざっくりと省略します．</p>

<p>実装例は <a href="https://github.com/s-yata/louds-trie">s-yata/louds-trie: LOUDS-trie implementation example (C++)</a> にあります．</p>

<h2 id="要点">要点</h2>

<p>要点は以下の 3 つです．</p>

<ul>
  <li>LOUDS の 0/1 を反転することで，少し実装しやすくなります．</li>
  <li>階層別にデータを持つことで，整列済みのキー集合から無駄なく構築できます．</li>
  <li>子ノードの探索に二分探索を使うことで，検索を高速化できます．</li>
</ul>

<h3 id="louds-の-01-を反転すること">LOUDS の 0/1 を反転すること</h3>

<p>LOUDS というデータ構造では，各ノードについて，子ノードの数だけ 1 を並べ，その後に 0 を置きます．
この 0 と 1 を反転することで，実装が少しだけ楽になります．
楽になる理由は，実装に用いる CPU 命令 POPCNT (<code class="language-plaintext highlighter-rouge">__builtin_popcountll</code>), CTZ (<code class="language-plaintext highlighter-rouge">__builtin_ctzll</code>) との相性が良いからです．
簡潔ビットベクトルの実装によっては，速度に影響が出ることもあります．</p>

<p>簡潔データ構造の根幹にあたる 0/1 を反転することは暴挙に思えるかもしれませんが，実装に都合が良い方を選べば良いのではないでしょうか．</p>

<h3 id="階層別にデータを持つこと">階層別にデータを持つこと</h3>

<p>LOUDS を用いた trie の構成は以下のようになります．</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">Trie</span> <span class="p">{</span>
  <span class="n">BitVector</span> <span class="n">louds</span><span class="p">;</span>
  <span class="n">BitVector</span> <span class="n">outs</span><span class="p">;</span>
  <span class="n">vector</span><span class="o">&lt;</span><span class="kt">uint8_t</span><span class="o">&gt;</span> <span class="n">labels</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p>しかし，この構成では，整列済みのキー集合から 1-pass で構築することができません．
一巡目では各階層のビット数やノード数がわからず，書き込み位置が定まらないからです．</p>

<p>各配列を階層別に持つように変更すれば，この問題は解決します．
変更後の構成は以下のようになります．</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">Level</span> <span class="p">{</span>
  <span class="n">BitVector</span> <span class="n">louds</span><span class="p">;</span>
  <span class="n">BitVector</span> <span class="n">outs</span><span class="p">;</span>
  <span class="n">vector</span><span class="o">&lt;</span><span class="kt">uint8_t</span><span class="o">&gt;</span> <span class="n">labels</span><span class="p">;</span>
<span class="p">};</span>

<span class="k">struct</span> <span class="nc">Trie</span> <span class="p">{</span>
  <span class="n">vector</span><span class="o">&lt;</span><span class="n">Level</span><span class="o">&gt;</span> <span class="n">levels</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p>階層別にすれば，一巡目でも各階層の書き込み位置が定まるため， 1-pass で構築できます．
入力し終わったキーを破棄しながら構築できるため，メモリ使用量も減らせます．
LOUDS trie はサイズが小さいため，ほかの trie を構築する準備段階として使っても良いと思います．</p>

<h3 id="子ノードの探索に二分探索を使うこと">子ノードの探索に二分探索を使うこと</h3>

<p>LOUDS trie の実装を探してみると，子ノードの探索に線形探索を使うものが見つかります．
子ノードが少なければ問題になりませんが，子ノードが多いと効率が悪くなります．
二分探索を使うことで高速化できる可能性が高いです．
<a href="http://sile.hatenablog.jp/entry/20100619/1276985956">LOUDS++(6): trie改良試作(TAIL配列版) - sileのブログ</a> や <a href="http://sile.hatenablog.jp/entry/20100625/1277423827">LOUDS++(8): trie - 検索速度向上 - sileのブログ</a> で言及されているのと同じことです．</p>

<p>整列済みのキー集合から構築した LOUDS trie であれば，子ノードのラベルは昇順に並んでいるはずです．
後は，子ノードの数さえわかれば，二分探索は簡単に実装できます．</p>

<p>重要になるのは子ノードの数を求める方法です．
LOUDS としては select で求めるのが正攻法ですが， select は若干重たい処理になるので，あまり使いたくありません．
子ノードが多いノードばかりということは稀なので， LOUDS を少し先まで見て，子ノードの終わりを示す 1 を探す方が速いケースが大半だと思います．</p>

<p>実装例から子ノードの数を求める部分を抜き出したものが以下になります．
子ノードの数は最大でも 256 なので， select は使わずに， LOUDS を 64 bits ずつチェックするようにしています．</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">uint64_t</span> <span class="n">end</span> <span class="o">=</span> <span class="n">node_pos</span><span class="p">;</span>
<span class="kt">uint64_t</span> <span class="n">word</span> <span class="o">=</span> <span class="n">level</span><span class="p">.</span><span class="n">louds</span><span class="p">.</span><span class="n">words</span><span class="p">[</span><span class="n">end</span> <span class="o">/</span> <span class="mi">64</span><span class="p">]</span> <span class="o">&gt;&gt;</span> <span class="p">(</span><span class="n">end</span> <span class="o">%</span> <span class="mi">64</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">word</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">end</span> <span class="o">+=</span> <span class="mi">64</span> <span class="o">-</span> <span class="p">(</span><span class="n">end</span> <span class="o">%</span> <span class="mi">64</span><span class="p">);</span>
  <span class="n">word</span> <span class="o">=</span> <span class="n">level</span><span class="p">.</span><span class="n">louds</span><span class="p">.</span><span class="n">words</span><span class="p">[</span><span class="n">end</span> <span class="o">/</span> <span class="mi">64</span><span class="p">];</span>
  <span class="k">while</span> <span class="p">(</span><span class="n">word</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">end</span> <span class="o">+=</span> <span class="mi">64</span><span class="p">;</span>
    <span class="n">word</span> <span class="o">=</span> <span class="n">level</span><span class="p">.</span><span class="n">louds</span><span class="p">.</span><span class="n">words</span><span class="p">[</span><span class="n">end</span> <span class="o">/</span> <span class="mi">64</span><span class="p">];</span>
  <span class="p">}</span>
<span class="p">}</span>
<span class="n">end</span> <span class="o">+=</span> <span class="n">__builtin_ctzll</span><span class="p">(</span><span class="n">word</span><span class="p">);</span>
<span class="kt">uint64_t</span> <span class="n">begin</span> <span class="o">=</span> <span class="n">node_id</span><span class="p">;</span>
<span class="n">end</span> <span class="o">=</span> <span class="n">begin</span> <span class="o">+</span> <span class="n">end</span> <span class="o">-</span> <span class="n">node_pos</span><span class="p">;</span>
</code></pre></div></div>

<p>ラベルの種類が多くて子ノードの数が大きくなる用途では，なかなか見つからないときに select を使うなど，最悪ケースを避ける工夫をするべきです．
たとえば，ラベルの型を <code class="language-plaintext highlighter-rouge">uint16_t</code> にするのであれば，回避策を入れておかないと最悪ケースで残念なことになります．</p>]]></content><author><name>矢田 晋 / Susumu Yata</name></author><summary type="html"><![CDATA[概要]]></summary></entry><entry><title type="html">表示確認用</title><link href="/2018/11/13/test.html" rel="alternate" type="text/html" title="表示確認用" /><published>2018-11-13T00:00:00+00:00</published><updated>2018-11-13T00:00:00+00:00</updated><id>/2018/11/13/test</id><content type="html" xml:base="/2018/11/13/test.html"><![CDATA[<h2 id="heading-2">Heading 2</h2>

<p>Hello, world!!</p>

<h3 id="heading-3">Heading 3</h3>

<p>Hello, world!!!</p>

<h4 id="heading-4">Heading 4</h4>

<p>Hello, world!!!</p>

<h5 id="heading-5">Heading 5</h5>

<p>Hello, world!!!</p>

<h2 id="見出し-2">見出し 2</h2>

<p>いろはにほへと</p>

<h3 id="見出し-3">見出し 3</h3>

<p>いろはにほへと</p>

<h4 id="見出し-4">見出し 4</h4>

<p>いろはにほへと</p>

<h5 id="見出し-5">見出し 5</h5>

<p>いろはにほへと</p>

<h2 id="テーブルの例">テーブルの例</h2>

<p>テーブルです．</p>

<table>
  <thead>
    <tr>
      <th>A</th>
      <th>B</th>
      <th>C</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>D</td>
      <td>E</td>
      <td>F</td>
    </tr>
    <tr>
      <td>g</td>
      <td>h</td>
      <td>i</td>
    </tr>
  </tbody>
</table>

<table>
  <thead>
    <tr>
      <th>あいうえお</th>
      <th>かきくけこ</th>
      <th>さしすせそ</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>タチツテト</td>
      <td>ナニヌネノ</td>
      <td>ハヒフヘホ</td>
    </tr>
  </tbody>
</table>

<h2 id="ソースコードの例">ソースコードの例</h2>

<p>ソースコードです．</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"Hello, world!</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name>矢田 晋 / Susumu Yata</name></author><summary type="html"><![CDATA[Heading 2]]></summary></entry></feed>