<?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="https://hydrakecat.net/feed.xml" rel="self" type="application/atom+xml" /><link href="https://hydrakecat.net/" rel="alternate" type="text/html" /><updated>2025-08-12T10:31:34+00:00</updated><id>https://hydrakecat.net/feed.xml</id><title type="html">Hiroshi Kurokawa’s site</title><subtitle></subtitle><entry><title type="html">SA-IS（Suffix Array - Induced Sorting）アルゴリズム</title><link href="https://hydrakecat.net/2025/08/11/sa-is.html" rel="alternate" type="text/html" title="SA-IS（Suffix Array - Induced Sorting）アルゴリズム" /><published>2025-08-11T15:00:00+00:00</published><updated>2025-08-11T15:00:00+00:00</updated><id>https://hydrakecat.net/2025/08/11/sa-is</id><content type="html" xml:base="https://hydrakecat.net/2025/08/11/sa-is.html"><![CDATA[<p><a href="https://www.iwanami.co.jp/book/b257894.html">『高速文字列解析の世界』</a>を読んでいたら、Suffix Arrayを線形時間で構築するアルゴリズムとしてInduced Sorting（IS）を用いたものが紹介されていた。文章で読んだだけでは、いまいち十分に理解できた気分にならなかったので、自分で実装してみた。これはそのSA-ISアルゴリズムの解説である。</p>

<p>ソースコードは <a href="https://github.com/hkurokawa/bwt-java">https://github.com/hkurokawa/bwt-java</a> にあるので、適宜参照してほしい。</p>

<!-- more -->

<p><a href="https://ja.wikipedia.org/wiki/%E6%8E%A5%E5%B0%BE%E8%BE%9E%E9%85%8D%E5%88%97">接尾辞配列（SA; Suffix Array）</a>については既知とする。</p>

<p>SAを構築するナイーブな方法は、すべての接尾辞（元の文字列の長さを<code class="language-plaintext highlighter-rouge">n</code>とすると、<code class="language-plaintext highlighter-rouge">n</code>個ある）をソートするものである。ソート自体は \(O(n \log n)\) でできるが、接尾辞同士の比較に \(O(n)\) かかるので、全体としては \(O(n^2 \log n)\) になってしまう。</p>

<p>これを改善して、のみならず、 \(O(n)\) でやってしまうのがInduced Sorting（IS）を利用したアルゴリズムである。なおメモリ空間使用量も \(O(n)\) で収まる。</p>

<h2 id="induced-sorting-isとは">Induced Sorting (IS)とは</h2>
<p>“induced”は日本語で「誘起された」と訳されることが多い。このアルゴリズムが提案された元論文<a href="https://ieeexplore.ieee.org/abstract/document/5582081">Two Efficient Algorithms for Linear Time Suffix Array Construction, G Nong, et.al.</a>をざっと眺めた感じだと、貪欲に表を埋めていったときに自然とソート状態が達成される操作を”induced sorting”と呼んでいるように感じる。この呼び方が接尾辞配列以外でも使う一般的なものなのかは知らない。なにかご存知の方がいれば、教えていただきたい。</p>

<h2 id="大まかな手順">大まかな手順</h2>
<p>最初に、大まかなアルゴリズムを説明する。目的は与えられた文字列から接尾辞配列を作ること。ここでは簡単のために文字列は英小文字から成り立ち、文字列の最後尾にどの文字よりも小さい番兵（sentinel）文字<code class="language-plaintext highlighter-rouge">$</code>があるとする。</p>

<p>基本的にはバケットソートを行う。たとえば与えられた文字列の文字がすべて相異なるなら、バケットソートを1回行うだけで接尾辞配列が得られる。一方、同じ文字が複数回登場するなら、各バケット内で接尾辞同士をソートしなければならない。これをなんとか\(O(n)\)で実行したい。</p>

<p>ここで接尾辞をつぎの2種類に分けて考える。</p>
<ol>
  <li>S-type: \(S_i \lt S_{i+1}\)となる接尾辞もしくは番兵文字（<code class="language-plaintext highlighter-rouge">$</code>）</li>
  <li>L-type: \(S_i \gt S_{i+1}\)となる接尾辞</li>
</ol>

<p>すると、同じバケット内ではL-typeの接尾辞がS-typeの接尾辞よりも前に位置することが保証される。</p>

<p>これを厳密ではないが直感的な方法で説明する。同じバケット内ということはどちらも同じ文字から始まっており、ここではその文字を”b”としよう。すなわち、S-typeは”bc…“や”bbc…“のような最初に現れる”b”以外の文字が”b”より大きい文字列である。一方、L-typeは”ba…“もしくは”bba…“のような最初に現れる”b”以外の文字が”b”より小さい文字列である。この2つの接尾辞を比較すると、L-typeの方が常にS-typeのものより小さくなる。</p>

<p>実際に”abracadabra”という文字列の接尾辞配列はつぎのようになり、同じバケット内でL-typeがS-typeより前に位置することが分かるだろう。</p>

<table>
  <thead>
    <tr>
      <th>index</th>
      <th>pos</th>
      <th>suffix</th>
      <th>bucket</th>
      <th>type</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>11</td>
      <td>$</td>
      <td>$</td>
      <td>S</td>
    </tr>
    <tr>
      <td>1</td>
      <td>10</td>
      <td>a$</td>
      <td>a</td>
      <td>L</td>
    </tr>
    <tr>
      <td>2</td>
      <td>7</td>
      <td>abra$</td>
      <td>a</td>
      <td>S</td>
    </tr>
    <tr>
      <td>3</td>
      <td>0</td>
      <td>abracadabra$</td>
      <td>a</td>
      <td>S</td>
    </tr>
    <tr>
      <td>4</td>
      <td>3</td>
      <td>acadabra$</td>
      <td>a</td>
      <td>S</td>
    </tr>
    <tr>
      <td>5</td>
      <td>5</td>
      <td>adabra$</td>
      <td>a</td>
      <td>S</td>
    </tr>
    <tr>
      <td>6</td>
      <td>8</td>
      <td>bra$</td>
      <td>b</td>
      <td>S</td>
    </tr>
    <tr>
      <td>7</td>
      <td>1</td>
      <td>bracadabra$</td>
      <td>b</td>
      <td>S</td>
    </tr>
    <tr>
      <td>8</td>
      <td>4</td>
      <td>cadabra$</td>
      <td>c</td>
      <td>L</td>
    </tr>
    <tr>
      <td>9</td>
      <td>6</td>
      <td>dabra$</td>
      <td>d</td>
      <td>L</td>
    </tr>
    <tr>
      <td>10</td>
      <td>9</td>
      <td>ra$</td>
      <td>r</td>
      <td>L</td>
    </tr>
    <tr>
      <td>11</td>
      <td>2</td>
      <td>racadabra$</td>
      <td>r</td>
      <td>L</td>
    </tr>
  </tbody>
</table>

<p>さらに興味深いことに、S-typeの接尾辞がすでに表に埋められている場合、以下の手順で自然にバケット内のL-type同士がソートされた状態が実現できる（詳細は後述）。</p>
<ol>
  <li>表の先頭を見る</li>
  <li>すでに\(S_i\)が埋まっていたら\(S_{i-1}\)がL-typeか調べる</li>
  <li>L-typeだったら、\(S_{i-1}\)を該当するバケット内の空いている先頭に入れる</li>
  <li>その行が空もしくは\(S_{i-1}\)がS-typeだったら何もしないでつぎの行に移る</li>
  <li>#2-#4を表の最後に達するまで行う</li>
</ol>

<p>したがって、まずはS-typeの接尾辞をソート済みの状態で表に入れることを目指す。実はこのとき、すべてのS-typeの接尾辞が表に埋まっている必要はない。「\(S_{i-1}\)がL-typeであるようなS-typeの\(S_i\)」だけ表に埋まっていればよい。このようなS-typeの接尾辞は連続するS-type接尾辞の左端でもあるのでLMS (Leftmost S-type)と呼ぶ（『高速文字列解析の世界』では\(S^*\)と表記）。このLMSはL-typeを埋めるために必要なので、とりあえずLMS同士がソートされていればよい（どうやってLMS同士をソートするかは後述）。</p>

<p>このLMSだけ埋められた表にL-typeの接尾辞を埋め、そのあとLMSはいったん表から削除する。そしてL-typeの接尾辞だけ埋められた表にS-typeの接尾辞を埋めていく。そうすると表全体すなわち接尾辞配列が完成する。</p>

<p>全体の流れはつぎのようになる。</p>
<ol>
  <li>接尾辞をS-type、L-typeに分類</li>
  <li>S-typeの接尾辞のうちLMSのものを取り出してソートする</li>
  <li>表にLMSを埋める</li>
  <li>そのLMSを使って表にL-typeの接尾辞を埋める</li>
  <li>LMSを表から削除する</li>
  <li>表に埋められたL-type接尾辞を使ってS-type接尾辞を埋める</li>
</ol>

<h2 id="アルゴリズムの詳細">アルゴリズムの詳細</h2>
<p>ここでは、さきほど述べたアルゴリズムの詳細を述べる。分かりやすさを優先して、実際の実行順とは異なり、つぎの順番で説明する。</p>
<ol>
  <li>表にLMSが埋められた状態でL-typeの接尾辞を埋める（前節の#4）</li>
  <li>表にL-typeの接尾辞が埋められた状態でS-typeの接尾辞を埋める（前節の#6）</li>
  <li>LMSをソートする（前節の#2）</li>
</ol>

<h3 id="l-typeの接尾辞を埋める">L-typeの接尾辞を埋める</h3>
<p>“abracadabra”の例だと、LMSだけ埋めた状態はつぎのようになる（LMSのtypeはS*と表記）。先ほどの表と見比べれば分かるが、接尾辞の位置は最終的な位置と若干異なる。たとえば\(S_7\)の位置はここではindex 3になっているが、最終的な接尾辞配列ではindex 2にいるべきだ。</p>

<table>
  <thead>
    <tr>
      <th>index</th>
      <th>pos</th>
      <th>suffix</th>
      <th>bucket</th>
      <th>type</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>11</td>
      <td>$</td>
      <td>$</td>
      <td>S*</td>
    </tr>
    <tr>
      <td>1</td>
      <td> </td>
      <td> </td>
      <td>a</td>
      <td> </td>
    </tr>
    <tr>
      <td>2</td>
      <td> </td>
      <td> </td>
      <td>a</td>
      <td> </td>
    </tr>
    <tr>
      <td>3</td>
      <td>7</td>
      <td>abra$</td>
      <td>a</td>
      <td>S*</td>
    </tr>
    <tr>
      <td>4</td>
      <td>3</td>
      <td>acadabra$</td>
      <td>a</td>
      <td>S*</td>
    </tr>
    <tr>
      <td>5</td>
      <td>5</td>
      <td>adabra$</td>
      <td>a</td>
      <td>S*</td>
    </tr>
    <tr>
      <td>6</td>
      <td> </td>
      <td> </td>
      <td>b</td>
      <td> </td>
    </tr>
    <tr>
      <td>7</td>
      <td> </td>
      <td> </td>
      <td>b</td>
      <td> </td>
    </tr>
    <tr>
      <td>8</td>
      <td> </td>
      <td> </td>
      <td>c</td>
      <td> </td>
    </tr>
    <tr>
      <td>9</td>
      <td> </td>
      <td> </td>
      <td>d</td>
      <td> </td>
    </tr>
    <tr>
      <td>10</td>
      <td> </td>
      <td> </td>
      <td>r</td>
      <td> </td>
    </tr>
    <tr>
      <td>11</td>
      <td> </td>
      <td> </td>
      <td>r</td>
      <td> </td>
    </tr>
  </tbody>
</table>

<p>ここから、L-typeの接尾辞を埋めていく。</p>

<p>まず表の一番上、index 0を見る。index 0には\(S_{11}\)が埋まっているので、その1つ前の接尾辞\(S_{10}\)のtypeを調べるとL-typeなので表に入れる。\(S_{10}\)はすなわち”a$”である。これをバケット”a”の空いている箇所のうち一番先頭、つまり、表のindex 1に入れる。</p>

<p>つぎに表のindex 1を見ると、先ほど入れた\(S_{10}\)が入っている。この1つ前の接尾辞\(S_9\)はL-typeで”ra$”なのでバケット”r”の先頭、index 10に入る。</p>

<p>そのつぎのindex 2は埋まっていないので飛ばしてindex 3をみると\(S_7\)である。この1つ前の接尾辞\(S_6\)はL-typeで”dabra$”である。バケット”d”はindex 9に1つしか枠がないので、そこに入れる。</p>

<p>index 4およびindex 5についても同様にして\(S_2\)と\(S_4\)を表に埋める。ここまでで表はつぎのようになる。</p>

<table>
  <thead>
    <tr>
      <th>index</th>
      <th>pos</th>
      <th>suffix</th>
      <th>bucket</th>
      <th>type</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>11</td>
      <td>$</td>
      <td>$</td>
      <td>S*</td>
    </tr>
    <tr>
      <td>1</td>
      <td>10</td>
      <td>a$</td>
      <td>a</td>
      <td>L</td>
    </tr>
    <tr>
      <td>2</td>
      <td> </td>
      <td> </td>
      <td>a</td>
      <td> </td>
    </tr>
    <tr>
      <td>3</td>
      <td>7</td>
      <td>abra$</td>
      <td>a</td>
      <td>S*</td>
    </tr>
    <tr>
      <td>4</td>
      <td>3</td>
      <td>acadabra$</td>
      <td>a</td>
      <td>S*</td>
    </tr>
    <tr>
      <td>5</td>
      <td>5</td>
      <td>adabra$</td>
      <td>a</td>
      <td>S*</td>
    </tr>
    <tr>
      <td>6</td>
      <td> </td>
      <td> </td>
      <td>b</td>
      <td> </td>
    </tr>
    <tr>
      <td>7</td>
      <td> </td>
      <td> </td>
      <td>b</td>
      <td> </td>
    </tr>
    <tr>
      <td>8</td>
      <td>4</td>
      <td>cadabra$</td>
      <td>c</td>
      <td>L</td>
    </tr>
    <tr>
      <td>9</td>
      <td>6</td>
      <td>dabra$</td>
      <td>d</td>
      <td>L</td>
    </tr>
    <tr>
      <td>10</td>
      <td>9</td>
      <td>ra$</td>
      <td>r</td>
      <td>L</td>
    </tr>
    <tr>
      <td>11</td>
      <td>2</td>
      <td>racadabra$</td>
      <td>r</td>
      <td>L</td>
    </tr>
  </tbody>
</table>

<p>このあとはindex 6および7は埋まっていないので飛ばして、8から11に関してはそれぞれの1つ前の接尾辞はL-typeでないので何もしない。以上でL-typeはすべて埋まった。</p>

<p>この手順でL-type接尾辞がソート済みの状態で表に埋まることが不思議に思えるかもしれない。それについて説明する。</p>

<p>まず、すべてのL-type接尾辞\(S_i\)について\(S_i \gt S_{i+1}\)が成り立つ。この\(S_{i+1}\)はL-typeかLMSかのどちらかである。いいかえると\(S_i\)がL-typeなら、\(S_{i+1}\)は表中で\(S_i\)よりも上側に位置してすでに埋まっているはずである。以上のことから、表を上から下まで見ていくさきほどの手順では、かならず新しく埋められるL-typeの接尾辞はいま見ているところよりも後ろに位置し、すべてのL-typeが網羅されることが保証される。</p>

<h3 id="s-typeの接尾辞を埋める">S-typeの接尾辞を埋める</h3>

<p>さて、L-typeの接尾辞が埋まったので、LMSはいったん削除し、表はつぎのようになる。</p>

<table>
  <thead>
    <tr>
      <th>index</th>
      <th>pos</th>
      <th>suffix</th>
      <th>bucket</th>
      <th>type</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td> </td>
      <td> </td>
      <td>$</td>
      <td> </td>
    </tr>
    <tr>
      <td>1</td>
      <td>10</td>
      <td>a$</td>
      <td>a</td>
      <td>L</td>
    </tr>
    <tr>
      <td>2</td>
      <td> </td>
      <td> </td>
      <td>a</td>
      <td> </td>
    </tr>
    <tr>
      <td>3</td>
      <td> </td>
      <td> </td>
      <td>a</td>
      <td> </td>
    </tr>
    <tr>
      <td>4</td>
      <td> </td>
      <td> </td>
      <td>a</td>
      <td> </td>
    </tr>
    <tr>
      <td>5</td>
      <td> </td>
      <td> </td>
      <td>a</td>
      <td> </td>
    </tr>
    <tr>
      <td>6</td>
      <td> </td>
      <td> </td>
      <td>b</td>
      <td> </td>
    </tr>
    <tr>
      <td>7</td>
      <td> </td>
      <td> </td>
      <td>b</td>
      <td> </td>
    </tr>
    <tr>
      <td>8</td>
      <td>4</td>
      <td>cadabra$</td>
      <td>c</td>
      <td>L</td>
    </tr>
    <tr>
      <td>9</td>
      <td>6</td>
      <td>dabra$</td>
      <td>d</td>
      <td>L</td>
    </tr>
    <tr>
      <td>10</td>
      <td>9</td>
      <td>ra$</td>
      <td>r</td>
      <td>L</td>
    </tr>
    <tr>
      <td>11</td>
      <td>2</td>
      <td>racadabra$</td>
      <td>r</td>
      <td>L</td>
    </tr>
  </tbody>
</table>

<p>ここからS-type接尾辞を埋めていく。手順はさきほどの逆を行えばよい。すなわち表を下から上に見ていって、いま見ている接尾辞\(S_i\)の1つ前の\(S_{i-1}\)がS-typeなら、その接尾辞をバケットの空いている後ろから詰めていく。</p>

<p>実際に見てみよう。最初はindex 11からスタートする。この接尾辞\(S_2\)の1つ前の接尾辞\(S_1\)は”bracadabra$”なのでバケット”b”の最後尾index 7に入れる。index 10の接尾辞\(S_9\)の1つ前\(S_8\)は”bra$”なので、バケット”b”の空いている最後尾index 6に入れる。</p>

<table>
  <thead>
    <tr>
      <th>index</th>
      <th>pos</th>
      <th>suffix</th>
      <th>bucket</th>
      <th>type</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td> </td>
      <td> </td>
      <td>$</td>
      <td> </td>
    </tr>
    <tr>
      <td>1</td>
      <td>10</td>
      <td>a$</td>
      <td>a</td>
      <td>L</td>
    </tr>
    <tr>
      <td>2</td>
      <td> </td>
      <td> </td>
      <td>a</td>
      <td> </td>
    </tr>
    <tr>
      <td>3</td>
      <td> </td>
      <td> </td>
      <td>a</td>
      <td> </td>
    </tr>
    <tr>
      <td>4</td>
      <td> </td>
      <td> </td>
      <td>a</td>
      <td> </td>
    </tr>
    <tr>
      <td>5</td>
      <td> </td>
      <td> </td>
      <td>a</td>
      <td> </td>
    </tr>
    <tr>
      <td>6</td>
      <td>8</td>
      <td>bra$</td>
      <td>b</td>
      <td>S</td>
    </tr>
    <tr>
      <td>7</td>
      <td>1</td>
      <td>bracadabra$</td>
      <td>b</td>
      <td>S</td>
    </tr>
    <tr>
      <td>8</td>
      <td>4</td>
      <td>cadabra$</td>
      <td>c</td>
      <td>L</td>
    </tr>
    <tr>
      <td>9</td>
      <td>6</td>
      <td>dabra$</td>
      <td>d</td>
      <td>L</td>
    </tr>
    <tr>
      <td>10</td>
      <td>9</td>
      <td>ra$</td>
      <td>r</td>
      <td>L</td>
    </tr>
    <tr>
      <td>11</td>
      <td>2</td>
      <td>racadabra$</td>
      <td>r</td>
      <td>L</td>
    </tr>
  </tbody>
</table>

<p>これを繰り返していくと、接尾辞”$”、ここでは\(S_{11}\)以外のS-type接尾辞が埋まる。”$”については常にindex 0に入ることが分かっているので、最後にindex 0に\(S_{11}\)を入れると、求める接尾辞配列が得られる。</p>

<h3 id="lmsのソート">LMSのソート</h3>

<p>ここまでで、「LMSのソート」以外はすべて説明した。残るはLMSのソートだけである。</p>

<p>まず「LMS-substring」という概念（『高速文字列解析の世界』では「\(S^*\)部分文字列」と呼んでいる）を導入する。これは元の文字列においてLMSになる接尾辞間の部分文字列を表す。”abracadabra”の場合、LMSは\(S_3\)、\(S_5\)、\(S_7\)、\(S_{11}\)の4つになる。したがってLMS-substringは3-5間の”aca”、5-7間の”ada”、7-11間の”abra$”になる。さらに”$”も便宜的にLMS-substringとして付け加えて、4つがLMS-substringになる。</p>

<p>これをソートして0始まりで数字を振る。もし同じLMS-substringがある場合は同じ数字を振る必要がある。</p>
<ul>
  <li>0: $</li>
  <li>1: abra$</li>
  <li>2: aca</li>
  <li>3: ada</li>
</ul>

<p>これを出現順に並べて、その接尾辞配列を計算する。この例の場合、”2310”の接尾辞配列なので、つぎのようになる。</p>

<table>
  <thead>
    <tr>
      <th>index</th>
      <th>pos</th>
      <th>suffix</th>
      <th>LMS-substring</th>
      <th>LMS</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>3</td>
      <td>0</td>
      <td>$</td>
      <td>\(S_{11}\)</td>
    </tr>
    <tr>
      <td>1</td>
      <td>2</td>
      <td>10</td>
      <td>abra$$</td>
      <td>\(S_7\)</td>
    </tr>
    <tr>
      <td>2</td>
      <td>0</td>
      <td>2310</td>
      <td>acaadaabra$$</td>
      <td>\(S_3\)</td>
    </tr>
    <tr>
      <td>3</td>
      <td>1</td>
      <td>310</td>
      <td>adaabra$$</td>
      <td>\(S_5\)</td>
    </tr>
  </tbody>
</table>

<p>これを見るとわかるが、この接尾辞配列に対応するLMSの並びがソート済みのLMSになっている。実際、LMS-substringの大小関係とこの数字の大小関係は一致しているため、この結果は当然といえる。</p>

<p>ここまでで、LMS-substringをソートさえすれば、LMSのソートができることがわかった。残るはLMS-substringのソートである。</p>

<h4 id="lms-substringのソート">LMS-substringのソート</h4>

<p>ここでLMSを接尾辞配列の表に入れるときにきちんとソートせずに入れて、それからinduced sortingしたらどうなるか考えてみよう。具体的には、LMSを各バケットの下側に入れるが、その入れる順序は適当に行う。たとえば接尾辞配列の表はつぎのようになる。</p>

<table>
  <thead>
    <tr>
      <th>index</th>
      <th>pos</th>
      <th>suffix</th>
      <th>bucket</th>
      <th>type</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>11</td>
      <td><strong>$</strong></td>
      <td>$</td>
      <td>S*</td>
    </tr>
    <tr>
      <td>1</td>
      <td> </td>
      <td> </td>
      <td>a</td>
      <td> </td>
    </tr>
    <tr>
      <td>2</td>
      <td> </td>
      <td> </td>
      <td>a</td>
      <td> </td>
    </tr>
    <tr>
      <td>3</td>
      <td>7</td>
      <td><strong>a</strong>bra$</td>
      <td>a</td>
      <td>S*</td>
    </tr>
    <tr>
      <td>4</td>
      <td>5</td>
      <td><strong>a</strong>dabra$</td>
      <td>a</td>
      <td>S*</td>
    </tr>
    <tr>
      <td>5</td>
      <td>3</td>
      <td><strong>a</strong>cadabra$</td>
      <td>a</td>
      <td>S*</td>
    </tr>
    <tr>
      <td>6</td>
      <td> </td>
      <td> </td>
      <td>b</td>
      <td> </td>
    </tr>
    <tr>
      <td>7</td>
      <td> </td>
      <td> </td>
      <td>b</td>
      <td> </td>
    </tr>
    <tr>
      <td>8</td>
      <td> </td>
      <td> </td>
      <td>c</td>
      <td> </td>
    </tr>
    <tr>
      <td>9</td>
      <td> </td>
      <td> </td>
      <td>d</td>
      <td> </td>
    </tr>
    <tr>
      <td>10</td>
      <td> </td>
      <td> </td>
      <td>r</td>
      <td> </td>
    </tr>
    <tr>
      <td>11</td>
      <td> </td>
      <td> </td>
      <td>r</td>
      <td> </td>
    </tr>
  </tbody>
</table>

<p>ここで、index 3、4、5のLMSの並びはソートされていない。またバケットソートによって1文字目はソートされているので、それを表すために1文字目だけ太字にしてある。</p>

<p>この状態でL-typeおよびS-typeの接尾辞をinduced sortingで表に入れてみよう。結果はつぎのようになる。同じようにソートされている文字は太字にしてある。</p>

<table>
  <thead>
    <tr>
      <th>index</th>
      <th>pos</th>
      <th>suffix</th>
      <th>bucket</th>
      <th>type</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>11</td>
      <td><strong>$</strong></td>
      <td>$</td>
      <td>S*</td>
    </tr>
    <tr>
      <td>1</td>
      <td>10</td>
      <td><strong>a$</strong></td>
      <td>a</td>
      <td>L</td>
    </tr>
    <tr>
      <td>2</td>
      <td>7</td>
      <td><strong>abra$</strong></td>
      <td>a</td>
      <td>S*</td>
    </tr>
    <tr>
      <td>3</td>
      <td>0</td>
      <td><strong>abra</strong>cadabra$</td>
      <td>a</td>
      <td>S</td>
    </tr>
    <tr>
      <td>4</td>
      <td>3</td>
      <td><strong>aca</strong>dabra$</td>
      <td>a</td>
      <td>S*</td>
    </tr>
    <tr>
      <td>5</td>
      <td>5</td>
      <td><strong>ada</strong>bra$</td>
      <td>a</td>
      <td>S*</td>
    </tr>
    <tr>
      <td>6</td>
      <td>8</td>
      <td><strong>bra$</strong></td>
      <td>b</td>
      <td>S</td>
    </tr>
    <tr>
      <td>7</td>
      <td>1</td>
      <td><strong>bra</strong>cadabra$</td>
      <td>b</td>
      <td>S</td>
    </tr>
    <tr>
      <td>8</td>
      <td>4</td>
      <td><strong>ca</strong>dabra$</td>
      <td>c</td>
      <td>L</td>
    </tr>
    <tr>
      <td>9</td>
      <td>6</td>
      <td><strong>da</strong>bra$</td>
      <td>d</td>
      <td>L</td>
    </tr>
    <tr>
      <td>10</td>
      <td>9</td>
      <td><strong>ra$</strong></td>
      <td>r</td>
      <td>L</td>
    </tr>
    <tr>
      <td>11</td>
      <td>2</td>
      <td><strong>ra</strong>cadabra$</td>
      <td>r</td>
      <td>L</td>
    </tr>
  </tbody>
</table>

<p>LMSのあとにL-typeの接尾辞を入れたときにソート済みの部分が伸び、さらにS-typeの接尾辞を入れるときに伸びる。LMSのみに注目すると、ちょうどLMS-substringに相当する部分、つまり”$”、”abra$”、”aca”、”ada”が太字になっている。このことから、LMSをバケットの後ろに入れたあとにinduced sortingしてLMSに注目すると、ソート済みのLMS-substringが得られることがわかる。</p>

<p>以上をまとめると、LMSをソートするためにはつぎのようにすればよい。</p>
<ol>
  <li>接尾辞配列の表にLMSを入れる。このとき、LMSはソートされていなくてよい。</li>
  <li>induced sortingをしてL-typeとS-typeを表に埋める</li>
  <li>表におけるLMSの順序に応じてLMS-substringに数字を振っていく。同じ部分文字列には同じ数字を振ること。</li>
  <li>元の文字列におけるLMSの出現順に対応する数字を並べかえて、その数字を連結した文字列の接尾辞配列を求める</li>
  <li>接尾辞配列の各要素に対応するLMSの並びがソート済みLMSになる</li>
</ol>

<p>この#3で、LMS-substring同士の比較が発生するが、比較する文字の最大合計数は\(n\)なので、ぜんたいで\(O(n)\)で実行できる。また、#4で再起が発生するが、このときの接尾辞配列のサイズは半分になっている。したがって、全体のアルゴリズムの実行時間が\(O(n)\)なら、再起を含めても\(O(n) + O(n/2) + O(n/4) + \cdots O(1) = O(n)\)で収まる。</p>

<h3 id="実装上の注意点">実装上の注意点</h3>

<p>いくつか実装上で注意しておくとよいこと。</p>

<h4 id="文字は置換してよい">文字は置換してよい</h4>
<p>与えられた文字列内における文字の大小関係が維持されていれば、好きな文字に置換してしまってよい。たとえば”fox”を”abc”に置換してよい。もっといえば、整数配列の<code class="language-plaintext highlighter-rouge">[1, 2, 3]</code>に置換してしまった方がよいだろう。こうすると、LMSのソートでLMS番号の接尾辞配列を求めるときに同じ関数が使える。</p>

<p>また、この置換のときに番兵文字を考慮しておけば、番兵文字が実際の文字と被ることもなくて便利である。</p>

<h4 id="lms-substringを保持する必要はない">LMS-substringを保持する必要はない</h4>
<p>アルゴリズムを見ればわかるが、LMS-substringそれ自体を覚えておく必要はない。最終的に、対応するLMSの接尾辞配列内における順番が分かればよい。ただし、ソートされたLMS-substringに番号を振るときに隣りあうLMS-substringが一致するか調べる必要はある。</p>

<h3 id="おまけlms-substringが重複するときのlmsソート">（おまけ）LMS-substringが重複するときのLMSソート</h3>
<p>この記事で利用した”abracadabra”はよくあるサンプルであるが、LMS-substringは相異なるので、すこし分かりづらい。実際、LMSソート用の接尾辞配列をつくると、それが実は最終的に求める接尾辞配列になっているので、なんでこんなややこしいことをしないといけないのかよく分からなくなる。</p>

<p>そこで”babababb”という文字列を考えよう。これのLMS-substringは”aba”, “aba”, “abb$”, “$”の4つ、3種類になる（”aba”が2つあることに注意）。LMS-substringのソートをするための接尾辞配列はつぎのようになる。</p>

<table>
  <thead>
    <tr>
      <th>index</th>
      <th>pos</th>
      <th>suffix</th>
      <th>bucket</th>
      <th>type</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>8</td>
      <td><strong>$</strong></td>
      <td>$</td>
      <td>S*</td>
    </tr>
    <tr>
      <td>1</td>
      <td>3</td>
      <td><strong>aba</strong>bb$</td>
      <td>a</td>
      <td>S*</td>
    </tr>
    <tr>
      <td>2</td>
      <td>1</td>
      <td><strong>aba</strong>babb$</td>
      <td>a</td>
      <td>S*</td>
    </tr>
    <tr>
      <td>3</td>
      <td>5</td>
      <td><strong>abb$</strong></td>
      <td>a</td>
      <td>S*</td>
    </tr>
    <tr>
      <td>4</td>
      <td>7</td>
      <td><strong>b$</strong></td>
      <td>b</td>
      <td>L</td>
    </tr>
    <tr>
      <td>5</td>
      <td>4</td>
      <td><strong>ba</strong>bb$</td>
      <td>b</td>
      <td>L</td>
    </tr>
    <tr>
      <td>6</td>
      <td>2</td>
      <td><strong>ba</strong>babb$</td>
      <td>b</td>
      <td>L</td>
    </tr>
    <tr>
      <td>7</td>
      <td>0</td>
      <td><strong>ba</strong>bababb$</td>
      <td>b</td>
      <td>L</td>
    </tr>
    <tr>
      <td>8</td>
      <td>6</td>
      <td><strong>bb$</strong></td>
      <td>b</td>
      <td>L</td>
    </tr>
  </tbody>
</table>

<p>見てわかるように、index 1とindex 2は位置が逆だし、最終形の接尾辞配列とは異なっている。しかし、LMS-substringのソートという観点では、\(S_8\)、\(S_3\)、\(S_1\)、\(S_5\)と期待する並びになっている。</p>

<p>したがってLMS-substringそれぞれに<code class="language-plaintext highlighter-rouge">0</code>、<code class="language-plaintext highlighter-rouge">1</code>、<code class="language-plaintext highlighter-rouge">2</code>と番号を振ると、出現順は”1120”になり、これの接尾辞配列は<code class="language-plaintext highlighter-rouge">["0", "1120", "120", "20"]</code>なので、LMSの並び順は\(S_8\)、\(S_1\)、\(S_3\)、\(S_5\)と期待どおりになる。</p>

<h2 id="参考文献">参考文献</h2>
<ul>
  <li><a href="https://www.iwanami.co.jp/book/b257894.html">『高速文字列解析の世界』</a></li>
  <li><a href="https://shogo82148.github.io/homepage/memo/algorithm/suffix-array/sa-is.html#">SA-IS - Shogo Computing Laboratory</a></li>
  <li><a href="https://ieeexplore.ieee.org/abstract/document/5582081">Two Efficient Algorithms for Linear Time Suffix Array Construction, G Nong, et.al.</a></li>
</ul>]]></content><author><name></name></author><summary type="html"><![CDATA[『高速文字列解析の世界』を読んでいたら、Suffix Arrayを線形時間で構築するアルゴリズムとしてInduced Sorting（IS）を用いたものが紹介されていた。文章で読んだだけでは、いまいち十分に理解できた気分にならなかったので、自分で実装してみた。これはそのSA-ISアルゴリズムの解説である。]]></summary></entry><entry><title type="html">Ubuntu 24.10で日本語句読点が中央に表示される問題とその修正</title><link href="https://hydrakecat.net/ubuntu/2025/08/09/cjk-punctuation-font-on-ubuntu.html" rel="alternate" type="text/html" title="Ubuntu 24.10で日本語句読点が中央に表示される問題とその修正" /><published>2025-08-09T15:00:00+00:00</published><updated>2025-08-09T15:00:00+00:00</updated><id>https://hydrakecat.net/ubuntu/2025/08/09/cjk-punctuation-font-on-ubuntu</id><content type="html" xml:base="https://hydrakecat.net/ubuntu/2025/08/09/cjk-punctuation-font-on-ubuntu.html"><![CDATA[<p>Ubuntu 24.10で、日本語の読点や句点がつぎのように中央に表示されることがある。どうやら、まわりを日本語以外で囲まれているときに発生するようだ。</p>

<p><img src="/assets/images/wrong_cjk_punctuation_img01.png" alt="A Japanese punctuation is rendered with a wrong font" /></p>

<p>これはテキストエディタ（gnome-text-editor）で自分で入力しても発生するし、ブラウザでこのような文字列を表示するときも発生する。</p>

<p>この問題の修正方法と原因（と思われるもの）を説明する。</p>

<!-- more -->
<h2 id="環境">環境</h2>
<ul>
  <li>Ubuntu 24.10</li>
  <li>fontconfig 2.15.0-2.2</li>
  <li>language-selector-common 0.227</li>
</ul>

<h2 id="再現手順">再現手順</h2>
<ol>
  <li>Ubuntuにログインする</li>
  <li>テキストエディタ（gnome-text-editor）を起動する</li>
  <li>一行目に<code class="language-plaintext highlighter-rouge">abc、def</code>（読点は<code class="language-plaintext highlighter-rouge">U+3001</code>）を入力する</li>
  <li>二行目に<code class="language-plaintext highlighter-rouge">あいう、えおか</code>（読点は<code class="language-plaintext highlighter-rouge">U+3001</code>）を入力する</li>
</ol>

<h2 id="現象">現象</h2>
<p>さきほどのスクリーンショットのように読点（<code class="language-plaintext highlighter-rouge">U+3001</code>）の字形（glyph）が一行目と二行目で異なるものになる。</p>

<h2 id="修正方法">修正方法</h2>
<ol>
  <li><code class="language-plaintext highlighter-rouge">$HOME/.config/fontconfig/fonts.conf</code>に以下のように記述する（ファイルがなければ作成する）
    <div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0"?&gt;</span>
<span class="cp">&lt;!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd"&gt;</span>
<span class="c">&lt;!--
  $XDG_CONFIG_HOME/fontconfig/fonts.conf for per-user font configuration
--&gt;</span>
<span class="nt">&lt;fontconfig&gt;</span>
  <span class="nt">&lt;match&gt;</span>
    <span class="nt">&lt;test</span> <span class="na">name=</span><span class="s">"lang"</span> <span class="na">compare=</span><span class="s">"contains"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;string&gt;</span>en<span class="nt">&lt;/string&gt;</span>
    <span class="nt">&lt;/test&gt;</span>
    <span class="nt">&lt;test</span> <span class="na">name=</span><span class="s">"family"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;string&gt;</span>sans-serif<span class="nt">&lt;/string&gt;</span>
    <span class="nt">&lt;/test&gt;</span>
    <span class="nt">&lt;edit</span> <span class="na">name=</span><span class="s">"family"</span> <span class="na">mode=</span><span class="s">"prepend"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;string&gt;</span>Noto Sans<span class="nt">&lt;/string&gt;</span>
    <span class="nt">&lt;/edit&gt;</span>
    <span class="c">&lt;!-- This entry is necessary to avoid a wrong font is used for a colon in the system clock display. --&gt;</span>
    <span class="nt">&lt;edit</span> <span class="na">name=</span><span class="s">"family"</span> <span class="na">mode=</span><span class="s">"prepend"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;string&gt;</span>Noto Sans Math<span class="nt">&lt;/string&gt;</span>
    <span class="nt">&lt;/edit&gt;</span>
    <span class="nt">&lt;edit</span> <span class="na">name=</span><span class="s">"family"</span> <span class="na">mode=</span><span class="s">"prepend"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;string&gt;</span>Noto Sans CJK JP<span class="nt">&lt;/string&gt;</span>
    <span class="nt">&lt;/edit&gt;</span>
  <span class="nt">&lt;/match&gt;</span>
<span class="nt">&lt;/fontconfig&gt;</span>
</code></pre></div>    </div>
  </li>
  <li>さきほどの再現手順のステップ#2 - #4を行い、直っていることを確認する</li>
  <li>Ubuntuに再ログインして、問題がなくなっていることを確認する</li>
</ol>

<h2 id="原因と思われるもの">原因（と思われるもの）</h2>
<p>読点（<code class="language-plaintext highlighter-rouge">U+3001</code>）の字形はフォントによって異なる。この文字コードはUnicodeの108個目の「CJKの記号及び約物」というブロックに収録されている。名前のとおり、CJKで統合されているので、このユニコードだけを見て、どの言語（文字）の字形を使うべきかは判断できない。</p>

<p>どうやら最近のUbuntu（23.10以降？）ではテキストの言語を判定してそれに合わせてNoto Sansのフォントが選ばれるらしい。読点（<code class="language-plaintext highlighter-rouge">U+3001</code>）の周囲が日本語なら、その文字は、日本語のフォント、たとえば”Noto Sans CJK JP”でレンダリングされる。一方、読点の周囲がアルファベットだった場合、そのテキストの言語は英語と判断される。このとき英語のフォント（たとえば”Ubuntu Sans”）は<code class="language-plaintext highlighter-rouge">U+3001</code>に対応する字形を持っていない可能性が高く、他のフォントにフォールバックされる。</p>

<p>Ubuntu 24.10では、このフォールバックが発生し、このフォールバック順序は、どうやら<code class="language-plaintext highlighter-rouge">/etc/fonts/conf.d/56-language-selector-prefer.conf</code>で指定されているらしい。このファイルでは”Noto Sans Mongolian”が”Noto Sans CJK JP”よりも前にあるので、”Noto Sans Mongolian”が使われる。実際に”Noto Sans Mongolian”の<code class="language-plaintext highlighter-rouge">U+3001</code>の字形はスクリーンショットで見たようなものになる（<a href="https://www.unicode.org/L2/L2019/19132-mwg3-10-mong-punct-marks-r2.pdf">参照：On Mongolian punctuation marks</a>）。</p>

<p>では、どうやって修正するか？手っ取り早いのは、フォントの優先順を変更して、好みの<code class="language-plaintext highlighter-rouge">U+3001</code>の字形を含むフォントを前に持ってくることだ。実際に、<a href="https://github.com/canonical/Ubuntu-Sans-fonts/issues/117#issuecomment-2439368846">GitHubのissueについたコメント</a>では<code class="language-plaintext highlighter-rouge">/etc/fonts/local.conf</code>を書いて、”Noto Sans”の次に自分の使いたいフォントを指定する方法が紹介されている。ただし、これを行うとつぎのスクリーンショットのように、システムバーの時計の表示がおかしくなり、真ん中のコロンが全角（？）のようになってしまう。</p>

<p><img src="/assets/images/wrong_cjk_punctuation_img02.png" alt="The colon in the middle of the system clock is rendered wrongly" /></p>

<p>これの修正のために<code class="language-plaintext highlighter-rouge">/etc/fonts/conf.d/56-language-selector-prefer.conf</code>を直接書き換える方法が<a href="https://github.com/canonical/Ubuntu-Sans-fonts/issues/117#issuecomment-2472268566">同じissueのコメントで提案されている</a>が、これはこれで、バージョンアップで上書きされる可能性があり、望ましくない。</p>

<p>自分の修正は1つめの修正に近いが、つぎの違いがある。</p>
<ol>
  <li><code class="language-plaintext highlighter-rouge">$HOME/.config/fontconfig</code>以下なので、自分以外のユーザーには影響がない</li>
  <li>“Noto Sans Math”を追加することでシステムバーの時計表示の不具合を回避</li>
  <li>言語を”en”に限定することで、影響を小さめにしている</li>
</ol>

<h2 id="付録">付録</h2>
<p>最後に、調査をする上で便利だったコマンドを紹介する。</p>

<h3 id="fc_debug">FC_DEBUG</h3>
<p>環境変数<code class="language-plaintext highlighter-rouge">FC_DEBUG</code>を指定してアプリケーションを起動すると、標準出力にデバッグ情報が出力される。たとえば、この環境変数つきでgnome-text-editorを起動して読点（<code class="language-plaintext highlighter-rouge">U+3001</code>）を入力するとつぎのように出力され、どのフォントが使われているか分かる。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ FC_DEBUG</span><span class="o">=</span>1 gnome-text-editor
...
Match Pattern has 29 elts <span class="o">(</span>size 32<span class="o">)</span>
	family: <span class="s2">"Noto Sans Mongolian"</span><span class="o">(</span>s<span class="o">)</span> <span class="s2">"Noto Sans"</span><span class="o">(</span>w<span class="o">)</span> <span class="s2">"Noto Sans Adlam"</span><span class="o">(</span>w<span class="o">)</span> ...
	familylang: <span class="s2">"en"</span><span class="o">(</span>s<span class="o">)</span> <span class="s2">"en-us"</span><span class="o">(</span>w<span class="o">)</span>
	stylelang: <span class="s2">"en"</span><span class="o">(</span>s<span class="o">)</span> <span class="s2">"en-us"</span><span class="o">(</span>w<span class="o">)</span>
	fullnamelang: <span class="s2">"en"</span><span class="o">(</span>s<span class="o">)</span> <span class="s2">"en-us"</span><span class="o">(</span>w<span class="o">)</span>
	...
</code></pre></div></div>

<h3 id="fc-match">fc-match</h3>
<p><code class="language-plaintext highlighter-rouge">fc-match</code>というコマンドを使うと、特定のフォントファミリーや特定の言語のときに使われるフォントの優先順位を見ることができる。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>fc-match <span class="nt">-s</span> sans-serif:en
NotoSans-Regular.ttf: <span class="s2">"Noto Sans"</span> <span class="s2">"Regular"</span>
NotoSansAdlam-Regular.ttf: <span class="s2">"Noto Sans Adlam"</span> <span class="s2">"Regular"</span>
NotoSansArabic-Regular.ttf: <span class="s2">"Noto Sans Arabic"</span> <span class="s2">"Regular"</span>
NotoSansArmenian-Regular.ttf: <span class="s2">"Noto Sans Armenian"</span> <span class="s2">"Regular"</span>
...
</code></pre></div></div>
<p>さきに書いた修正方法を適用したあとはつぎのようになる。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>NotoSans-Regular.ttf: <span class="s2">"Noto Sans"</span> <span class="s2">"Regular"</span>
NotoSansMath-Regular.ttf: <span class="s2">"Noto Sans Math"</span> <span class="s2">"Regular"</span>
NotoSansCJK-Regular.ttc: <span class="s2">"Noto Sans CJK JP"</span> <span class="s2">"Regular"</span>
NotoSansAdlam-Regular.ttf: <span class="s2">"Noto Sans Adlam"</span> <span class="s2">"Regular"</span>
...
</code></pre></div></div>]]></content><author><name></name></author><category term="ubuntu" /><summary type="html"><![CDATA[Ubuntu 24.10で、日本語の読点や句点がつぎのように中央に表示されることがある。どうやら、まわりを日本語以外で囲まれているときに発生するようだ。]]></summary></entry><entry><title type="html">浮動小数点数の文字列化アルゴリズムSchubfach</title><link href="https://hydrakecat.net/2023/08/12/schubfatch.html" rel="alternate" type="text/html" title="浮動小数点数の文字列化アルゴリズムSchubfach" /><published>2023-08-12T07:00:00+00:00</published><updated>2023-08-12T07:00:00+00:00</updated><id>https://hydrakecat.net/2023/08/12/schubfatch</id><content type="html" xml:base="https://hydrakecat.net/2023/08/12/schubfatch.html"><![CDATA[<h2 id="tldr">TL;DR</h2>
<p>C++20の<a href="https://en.cppreference.com/w/cpp/utility/format/format"><code class="language-plaintext highlighter-rouge">std::format</code></a>やJavaの<code class="language-plaintext highlighter-rouge">Double#toString()</code>のもとにもなっている<sup id="fnref:cpp_std_fmt" role="doc-noteref"><a href="#fn:cpp_std_fmt" class="footnote" rel="footnote">1</a></sup>浮動小数点数の文字列化アルゴリズム<a href="https://drive.google.com/file/d/1luHhyQF9zKlM8yJ1nebU0OgVYhfC6CBN/view?pli=1">Schubfachアルゴリズムの論文</a>を読んだ。</p>

<p>この記事ではそのアルゴリズムを解説するが厳密な定義やエッジケースは論文をあたってほしい。</p>

<h2 id="問題とその背景">問題とその背景</h2>
<p>浮動小数点数の文字列化がなぜ問題になるかというと、IEEE 754の浮動小数点数を10進数の小数点数に変換したときに人間にとって読みやすい表記が自明ではないからである。これは10進数の有限桁の小数点数が有限桁の2進数で表せる保証がないことが一因となっている。</p>

<p>よくある例を考えよう。10進数の<code class="language-plaintext highlighter-rouge">1.2</code>を2進数表記するとつぎのような循環小数になる<sup id="fnref:inf_binary" role="doc-noteref"><a href="#fn:inf_binary" class="footnote" rel="footnote">2</a></sup>。</p>

\[1.00110011001100110011001100110011001100110011001100110...\]

<p>これをIEEE 754の倍精度浮動小数点数で表そうとすると、仮数部が52ビットなので、小数点以下53ビット以下は切り捨てる（より正確には最近接偶数丸め）。したがって、人間が端末にむかって<code class="language-plaintext highlighter-rouge">1.2</code>と入力すると、多くの場合はそれが上記のビット列として表され仮数部52ビットの倍精度浮動小数点数として記憶領域に格納される。さて、これを再び同じ人間に10進数で表示するときにどうすればいいだろうか。</p>

<p>困るのは、倍精度浮動小数点数に変換したときに、この格納されている値になり得る10進数の小数点数が <code class="language-plaintext highlighter-rouge">1.2</code> を含めて複数あるということである。実際、この倍精度浮動小数点数をそのまま10進数に戻すと以下のようになる。</p>

\[1.1999999999999999555910790149937383830547332763671875\]

<p>これをそのまま表示するのも一案ではあるが、入力した人間からすると<code class="language-plaintext highlighter-rouge">1.2</code>が上記のような数値で表示されると、これは長すぎて困惑することだろう。一方コンピュータからすると、この値も<code class="language-plaintext highlighter-rouge">1.2</code>もあるいは<code class="language-plaintext highlighter-rouge">1.20000000000000001</code>も倍精度浮動小数点数にしてしまえばすべて内部的には同じ表現になってしまい、どれを表示するのが適切か不明である。</p>

<p>ということで、任意の浮動小数点数を人間に読みやすい10進数文字列として表示するときにどのようにフォーマットするのが適切か、というのが問題となる<sup id="fnref:decimal" role="doc-noteref"><a href="#fn:decimal" class="footnote" rel="footnote">3</a></sup>。</p>

<h3 id="ナイーブな解法とその欠陥">ナイーブな解法とその欠陥</h3>
<p>とてもナイーブな解法は小数点以下何桁かで打ち切って表示するというものである。上の例でいえばたとえば小数点以下5桁で四捨五入すれば、<code class="language-plaintext highlighter-rouge">1.2</code>になる。表示するときにアプリケーションで小数点以下何桁まで表示したいかを選び、あとは四捨五入するなり切り捨てるなりして表示するというのが1つの解法ではあるし、実際いくつかのアプリケーションではそれを行っている。</p>

<p>ただし、この解法には重大な欠陥がある。というのは、そうやって表示した10進数をふたたびIEEE 754の倍精度浮動小数点数に変換したときに同じバイナリ表記になる保証がない、ということである。たとえば、<code class="language-plaintext highlighter-rouge">1.2000000000000001</code>は小数点以下5桁で四捨五入すると<code class="language-plaintext highlighter-rouge">1.2</code>になってしまうが、この値を2進数表記すると <code class="language-plaintext highlighter-rouge">1.0011...00110100</code> となり、これは <code class="language-plaintext highlighter-rouge">1.2</code> を倍精度浮動小数点数にしたものと異なる値である。</p>

<p>このように、浮動小数点数→文字列→浮動小数点数と変換したときに、同じ値になっていてほしいという要求をround tripと呼ぶらしい<sup id="fnref:round_trip" role="doc-noteref"><a href="#fn:round_trip" class="footnote" rel="footnote">4</a></sup>。ちなみに、このround tripを保証するために小数点以下何桁まで表示すべきか、という問題はすでに1968年に解かれていて、小数点以下17桁まで表示すればround tripは保証される<sup id="fnref:17digits" role="doc-noteref"><a href="#fn:17digits" class="footnote" rel="footnote">5</a></sup>。</p>

<p>ただし、ほとんどの人間はround tripが保証されている中でもっとも桁数が小さいものを読みやすいと感じるだろう。たとえば<code class="language-plaintext highlighter-rouge">1.20000000000000001</code>も<code class="language-plaintext highlighter-rouge">1.2</code>も同じ倍精度浮動小数点数になるなら<code class="language-plaintext highlighter-rouge">1.2</code>を読みやすいと感じるはずだ<sup id="fnref:shorter_better" role="doc-noteref"><a href="#fn:shorter_better" class="footnote" rel="footnote">6</a></sup>。</p>

<h2 id="問題の定式化">問題の定式化</h2>

<p>以上を踏まえて、浮動小数点数の文字列化の問題をつぎのように定式化する。</p>

<p>有限な任意の浮動小数点数表記（以降\(fp\)とする）で\(v\)と表される正の数が与えられたとき（負の場合は同じことをやればいいので簡単のために正とする）に\(D\)進数で表記された\(d_v\)をつぎのように定義する。</p>
<ul>
  <li>\(round\)関数を実数から\(fp\)へ変換する関数とする</li>
  <li>\(R = \{ x \mid round(x) = v \}\)、つまり上記の変換によって\(v\)になる値の集合とする</li>
  <li>\(m = \min\{len(x) \mid x \in R \}\)は集合\(R\)の各値を\(D\)進表記したときの長さ（後述）の最小値</li>
  <li>\(T = \{x \in R \mid len(x) = m\}\)、つまり集合\(R\)のうち\(D\)進表記した長さが\(m\)の値の集合</li>
  <li>\(d_v\)を集合\(T\)のうちもっとも\(v\)に近いものと定義する、ただし、そのような\(v\)が2つあった場合は\(d_v\)は偶数のものとする</li>
</ul>

<p>なお\(fp\)はIEEE 754の単精度浮動小数点数表記や倍精度浮動小数点数表記と考えてもらって構わない。また\(D\)も一般には\(10\)であるが、ここではより一般化した形式を考える。</p>

<p>また、\(len\)関数で表される長さであるが、これはざっくり言えば\(D\)進数で表された値の先頭に連続する0と末尾に連続する0を取り除いた桁数ということになる。たとえば\(12000\)の長さは\(2\)だし、\(0.450\)の長さも\(2\)、\(1.23\)の長さは\(3\)となる。科学表記にしたときの仮数部の長さといってもよい。論文ではより厳密に定義しているが、アルゴリズムを理解するにはこれくらいで十分だと思う。</p>

<h2 id="問題の定式化修正版">問題の定式化（修正版）</h2>

<p>これはどれくらい一般的なのか知らないが、論文の著者によれば、すくなくともJavaの<code class="language-plaintext highlighter-rouge">Double#toString()</code>メソッドの仕様では小さな値に<code class="language-plaintext highlighter-rouge">1.0E-21</code>のような科学表記がつかわれる。これがなにを意味するかというと、長さは最低でも\(2\)なわけであるから、前節の定式化において\(m=1\)の場合は、\(m=2\)となる値も候補に入れてもっとも\(v\)に近い値を選ぶべき、というのである。</p>

<p>たとえば\(v = 20 \cdot 2^{-1074} = 9.88\ldots\cdot10^{-323}\)であるが、前節の定式化にしたがうと集合\(T\)は</p>

\[\begin{align}
R &amp;= \{1\cdot10^{-322}, 97\cdot10^{-324}, 98\cdot10^{-324}, 99\cdot10^{-324}, \ldots\} \\
m &amp;= 1 \\
T &amp;= \{1\cdot10^{-322}\}
\end{align}\]

<p>となり、\(d_v = 1\cdot10^{-322}\)となる。これは科学表記だと<code class="language-plaintext highlighter-rouge">1.0E-322</code>となり有効数字2桁も使っていてもったいない、というわけだ。この場合は<code class="language-plaintext highlighter-rouge">9.9E-323</code>の方がより真実の値に近いので相応しいと著者は主張している。</p>

<p>個人的には細かすぎる気もするのだけれど、ともあれ、この新たな要求を言葉にすると、\(M\)を正の整数としたときに\(d_v\)はその長さが\(M\)を超えない限りにおいて\(v\)に近いものを選ぶ、となる。前節の定式化は\(M=1\)としたもので、Javaの<code class="language-plaintext highlighter-rouge">Double#toString()</code>のような挙動にしたければ\(M=2\)とすればよいことになる。</p>

<p>修正版の問題の定式化はつぎのようになる。</p>

<p>有限な\(fp\)で\(v\)と表される正の数、および\(M\ge1\)が与えられたときに\(D\)進数で表される\(d_v\)をつぎのように定義する。</p>
<ul>
  <li>\(round\)関数を実数から\(fp\)へ変換する関数とする</li>
  <li>\(R = \{ x \mid round(x) = v \}\)、つまり上記の変換によって\(v\)になる値の集合とする</li>
  <li>\(m = \min\{len(x) \mid x \in R \}\)は集合\(R\)の各値を\(D\)進表記したときの長さの最小値</li>
  <li>もし\(m \ge M\)なら、\(T = \{x \in R \mid len(x) = m\}\)すなわち集合\(R\)のうち長さが\(m\)の値の集合とする。そうでない場合は\(T = \{x \in R \mid len(x) \le M\}\)とする</li>
  <li>\(d_v\)を集合\(T\)のうちもっとも\(v\)に近いものと定義する、ただし、そのような\(v\)が2つあった場合は\(d_v\)は最終桁の値が偶数のものとする</li>
</ul>

<h2 id="アルゴリズムの概要">アルゴリズムの概要</h2>
<p>次節以降ではアルゴリズムの詳細を見ていくが、先に大まかなアイディアを説明する。これは原論文には書いていない、自分の解釈なので間違っている可能性があることに留意されたい。</p>

<p>前節の定式化で分かると思うが、まずround tripを保証するには浮動小数点\(v\)が表す区間に収まる範囲で\(d_v\)を選ぶ必要がある。さらにそのなかで一番長さが短いものを知りたい。さて、ここで数直線上に\(10^i, 2\cdot 10^i, 3\cdot 10^i, \ldots\)とまず目盛りをつけ、つぎに\(10^{i-1},2\cdot 10^{i-1},\ldots\)というように、粗いものから徐々に細かくして\(i\)を減らしながら\(10^i\)間隔（\(i\)は整数）の目盛りをつけていく作業を考えよう。この目盛りが細かくなるほど基本的に長さが長くなる（科学表記したときの仮数部の長さが長くなる）。</p>

<p>このときに\(v\)が表す実数区間には最初は目盛りが1つも打たれないが、\(i\)を減らすにしたがって、どこかで目盛りが初めて入るはずである。そしてこの初めて区間に入った目盛りの値が目指す\(d_v\)になる。もちろん初めて入る目盛が複数ということもあり得るがその場合は\(v\)に近い方を手順に従って選べばよい。</p>

<p><img src="/assets/images/Schubfach_img01.png" alt="数直線上にvが代表する区間と10^i間隔の目盛を表示した図" /></p>

<p>さらに、区間に入る目盛りが見つかったらそれ以上は\(i\)を小さくしなくてもいいことに注意してほしい。というのは、図からもわかるように、目盛りを細かくしたときに必ずそれより前の粗い目盛りと重なる（\(0.1\)の目盛りはそのすべてが\(0.01\)の目盛りと重なるし、とうぜん\(1\)の目盛りもすべてがそれらと重なる。\(j \lt i\)について\(10^i\)の目盛りは\(10^j\)の目盛りと重なる）ことと、目盛りが細かいほど値が長くなることから、\(10^i\)の目盛りが\(v\)が代表する区間と重なったらそれが求める\(d_v\)になる。この図でいえば、\(2\cdot 10^i\)がその区間に入っているので\(M=1\)なら文句なしにこれが\(d_v\)になる。一方\(M=2\)なら\(19\cdot 10^{i-1}\)とどちらが\(v\)に近いか調べることになる。</p>

<p>また、この初めて区間に打たれる目盛りはたとえば二分法で探索してもよいが、ちょっとした計算で求まる。というのは\(v\)が表す区間の幅を\(\alpha\)とすれば、\(i = \lceil\log_{10}\alpha\rceil\)のあたりを調べればよいからだ。つまり目盛り幅がちょうど\(\alpha\)になる付近を探せばかならず区間に入る目盛りが見つかるし、その目盛りの数がぎりぎり0になるかならないかになる。</p>

<p>したがって、\(i = \lceil\log_{10}\alpha\rceil\)を計算して、あとは目盛りが区間に入るか調べ、入らなかったら\(i\)を\(1\)小さくしてもう一度調べれば、\(d_v\)が見つかる。</p>

<p>以上がこのSchubfachアルゴリズムの概略である。以降ではより詳細にただし原論文ほどは厳密にせずに解説する。</p>

<h2 id="前準備">前準備</h2>

<p>さて、アルゴリズムの詳細について説明するまえにアルゴリズムが前提とする事実を確認する。</p>

<h3 id="vが代表する区間r_v">\(v\)が代表する区間\(R_v\)</h3>

<p>浮動小数点数（\(fp\)）で表せる値は離散値であり、その値の代表する実数の区間がある。具体的に\(fp\)で表した値\(v = c \cdot 2^q\) とする。ただし \(c\)は正の整数で\(1\ldots\)の形式であるとする<sup id="fnref:c-explained" role="doc-noteref"><a href="#fn:c-explained" class="footnote" rel="footnote">7</a></sup>、\(q\)は整数。このとき\(v\)が表す区間の左端を\(v_l\)、右端を\(v_r\)とする。</p>

<p>ここで\(v_l\)は、\(v\)とその1つ前の\(fp\)で表せる値のちょうど中間点になる。これは数式で表すと\(v_l=c_l2^q\)とすれば</p>

\[c_l =
\begin{cases}
c - 1/4 &amp; \text{もし}v\text{が正規化数の最小値より大きなちょうど2の累乗なら} \\
c - 1/2 &amp; \text{それ以外} \\
\end{cases}\]

<p>となる。この条件分けが発生するのは浮動小数点数を大きくしていったときにちょうど\(q\)が増えるタイミングで左側と右側で分布が異なるからである。</p>

<p>いっぽう \(v_r = c_r2^q\)としたときに\(c_r = c + 1/2\)となる。</p>

<p>以降ではこの\(v\)が代表する区間を\(R_v\)と表す。</p>

<h3 id="集合d_i">集合\(D_i\)</h3>

<p>任意の整数\(i\)について\(D_i\)を\(D_i = \{dD^i\}\)で表される集合とする。ただし\(d\)は任意の自然数。</p>

<p>このとき与えられた\(v\)について、</p>

\[s_i(v) = \lfloor vD^{-i}\rfloor \\
t_i(v) = s_i(v) + 1\]

<p>として、</p>

\[u_i(v) = s_i(v)D^i \\
w_i(v) = t_i(v)D^i\]

<p>とすると、この\(u_i\)と\(w_i\)が集合\(D_i\)の中で\(v\)に最も近い可能性がある2つの値である。</p>

<p>証明については原論文の\(\S\)6を当っていただきたいが、ざっくり説明すると、\(s_i\)は\(v\)を\(D\)進表記して、小数点を\(i\)だけ左に動かし（\(i \lt 0\)なら右）、小数点以下を切り捨てた値であり、\(u_i\)はその\(s_i\)の\(D\)進表記したものの小数点を\(i\)だけ右（\(i \lt 0\)なら左）に動かした値になる。\(w_i\)は\(D_i\)の中で\(u_i\)より1つ大きいものとなる。</p>

<p>具体例を挙げて説明すると、\(v = 123.4\)、\(D = 10\)、\(i = 1\)とすると、\(s_1 = 12\)、\(t_1 = 13\)、\(u_1 = 120\)、\(w_1 = 130\)となる。\(D_1\)の集合は10の倍数で\(120\)と\(130\)以外の値が\(123.4\)により近いことはない、ということが分かるだろう。</p>

<p>さて、この集合\(D_i\)がなんの役に立つかというと、長さに関わってくる。というのは、直感的に\(D_i\)と\(D_{i-1}\)を比較すると後者の方がより細かい刻みを表しているので、前者の方が長さが短くなることが予想される。もうすこし正確に言えば、ある値\(v\)の周辺で\(D_i\)と\(D_{i-1}\)の各要素を比較したら前者の長さが後者のものより長くなることはない。</p>

<p>これも原論文の\(\S\)6で厳密に定式化されているが、ここでは具体例を考えれば十分だろう。たとえば\(D=10\)としたときに\(v = 123.4\)に近い\(D_1\)の要素は\(120\)と\(130\)であった。これらは長さが\(2\)である。一方\(D_0\)で近い可能性がある2つの値は\(123\)と\(124\)で、こちらは長さが\(3\)となる。もちろん\(v = 120.3\)のようにキリがよければ\(D_0\)の\(v\)に近い要素も\(120\)と長さ\(2\)になるが、\(120\)は\(D_1\)の要素でもあるので\(D_1\)の要素より短くなることはない。</p>

<h3 id="r_vとd_iの積">\(R_v\)と\(D_i\)の積</h3>

<p>ここまで説明すると、この浮動小数点数の文字列化の問題はつぎのように言いかえられることが分かる。まずは\(M=1\)とする。</p>

<p>浮動小数点数\(v\)が与えられたときに、その\(v\)が代表する区間\(R_v\)と集合\(D_i = \{d D^i\}\)の積を\(R_i = R_v \cap D_i\)とする<sup id="fnref:intersection-set" role="doc-noteref"><a href="#fn:intersection-set" class="footnote" rel="footnote">8</a></sup>。空集合でない\(R_i\)のうち、\(i\)がもっとも大きなものを求めたい。</p>

<p>\(i\)が最大の空でない\(R_i\)が求まれば、そのなかで\(v\)にもっとも近い値が\(d_v\)になる。\(M=2\)については、\(d_v\)の長さが\(1\)だったときに必要なら\(D_{i-1}\)も考慮に入れればよい。</p>

<p>例として仮数部が2ビットの浮動小数点数を考え\(v = 96.0, R_v = \left[ 88, 104 \right]\)を考えてみよう。\(i = 0, 1, 2, 3\)について\(R_i\)を調べてみるとつぎのようになる。</p>

\[\begin{align}
R_0 &amp;= \{ 88, 89, \ldots, 104 \} \\
R_1 &amp;= \{ 90, 100 \} \\
R_2 &amp;= \{ 100 \} \\
R_3 &amp;= \emptyset \\
\end{align}\]

<p>この例では、\(R_2\)が空集合ではなくかついちばん短い要素を持つので、\(M=1\)の場合は\(d_v = 100\)となる。\(M=2\)の場合は\(R_0\)も調べ、このケースでは\(96\)が一番近いので\(96\)が選ばれる。</p>

<p>さて、この\(R_2\)のような集合を\(v\)に対して求めていきたいわけだが、じつは以下がなりたつ<sup id="fnref:pigeonhole-principle" role="doc-noteref"><a href="#fn:pigeonhole-principle" class="footnote" rel="footnote">9</a></sup>。</p>

<p>\(k\)を\(D^k \ge \left\| R_v \right\| \gt D^{k+1}\)を満たす整数とすると、このような\(k\)は唯一でかつ\(R_k\)はすくなくとも1つの要素を持ち、\(R_{k+1}\)はせいぜい1つの要素を持つ。</p>

<p>したがって、通常は\(R_k\)と\(R_{k+1}\)を調べればよいし、\(M=2\)でかつ\(d_v\)の長さが\(1\)になりそうな場合はそれに加えて\(R_{k-1}\)を調べればよい。さらに、このような\(k\)は\(k = \lfloor \log_D \left\| R_v \right\| \rfloor\)と計算で求められる。</p>

<p>ここで、さきほどの\(v = 96\)の例で\(k\)を計算すると\(\lfloor \log_{10} 16 \rfloor = 1\)なので、\(M=1\)なら\(R_1\)と\(R_2\)だけ、\(M=2\)なら\(R_0\)も調べればよいことがわかる。</p>

<h3 id="いくつかの変数の導入">いくつかの変数の導入</h3>
<p>次節からはアルゴリズムの詳細を見ていくが、簡単のために、この\(k\)をつかってあらかじめつぎの変数を導入しておく。</p>

\[\begin{align}
&amp;K_{\min} = k\text{の取りうる最小値}\\
&amp;K_{\max} = k\text{の取りうる最大値} \\
&amp;k' = k + 1 \\
&amp;s  = s_k,\quad t = t_k,\quad u = u_k,\quad w = w_k \\
&amp;s' = s_{k'}, \quad t' = t_{k'}, \quad u' = u_{k'}, \quad w' = w_{k'}
\end{align}\]

<p>この変数をつかうと、\(u\)と\(w\)の定義から、つぎのことがいえる。</p>

<p>\(u \in R_k\)もしくは\(w \in R_k\)のどちらかもしくは両方がなりたつ。また\(R_{k'} = \emptyset\)、\(R_{k'} = \{u'\}\)もしくは\(R_{k'} = \{w'\}\)のいずれかである。</p>

<h2 id="アルゴリズムの詳細">アルゴリズムの詳細</h2>

<ul>
  <li>\(s \ge D^M\)の場合
    <ul>
      <li>これが何を意味するかというと、\(s\)と\(t\)どちらかの長さは\(M\)より大きくなる、ということである。たとえば\(M = 1, s = 10, t = 11\)もしくは\(M = 2, s = 999, t = 1000\)を考えれば分かるだろう。\(k-1\)を考えなければならないのは長さが\(M\)以下でより\(u\)や\(w\)よりも\(v\)に近い値が表せるときだが、このケースでは\(u_{k-1}\)も\(w_{k-1}\)も長さが\(M\)より大きくなるか、あるいは\(u, w\)のいずれかと一致してしまう<sup id="fnref:k-1" role="doc-noteref"><a href="#fn:k-1" class="footnote" rel="footnote">10</a></sup>。したがって、\(k-1\)のケースは考える必要がない。</li>
      <li>\(R_{k'} \ne \emptyset\)の場合
        <ul>
          <li>\(R_{k'}=\{u'\}\)もしくは\(R_{k'}=\{w'\}\)となる</li>
          <li>\(u', w'\)は\(u, w\)よりも長さが短いかもしくは\(u, w\)と一致するので、\(u', w'\)の2つのどちらか\(R_v\)に含まれる方が\(d_v\)になる</li>
        </ul>
      </li>
      <li>\(R_{k'} = \emptyset\)の場合
        <ul>
          <li>前節で述べたように\(u \in R_k\)もしくは\(w \in R_k\)が成り立つので、このどちらかが\(d_v\)になる</li>
        </ul>
      </li>
      <li><strong>まとめ：</strong> \(s \ge D^M\)のとき\(u' \in R_v\)もしくは\(w' \in R_v\)なら、それぞれ\(d_v = u'\)、\(d_v=w'\)となる。\(u', w' \notin R_v\)のときは\(d_v = u\)もしくは\(d_v = w\)となる。</li>
    </ul>
  </li>
  <li>\(D \le s \lt D^M\)の場合
    <ul>
      <li>これは\(M=1\)のときは成立しないので\(M=2\)としてよい</li>
      <li>この場合は\(s, t\)の長さが\(1\)もしくは\(2\)になる。また両方の長さが\(1\)になることはない。\(s = 10, t = 11\)や\(s = 51, t = 52\)を考えれば分かるだろう。このケースも先ほどと同様に\(k-1\)を考える必要はない。なぜなら\(s_{k-1}\)も\(t_{k-1}\)も長さが\(3\)以上になるか、あるいは\(s\)や\(t\)と一致するからである。</li>
      <li>また、このとき、\(k + 1\)すなわち\(s', t'\)を考える必要もない。なぜなら\(u', w'\)が\(u, w\)より\(v\)に近いことはあり得ないし、長さが短くなったとしても\(1\)になるので、\(M=2\)の場合は\(v\)により近い\(u\)や\(w\)が選ばれるからである。</li>
      <li><strong>まとめ：</strong> \(D \le s \lt D^M\)のとき\(d_v = u\)もしくは\(d_v = w\)</li>
    </ul>
  </li>
  <li>\(s \lt D\)の場合
    <ul>
      <li>このとき\(u, w\)の長さは\(1\)になる。\(M=1\)ならこれが長さ\(M\)以下でもっとも\(v\)に近い値になる。</li>
      <li>\(M=2\)のときは\(u_{k-1}, w_{k-1}\)の長さが\(1\)もしくは\(2\)になり、より\(u, w\)よりも\(v\)から遠くはならないので\(d_v = u_{k-1}\)もしくは\(d_v = w_{k-1}\)</li>
      <li><strong>まとめ：</strong> \(M=1\)なら\(d_v = u\)もしくは\(d_v = w\)、\(M=2\)なら\(d_v = u_{k-1}\)もしくは\(d_v = w_{k-1}\)</li>
    </ul>
  </li>
</ul>

<h2 id="疑似コード">疑似コード</h2>
<p>以上のアルゴリズムを疑似コードにするとつぎのようになる。ただし、\(s\)を計算するまえに\(s \lt D\)の条件分岐をしたいという要請から\(v = c 2^q\)と表したときに、\(s \lt D \Leftrightarrow c \lt C_{\text{tiny}}\)となる\(C_{\text{tiny}}\)をあらかじめ計算しておく<sup id="fnref:c-tiny" role="doc-noteref"><a href="#fn:c-tiny" class="footnote" rel="footnote">11</a></sup>。なお\(c\)と\(q\)はそれぞれ整数で浮動小数点数の仮数部のように\(c = 1\ldots\)という形式を満たしている。さらに\(v_\text{tiny} = C_\text{tiny}2^Q_\min\)とする。ここで\(Q_\min\)は\(fp\)で\(q\)が取り得る値の最小値。</p>

<p>また、\(s_{k-1} = s_k(vD)\)かつ\(u_{k-1}=u_k(vD)\)から、疑似コードでは\(v \lt v_\text{tiny}\)のときに\(v\)に\(vD\)を代入して計算してさいごに指数部を\(1\)減らす、という簡略化を行っている。</p>

\[\begin{align}
&amp; \text{if} \, M = 2 \land v \lt v_\text{tiny} \, \text{then}\\
&amp; \quad v \gets v D, \Delta k \gets -1 \\
&amp; \text{else} \\
&amp; \quad \Delta k \gets 0 \\
&amp; \text{fi} \\
&amp; c, q, k, s \text{を計算する} \\
&amp; \text{if}\, s \ge D^M\,\text{then}\\
&amp; \quad k' \gets k + 1, s' \gets s // D, u' \gets s' D^{k'}, w' \gets (s' + 1)D^{k'} \\
&amp; \quad \text{if}\, u' \in R_v \, \text{then return}\, u' \\
&amp; \quad \text{if}\, w' \in R_v \, \text{then return}\, w' \\
&amp; \text{fi} \\
&amp; u \gets sD^k, w \gets (s + 1) D^k \\
&amp; \text{if}\, u \in R_v \land w \notin R_v\, \text{then} \\
&amp; \quad \text{return}\, uD^{\Delta k}\, \\
&amp; \text{fi} \\
&amp; \text{if}\, u \notin R_v \land w \in R_v\, \text{then} \\
&amp; \quad \text{return}\, wD^{\Delta k}\, \\
&amp; \text{fi} \\
&amp; \text{if}\, v - u \lt w - v\, \text{then} \\
&amp; \quad \text{return}\, uD^{\Delta k}\, \\
&amp; \text{fi}\\
&amp; \text{if}\, v - u \gt w - v\, \text{then} \\
&amp; \quad \text{return}\, wD^{\Delta k}\, \\
&amp; \text{fi}\\
&amp; \text{if}\, s \,\text{が偶数 then} \\
&amp; \quad \text{return}\, uD^{\Delta k}\, \\
&amp; \text{fi} \\
&amp; \text{return}\, wD^{\Delta k} \\
\end{align}\]

<p>なお、\(//\)は整数除算で余りを切り捨てる操作を表す。</p>

<p>このアルゴリズムをそのまま実装すると任意精度の計算（Javaの<code class="language-plaintext highlighter-rouge">BigInterger</code>など）を必要とするので、さらに改良してビット演算だけで行う方法が論文に紹介されている。また、このアルゴリズムはOpenJDKの<code class="language-plaintext highlighter-rouge">Double#toString()</code>の実装のもとになっているのでコードを読んでもいいだろう。</p>

<h2 id="蛇足">蛇足</h2>

<p>この論文に興味を持ったきっかけは<a href="https://www.zverovich.net/2023/06/04/printing-double.html">Printing double aka the most difficult problem in computer science</a>という記事だった。記事の内容は率直に言って鼻持ちならない内容で他人のStackOverflowの回答を貶していく、という控え目にいってもこの作者と一緒に働きたくないな、というものだった。タイトルの<code class="language-plaintext highlighter-rouge">double</code>の文字列化をコンピュータ科学で最も難しい問題と臆面もなく言い放つことにも反感を覚えて、論文を手にとった次第だ。</p>

<p>読んだ感想としては依然として「CSで一番難しい問題」とは思えないけれど、OpenJDKの実装にも使われるアルゴリズムを勉強できてよかった、と思う。</p>

<p>ただ、この浮動小数点数の文字列化という問題はかなり恣意的であるように思う。まず有効桁数を気にしない癖に出力がround tripを満たす範囲で短くあってほしい、という状況がよく分からない。実験や数値計算のアプリケーションでデータの再利用を前提とするなら有効桁数は常に気にするし、出力データで正確さを担保したければ記事の冒頭でも書いたように小数点以下17桁まで出力しておけばよい。一方、データの再利用を前提とせずにユーザーに表示するだけ、たとえばBMIの結果を表示する、となったらそれはそれでアプリケーションで適切に有効数字を決められるだろう。</p>

<p>この「いい感じに浮動小数点数を文字列化したい」という問題は、そう考えるとデバッグ用にログする場合くらいしか用途がなさそうで、実際Javaの<code class="language-plaintext highlighter-rouge">Double#toString()</code>はまさにそういう用途だ。そう考えると、解こうとしている問題はかなり限定的で、わざわざがんばって高速化する意味がどれくらいあるかは分からない。</p>

<p>なお、このアルゴリズムの前にも浮動小数点数の文字列化にはいくつもアルゴリズムが提唱されているらしい。それらについては調べていないので、興味のある人はどうぞ調べてみてほしい<sup id="fnref:ref" role="doc-noteref"><a href="#fn:ref" class="footnote" rel="footnote">12</a></sup>。</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:cpp_std_fmt" role="doc-endnote">
      <p><a href="https://stackoverflow.com/a/65329803">Stackoverflowの回答</a>によると、C++20の<code class="language-plaintext highlighter-rouge">std::format</code>は<a href="https://github.com/fmtlib/fmt">{fmt}ライブラリ</a>をもとにしており、{fmt}ライブラリは浮動小数点数のフォーマットに<a href="https://github.com/jk-jeon/dragonbox"><code class="language-plaintext highlighter-rouge">Dragonbox</code></a>というライブラリをつかっており、このライブラリの元がこの論文らしい <a href="#fnref:cpp_std_fmt" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:inf_binary" role="doc-endnote">
      <p><code class="language-plaintext highlighter-rouge">1.2</code>がなぜ有限の2進数で表せないか考えるのは、ちょうどいい算数の問題だろう。一方で有限の2進数はかならず有限の10進数で表せることが分かっている。 <a href="#fnref:inf_binary" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:decimal" role="doc-endnote">
      <p>ここではほとんどの人間は10進数の方が理解しやすいという前提をおいている。2進数の方が理解しやすいという人に対してはそのまま浮動小数点数のバイナリ表記を表示すればいいので簡単だ。 <a href="#fnref:decimal" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:round_trip" role="doc-endnote">
      <p>Schubfachの論文ではとくになんの説明もなしにround tripという用語が使われていた。初出はどこか分からないが往復のことをround tripと呼ぶし英語の表現としては自然なのかもしれない。 <a href="#fnref:round_trip" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:17digits" role="doc-endnote">
      <p>When is round-trip floating point radix conversion exact? <a href="https://www.johndcook.com/blog/2020/03/16/round-trip-radix-conversion/">https://www.johndcook.com/blog/2020/03/16/round-trip-radix-conversion/</a> <a href="#fnref:17digits" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:shorter_better" role="doc-endnote">
      <p>率直にいうと、自分はたとえば数値計算で大量の数値を見るならすべての値の桁はそろっていてほしいし、これがどれくらい一般的な要求なのかやや疑問ではある。あと、17桁くらいならいいんじゃないと思ってしまうが、この感覚は一般的ではないのかもしれない。 <a href="#fnref:shorter_better" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:c-explained" role="doc-endnote">
      <p>分かりにくいが原論文がそうなっているので我慢されたい。たとえば\(v = 1.011_2\cdot 2^{-10}\)だったら\(c=101100000_2\)、\(q=-18\)となる <a href="#fnref:c-explained" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:intersection-set" role="doc-endnote">
      <p>なぜ\(R_v\)と同じ\(R\)を使うのが理解に苦しむが原論文がそうしているので混乱を避けるためにもそうする。読者は添字が\(v\)であるか、あるいは整数であるかで判断していただきたい。 <a href="#fnref:intersection-set" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:pigeonhole-principle" role="doc-endnote">
      <p>詳細はまたも原論文に譲るが、アイディアとしては「鳩の巣原理」にもとづいており、このアルゴリズムをSchubfachと呼ぶのもこの原理の発明者ディリクレのドイツ名Schubfachprinzipに由来するらしい。 <a href="#fnref:pigeonhole-principle" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:k-1" role="doc-endnote">
      <p>たとえば\(M=1\)で\(s_k(v)=10, t_k(v)=11\)となった場合を考えよう。\(s_{k-1}(v)\)と\(s_{k-1}(v)\)は\(v\)の値によるが、\(\{100, 101, 102, \ldots, 110\}\)のどれかであり、両端はそれぞれ\(s_k(v), t_k(v)\)と一致している。したがって\(k-1\)を探す必要はないことが分かる。 <a href="#fnref:k-1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:c-tiny" role="doc-endnote">
      <p>詳細はこれも原論文に譲るが、\(C_{\text{tiny}} = \lceil 2^{-Q_\min} D^{K_\min + 1} \rceil\)と表され、\(D=10\)で倍精度浮動小数点の場合は\(C_{\text{tiny}} = \lceil 2^{1074} 10^{-324 + 1} \rceil = 3\)、単精度浮動小数点の場合は\(C_{\text{tiny}} = 8\)となる。これは仮数部の下位2ビットもしくは3ビット以外がすべて0になっている場合なので、非正規化数のうちでもかなり小さい値であることがわかる。 <a href="#fnref:c-tiny" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:ref" role="doc-endnote">
      <p>日本語の資料だと<a href="https://blog.miz-ar.info/2022/04/float-to-string/?utm_source=pocket_saves">浮動小数点数の文字列化（基数変換）</a>という記事にいくつかの論文への参照が載っていた。 <a href="#fnref:ref" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[TL;DR C++20のstd::formatやJavaのDouble#toString()のもとにもなっている1浮動小数点数の文字列化アルゴリズムSchubfachアルゴリズムの論文を読んだ。 Stackoverflowの回答によると、C++20のstd::formatは{fmt}ライブラリをもとにしており、{fmt}ライブラリは浮動小数点数のフォーマットにDragonboxというライブラリをつかっており、このライブラリの元がこの論文らしい &#8617;]]></summary></entry><entry><title type="html">ChromeOS用SKK（chrome-skk）をWeb Storeに公開しました</title><link href="https://hydrakecat.net/chrome-skk/2023/03/19/chrome-skk-webstore.html" rel="alternate" type="text/html" title="ChromeOS用SKK（chrome-skk）をWeb Storeに公開しました" /><published>2023-03-19T07:00:00+00:00</published><updated>2023-03-19T07:00:00+00:00</updated><id>https://hydrakecat.net/chrome-skk/2023/03/19/chrome-skk-webstore</id><content type="html" xml:base="https://hydrakecat.net/chrome-skk/2023/03/19/chrome-skk-webstore.html"><![CDATA[<p><a href="/chrome-skk/2022/08/06/chrome-skk.html">先日の記事</a>でManifest V3の対応に時間がかかるのでWeb Storeの公開は先になりそうですと書いたのですが、多少進展があってギリギリ使い物になるかなくらいになったので公開しました。</p>

<p><a href="https://chrome.google.com/webstore/detail/skk-japanese-input/gdfnmlnbnmgdliccidmiphhpicaecffj">https://chrome.google.com/webstore/detail/skk-japanese-input/gdfnmlnbnmgdliccidmiphhpicaecffj</a></p>

<!-- more -->

<p>詳細は <a href="https://github.com/hkurokawa/chrome-skk">https://github.com/hkurokawa/chrome-skk</a> の README を読んでほしいのですが、最新版には以下のような既知の問題があります。</p>

<ul>
  <li>30秒以上同じウィンドウで何も入力していないと SKK 自体が動かなくなる（直接入力しかできなくなる）</li>
  <li>タブやウィンドウを切り替えると直るが、直後にキーボードのキーを押下して文字が入力されるまで数秒のラグがある</li>
</ul>

<p>これは回避策があって、Chrome のタブで Extension のオプションページを開き、開発者ツールを起動した状態で放置しておく、というものです。ハックなので、Chrome のバージョンが上がると動かなくなるかもしれませんが、とりあえず 112.0.5615.29 (Official Build) beta では動いています。</p>

<p>このハックが嫌な場合は <a href="https://github.com/hkurokawa/chrome-skk/releases">https://github.com/hkurokawa/chrome-skk/releases</a> から v0.x 系の zip をダウンロードして手動でインストールしてください。今後も v0.x 系は Manifest V3 に移行しないで、重要度の高いバグ修正は入れてメンテナンスしていこうと考えています。</p>

<p>要望や問題がありましたらお気軽に <a href="https://github.com/hkurokawa/chrome-skk/issues">https://github.com/hkurokawa/chrome-skk/issues</a> でご報告ください。</p>]]></content><author><name></name></author><category term="chrome-skk" /><summary type="html"><![CDATA[先日の記事でManifest V3の対応に時間がかかるのでWeb Storeの公開は先になりそうですと書いたのですが、多少進展があってギリギリ使い物になるかなくらいになったので公開しました。]]></summary></entry><entry><title type="html">スプラトゥーン2でオールX（ただし北米）になった</title><link href="https://hydrakecat.net/2022/08/20/splatoon-allx.html" rel="alternate" type="text/html" title="スプラトゥーン2でオールX（ただし北米）になった" /><published>2022-08-20T07:00:00+00:00</published><updated>2022-08-20T07:00:00+00:00</updated><id>https://hydrakecat.net/2022/08/20/splatoon-allx</id><content type="html" xml:base="https://hydrakecat.net/2022/08/20/splatoon-allx.html"><![CDATA[<p><a href="/2020/07/25/middle-age-competitive-programming.html">前の記事</a>で、スプラトゥーン2にハマっていると書いたのだけれど、2年たってようやくウデマエがオールXになったので、メモ。</p>

<p><img src="/assets/images/splatoon2_rankx_screenshot.png" alt="" /></p>

<!-- more -->

<p>以下は2年間のパワーとウデマエの推移。</p>

<p>スプラトゥーン2のガチマッチでは参加している8人のプレイヤーの平均ガチマッチパワーというのが表示されるのだけれど、その平均パワーと、例としてガチエリアのウデマエを時系列グラフで表示している。ウデマエは<code class="language-plaintext highlighter-rouge">C-</code>が<code class="language-plaintext highlighter-rouge">0</code>として、以降<code class="language-plaintext highlighter-rouge">C</code>が<code class="language-plaintext highlighter-rouge">1</code>、<code class="language-plaintext highlighter-rouge">C+</code>が<code class="language-plaintext highlighter-rouge">2</code>…、<code class="language-plaintext highlighter-rouge">S</code>が<code class="language-plaintext highlighter-rouge">9</code>…、<code class="language-plaintext highlighter-rouge">X</code>が<code class="language-plaintext highlighter-rouge">20</code>に対応する。</p>

<p><img src="/assets/images/splatoon2_rankx_power_history.png" alt="" /></p>

<p><img src="/assets/images/splatoon2_rankx_rank_history.png" alt="" /></p>

<p>だいたい朝に1 - 2時間くらいガチマッチに潜るというのを習慣にしていた。プレイ時間は1300時間くらい。ヒーローモード、オクト・エキスパンション、サーモンランも含んでいるけれど、たぶんガチマに使った時間を考えると誤差だろう。</p>

<p>見ると分かるけれど、2020年の夏から始めて2021年の冬くらいにウデマエ<code class="language-plaintext highlighter-rouge">S</code>でかなり停滞している。この頃はなかなかしんどくて、違うブキも試したり、画面を録画して反省したり知り合いに送って意見を貰ったりと試行錯誤していた。けっきょくブキはディアルスイーパーカスタムでかつ上にあるように疑似確でないギアで押し通した。たぶん強くないけれど低XP帯ならどのブキだろうがギアパワーだろうがそんなに関係ない気がしている。</p>

<p>2022年の年明けに急にウデマエが上がっているけれど、これはたぶん北米に引っ越したせいだと思っている。体感でしかないけれど、日本に比べると北米はガチマッチパワーの実態が数字よりも100 - 200くらい低い気がする。つまり北米での2,000が日本で1,800 - 1,900あたりになる。おそらくプレイ人口が少ないこと、あまりスプラトゥーン2に関する情報が出回っていないことが関係しているのだろう。実際に出張で2022年の6月に日本に帰ったときは、エリアはXになっていたけれど、あっという間にS+に降格してしまった。</p>

<p>そういう意味で、自分はオールXは達成したけれど、日本だったらS+5とかそのへんくらいの実力だと思っている。</p>

<h1 id="振り返り">振り返り</h1>
<p>FPS/TPSと呼ばれるゲームは初めて、かつ、対人戦ゲームをじっくりやりこむのも初めてだったので、新鮮だった。最初はキルカメラで敵がイカ状態とヒト状態を高速で繰り返すのを見てコントローラが壊れたかバグかと思うくらいには初（うぶ）だった((いわゆる煽り行為))し、コントローラのジャイロの使い方もおぼつかなかったけれど、思い通りにキャラコンが出来るようになって、相手の場所をなんとなく把握できるようになると、俄然たのしくなった。</p>

<p>そこそこやり込むと分かるけれど、ともかくキャラコンとエイムでごり押しする対面能力だけで勝ち進むプレイヤーもいれば、盤面を把握してルールにうまく関与することで試合に勝とうとするプレイヤーもいる。他のFPSゲームは分からないけれどスプラトゥーン2は後者のプレイスタイルでも勝てるのがとてもよかった。</p>

<p>スプラトゥーン3がもうすぐ出る。どれくらいやり込むことになるかは分からないけれど、たのしみだ。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[前の記事で、スプラトゥーン2にハマっていると書いたのだけれど、2年たってようやくウデマエがオールXになったので、メモ。]]></summary></entry><entry><title type="html">ChromeOS用SKKを公開しました</title><link href="https://hydrakecat.net/chrome-skk/2022/08/06/chrome-skk.html" rel="alternate" type="text/html" title="ChromeOS用SKKを公開しました" /><published>2022-08-06T07:00:00+00:00</published><updated>2022-08-06T07:00:00+00:00</updated><id>https://hydrakecat.net/chrome-skk/2022/08/06/chrome-skk</id><content type="html" xml:base="https://hydrakecat.net/chrome-skk/2022/08/06/chrome-skk.html"><![CDATA[<p><a href="https://github.com/hkurokawa/chrome-skk/releases/tag/v0.1.10">chrome-skk v0.1.10</a>を公開しました。インストール方法などは<a href="https://github.com/hkurokawa/chrome-skk">README</a>を参照ください。</p>

<!-- more -->

<p>もともとjmukさんが<a href="https://github.com/jmuk/chrome-skk">https://github.com/jmuk/chrome-skk</a>として公開していたレポジトリをフォークさせてもらいました。本当はChrome Web Storeに公開したかったのですが、Manifest V3に移行する必要があり、<a href="https://github.com/hkurokawa/chrome-skk/issues/14">service worker化に関するバグ</a>が意外と時間がかかりそうだったので、先にzip形式で公開することにしました。</p>

<p>SKKはもう10年近く愛用しているIMEで、1年前に私用のノートパソコンをPixelbookにしたことがきっかけでChromeOS用のSKKの需要が自分の中で高まりました。幸い、上記のjmukさんのレポジトリを見つけたので、適当に変更して手元で快適に使っていたのですが、せっかくなので公開した次第です。</p>

<p>どれくらいChromeOSユーザーでSKK使いがいるか分かりませんが、よかったら使ってみてください。</p>]]></content><author><name></name></author><category term="chrome-skk" /><summary type="html"><![CDATA[chrome-skk v0.1.10を公開しました。インストール方法などはREADMEを参照ください。]]></summary></entry><entry><title type="html">近況報告</title><link href="https://hydrakecat.net/2022/02/18/update-2018.html" rel="alternate" type="text/html" title="近況報告" /><published>2022-02-18T07:00:00+00:00</published><updated>2022-02-18T07:00:00+00:00</updated><id>https://hydrakecat.net/2022/02/18/update-2018</id><content type="html" xml:base="https://hydrakecat.net/2022/02/18/update-2018.html"><![CDATA[<p>米国に引っ越しました。</p>

<!--more-->
<p>今週の月曜日に米国のベイエリアに引っ越しました。いまは会社の用意してくれたアパートに住みながら家を探したりしています。会社も仕事も前と同じです。近くにお住まいの方はぜひお声がけください。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[米国に引っ越しました。]]></summary></entry><entry><title type="html">浮動小数点数のパーサを書いてみた</title><link href="https://hydrakecat.net/2020/11/01/parse-float-number.html" rel="alternate" type="text/html" title="浮動小数点数のパーサを書いてみた" /><published>2020-11-01T02:06:45+00:00</published><updated>2020-11-01T02:06:45+00:00</updated><id>https://hydrakecat.net/2020/11/01/parse-float-number</id><content type="html" xml:base="https://hydrakecat.net/2020/11/01/parse-float-number.html"><![CDATA[<p>先日、こういうツイートを見かけた。で、さいきん浮動小数点数づいているのもあって自分も浮動小数点数の文字列表記をパースして単精度および倍精度浮動小数点数に変換するパーサを書いてみた。</p>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">非常に苦労したので書いた<br />もっといいやり方がありそうだし、もっとうまい説明ができるかもしれないけどこれが限界でした・・・<br /><br />文字列少数点数表記を IEEE754 倍精度浮動小数点数にエンコードする方法｜Sukesan1984 <a href="https://twitter.com/hashtag/note?src=hash&amp;ref_src=twsrc%5Etfw">#note</a> <a href="https://t.co/2v5f1eMzea">https://t.co/2v5f1eMzea</a></p>&mdash; Sukesan1984 (@sukesan1984) <a href="https://twitter.com/sukesan1984/status/1319270055390048256?ref_src=twsrc%5Etfw">October 22, 2020</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<p>この記事では、自分が実装したパーサを説明する。説明はいいからコードを見せろ、という人はこちらをどうぞ。</p>

<p><a href="https://github.com/hkurokawa/FloatingPointNumberParser">https://github.com/hkurokawa/FloatingPointNumberParser</a></p>

<!--more-->

<h1 id="ゴール">ゴール</h1>
<p>今回は浮動小数点数の理解を深めるために自分でパーサを書くのが目的である。したがって、パフォーマンスは気にしない。また、シンタックスエラーも気にしないことにした<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>。さいわい先ほど引用したツイートへの<a href="https://twitter.com/sukesan1984/status/1319289881110048776?s=20">補足ツイート</a>にもあるがGo言語の標準ライブラリのテストケースがちょうどよさそうなので、このテストが通ることを目標とする。</p>

<p><a href="https://golang.org/src/strconv/atof_test.go">https://golang.org/src/strconv/atof_test.go</a></p>

<h1 id="実装方針">実装方針</h1>
<p>基本的には<a href="https://ja.wikipedia.org/wiki/%E5%8D%98%E7%B2%BE%E5%BA%A6%E6%B5%AE%E5%8B%95%E5%B0%8F%E6%95%B0%E7%82%B9%E6%95%B0#%E5%8D%81%E9%80%B2%E8%A1%A8%E7%8F%BE%E3%81%8B%E3%82%89_binary32_%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88%E3%81%B8%E3%81%AE%E5%A4%89%E6%8F%9B">Wikipediaの記事</a>にあるのと同じことをすればよい。</p>

<p>入力を10進数表記の文字列としよう（2進数表記でも同じ議論が成り立つ）。たとえば <code class="language-plaintext highlighter-rouge">0.15625</code> といった文字列だ。これを2進数表記にしたい。最初に気付くのは、無限精度のままこの10進数表記の値を扱う必要があるということだ<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>。一見、floatなら仮数部が23桁なので2進数表記で小数点以下23桁、切り上げや切り下げを考えても24桁まで見ればよさそうに思えるが、そうではない。最終的に最近接丸めをするには小数点以下24位が0だったとしても、そのさらに先で0以外の値があればそれは結果に影響する。したがって10進数表記の文字列を1文字目から順に見ていったとして、どこかでこれ以上読まなくてよいという場所は存在しない（ずっと0が続くのか、それともどこかで0以外が登場するのかは知る必要がある）。</p>

<p>以上のことから、まず10進数表記の文字列はそのまま無限精度の10進数の値として格納することにする。</p>

<p>つづいてこれを2進数表記に変換することを考えよう。整数部分については既知なので以降では小数部分についてだけ考える。</p>

<p>もっともナイーブな方法は変換したい値が\({0.5}\)より大きいか調べ、大きければ小数点以下第1位のビットを立てて\({0.5}\)を引く。つぎにその引いた結果と\({0.25}\)を比較し、それより大きければ小数点以下第2位のビットを立てる、という方法だ。この操作を行うと、たとえば10進表記の\({0.15625}\)という値は2進表記で\({0.00101}\)となることがわかる。</p>

<p>これは2進数の定義そのものなので理解はしやすいが、無限精度の10進数で引き算をやるのはあまりぞっとしない。ちょっと考えると、この\({0.5}\)を引いて、つぎは\({0.25}\)を引いて、という操作は、対象の値を\({2}\)倍して\({1}\)を引いて、さらに\({2}\)倍して\({1}\)を引くという操作と等価なことがわかる。じっさい、さきほどの\({0.15625}\)にこの操作を行うと同じ結果が得られる<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>。</p>

<p>さて、以上で無限精度の10進数を2進表記する方法がわかった。また、浮動小数点数の定義上、この値を\(a \times 2^{k}\)という形式にする必要がある。ただし\({a}\)は範囲\({[1, 2)}\)に収まる実数。すなわち、対象の数に\({2}\)もしくは\({\frac{1}{2}}\)を掛けて\({[1, 2)}\)の範囲に収める必要がある。このことから、無限精度の10進数が\({2}\)倍および\({\frac{1}{2}}\)倍の演算をサポートしている必要がある。</p>

<h1 id="parsefloatのおおまかな実装">parseFloatのおおまかな実装</h1>
<p>以下では、さきほど述べた\({2}\)倍および\({\frac{1}{2}}\)倍の演算をサポートした無限精度の10進数を表すクラス <code class="language-plaintext highlighter-rouge">BigDecimal</code> があるとする。これを使って値を浮動小数点数に変換する。</p>

<p>まずは\({[1, 2)}\)の範囲に収めよう。\({2}\)倍あるいは\({\frac{1}{2}}\)倍した場合はそれに合わせて指数部を増減させる。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nc">BigDeciaml</span> <span class="n">d</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">BigDecimal</span><span class="o">(</span><span class="n">s</span><span class="o">);</span> <span class="c1">// 10進数文字列表記を無限精度の10進数に保存する</span>
    <span class="kt">int</span> <span class="n">exponent</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
    <span class="k">while</span> <span class="o">(</span><span class="n">d</span><span class="o">.</span><span class="na">isEqualToOrGreaterThanTwo</span><span class="o">())</span> <span class="o">{</span> <span class="c1">// 2以上なら2未満になるまで1/2を掛ける</span>
      <span class="n">d</span><span class="o">.</span><span class="na">divideByTwo</span><span class="o">();</span>
      <span class="n">exponent</span><span class="o">++;</span>
    <span class="o">}</span>
    <span class="k">while</span> <span class="o">(</span><span class="n">d</span><span class="o">.</span><span class="na">isLessThanOne</span><span class="o">())</span> <span class="o">{</span> <span class="c1">// 1未満なら1以上になるまで2を掛ける</span>
      <span class="n">d</span><span class="o">.</span><span class="na">multiplyByTwo</span><span class="o">();</span>
      <span class="n">exponent</span><span class="o">--;</span>
    <span class="o">}</span>
    <span class="n">exponent</span> <span class="o">+=</span> <span class="mi">127</span><span class="o">;</span> <span class="c1">// バイアスを足す</span>
</code></pre></div></div>

<p>さて、ここで <code class="language-plaintext highlighter-rouge">d</code> は\({[1, 2)}\)の範囲に入っているはずである。あとはこの小数部を2進数表記すればよい。なお <code class="language-plaintext highlighter-rouge">d.discardNumberPart()</code> は整数部分を0クリアするメソッドである。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">d</span><span class="o">.</span><span class="na">discardNumberPart</span><span class="o">();</span>
    <span class="kt">int</span> <span class="n">mantissa</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
    <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">22</span><span class="o">;</span> <span class="n">i</span> <span class="o">&gt;=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span><span class="o">--)</span> <span class="o">{</span>
      <span class="n">d</span><span class="o">.</span><span class="na">multiplyByTwo</span><span class="o">();</span>
      <span class="k">if</span> <span class="o">(!</span><span class="n">d</span><span class="o">.</span><span class="na">isLessThanOne</span><span class="o">())</span> <span class="o">{</span>
        <span class="n">mantissa</span> <span class="o">|=</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">i</span><span class="o">;</span>
        <span class="n">d</span><span class="o">.</span><span class="na">discardNumberPart</span><span class="o">();</span>
      <span class="o">}</span>
    <span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">d</code>を2倍して\({1.0}\)以上であればビットを立て、\({1}\)を引いている。</p>

<p>以上でほぼ終わりである。あとは符号部を組み合わせれば単精度浮動小数点数になる。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kt">int</span> <span class="n">sign</span> <span class="o">=</span> <span class="n">d</span><span class="o">.</span><span class="na">isNegative</span><span class="o">()</span> <span class="o">?</span> <span class="mi">1</span> <span class="o">:</span> <span class="mi">0</span><span class="o">;</span>
    <span class="kt">int</span> <span class="n">bits</span> <span class="o">=</span> <span class="n">mantissa</span> <span class="o">|</span> <span class="o">(</span><span class="n">exponent</span> <span class="o">&lt;&lt;</span> <span class="mi">23</span><span class="o">)</span> <span class="o">|</span> <span class="o">(</span><span class="n">sign</span> <span class="o">&lt;&lt;</span> <span class="mi">31</span><span class="o">);</span>
    <span class="k">return</span> <span class="nc">Float</span><span class="o">.</span><span class="na">intBitsToFloat</span><span class="o">(</span><span class="n">bits</span><span class="o">);</span>
</code></pre></div></div>

<h2 id="いくつかのエッジケースの処理">いくつかのエッジケースの処理</h2>
<p>ここまでで “1.0” や “3.1415” といった文字列はパースできるはずである。しかし、いくつかのエッジケースについて考慮が漏れている。</p>

<p>まずは “0.0” が与えられた場合。この場合はいくら2倍しても\({1}\)以上になることはないので無限ループになる。したがって “0” に等しい場合はearly returnする必要がある。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">if</span> <span class="o">(</span><span class="n">d</span><span class="o">.</span><span class="na">isZero</span><span class="o">())</span> <span class="o">{</span>
      <span class="k">return</span> <span class="nc">Float</span><span class="o">.</span><span class="na">intBitsToFloat</span><span class="o">(</span><span class="n">sign</span> <span class="o">&lt;&lt;</span> <span class="mi">31</span><span class="o">);</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>続いて、オーバーフローの場合。単精度浮動小数点数では\({2^{127}}\)を超える数を表すことはできない。したがって <code class="language-plaintext highlighter-rouge">+Infinity</code> もしくは <code class="language-plaintext highlighter-rouge">-Infinity</code> を返す必要がある。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">if</span> <span class="o">(</span><span class="n">exponent</span> <span class="o">&gt;</span> <span class="mi">127</span><span class="o">)</span> <span class="k">return</span> <span class="n">d</span><span class="o">.</span><span class="na">isNegative</span><span class="o">()</span> <span class="o">?</span> <span class="nc">Float</span><span class="o">.</span><span class="na">NEGATIVE_INFINITY</span> <span class="o">:</span> <span class="nc">Float</span><span class="o">.</span><span class="na">POSITIVE_INFINITY</span><span class="o">;</span>
</code></pre></div></div>

<h2 id="最近接偶数丸め">最近接偶数丸め</h2>
<p>さらに最近接偶数丸めも対応する必要がある。まず仮数部で表せる桁の最下位の1つ下の桁が2進数表記で\({1}\)かどうかを調べる。これはいままでと同様に\({2}\)倍して\({1}\)以上になるか調べればよい。ここまでで対象の数が単精度浮動小数点数で表せる数のちょうど中間か中間よりも少しだけ0より遠い側にあることが分かる。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="c1">// 仮数部はすべて詰め終わっており、dには0.xxxxという形でそれより下の小数部が格納されている</span>
    <span class="c1">// 最近接偶数丸め</span>
    <span class="k">if</span> <span class="o">(!</span><span class="n">d</span><span class="o">.</span><span class="na">isZero</span><span class="o">())</span> <span class="o">{</span>
      <span class="n">d</span><span class="o">.</span><span class="na">multiplyByTwo</span><span class="o">();</span>
      <span class="k">if</span> <span class="o">(!</span><span class="n">d</span><span class="o">.</span><span class="na">isLessThanOne</span><span class="o">())</span> <span class="o">{</span> <span class="c1">// dは1.xxxxという形</span>
        <span class="n">d</span><span class="o">.</span><span class="na">discardNumberPart</span><span class="o">();</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">d</span><span class="o">.</span><span class="na">isZero</span><span class="o">())</span> <span class="o">{</span>
          <span class="c1">// ちょうど中間なので偶数になるように丸める</span>
          <span class="k">if</span> <span class="o">((</span><span class="n">mantissa</span> <span class="o">&amp;</span> <span class="mi">1</span><span class="o">)</span> <span class="o">==</span> <span class="mi">1</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">mantissa</span><span class="o">++;</span>
          <span class="o">}</span>
        <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
          <span class="c1">// 0より遠い側なので最近接にするために仮数部を1増やす</span>
          <span class="n">mantissa</span><span class="o">++;</span>
        <span class="o">}</span>
      <span class="o">}</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>以上で最近接偶数丸めができた。めでたしめでたし、となりたいところだが、そうは問屋が卸さない。実は仮数部を1増やしたことによって仮数部がオーバーフローする可能性があるのだ。すなわち仮数部23ビットすべてに<code class="language-plaintext highlighter-rouge">1</code>が立っている状態で\({1}\)を足せばとうぜん仮数部は足りなくなる。この場合は指数部を\({1}\)増やして仮数部は\({0}\)にクリアする必要がある。さらに指数部を\({1}\)増やしたことによって全体がオーバーフローしないかチェックする必要もある。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   <span class="k">if</span> <span class="o">(</span><span class="n">mantissa</span> <span class="o">==</span> <span class="mh">0x800000</span><span class="o">)</span> <span class="o">{</span>
     <span class="c1">// 仮数部が23ビットを超えている</span>
     <span class="n">mantissa</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
     <span class="n">exponent</span><span class="o">++;</span>
     <span class="k">if</span> <span class="o">(</span><span class="n">exponent</span> <span class="o">&gt;</span> <span class="mi">127</span><span class="o">)</span> <span class="o">{</span>
       <span class="k">return</span> <span class="n">d</span><span class="o">.</span><span class="na">isNegative</span><span class="o">()</span> <span class="o">?</span> <span class="nc">Float</span><span class="o">.</span><span class="na">NEGATIVE_INFINITY</span> <span class="o">:</span> <span class="nc">Float</span><span class="o">.</span><span class="na">POSITIVE_INFINITY</span><span class="o">;</span>
     <span class="o">}</span>
   <span class="o">}</span>
</code></pre></div></div>

<p>さて、ここまでで正規化数はすべてパースできるはずだ。最後に非正規化数をサポートする。</p>

<h2 id="非正規化数">非正規化数</h2>
<p>非正規化数は対象の値が\({2^{-126}}\)より小さい場合に必要になる。まず2進数表記で\({0.xx\ldots x \times 2^{-126}}\)という形にして、そのうえで指数部は\({0}\)にクリアしておく。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">if</span> <span class="o">(</span><span class="n">exponent</span> <span class="o">&gt;=</span> <span class="o">-</span><span class="mi">126</span><span class="o">)</span> <span class="o">{</span>
      <span class="c1">// 正規化数</span>
      <span class="c1">// バイアスを足す</span>
      <span class="n">exponent</span> <span class="o">+=</span> <span class="mi">127</span><span class="o">;</span>
    <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
      <span class="c1">// 非正規化数</span>
      <span class="k">while</span> <span class="o">(</span><span class="n">exponent</span> <span class="o">&lt;</span> <span class="o">-</span><span class="mi">126</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">d</span><span class="o">.</span><span class="na">divideByTwo</span><span class="o">();</span>
        <span class="n">exponent</span><span class="o">++;</span>
      <span class="o">}</span>
      <span class="n">exponent</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
    <span class="o">}</span>
</code></pre></div></div>

<h1 id="完成版">完成版</h1>
<p>以上をすべて行った文字列をパースして単精度浮動小数点数を返すコードが以下である。今後バグが見つかった場合は<a href="https://github.com/hkurokawa/FloatingPointNumberParser">GitHubレポジトリ</a>を更新していくので、最新版のコードが見たい方はそちらを参照いただきたい。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">public</span> <span class="kt">float</span> <span class="nf">parseFloat</span><span class="o">(</span><span class="nc">String</span> <span class="n">s</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">BigNumber</span> <span class="n">d</span> <span class="o">=</span> <span class="nc">BigNumber</span><span class="o">.</span><span class="na">parse</span><span class="o">(</span><span class="n">s</span><span class="o">);</span> <span class="c1">// 10進数文字列表記を無限精度の10進数に保存する</span>
    <span class="kt">int</span> <span class="n">sign</span> <span class="o">=</span> <span class="n">d</span><span class="o">.</span><span class="na">isNegative</span><span class="o">()</span> <span class="o">?</span> <span class="mi">1</span> <span class="o">:</span> <span class="mi">0</span><span class="o">;</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">d</span><span class="o">.</span><span class="na">isZero</span><span class="o">())</span> <span class="o">{</span>
      <span class="k">return</span> <span class="nc">Float</span><span class="o">.</span><span class="na">intBitsToFloat</span><span class="o">(</span><span class="n">sign</span> <span class="o">&lt;&lt;</span> <span class="mi">31</span><span class="o">);</span>
    <span class="o">}</span>
    <span class="kt">int</span> <span class="n">mantissa</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
    <span class="kt">int</span> <span class="n">exponent</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
    <span class="k">while</span> <span class="o">(</span><span class="n">d</span><span class="o">.</span><span class="na">isEqualToOrGreaterThanTwo</span><span class="o">())</span> <span class="o">{</span> <span class="c1">// 2以上なら2未満になるまで1/2を掛ける</span>
      <span class="n">d</span><span class="o">.</span><span class="na">divideByTwo</span><span class="o">();</span>
      <span class="n">exponent</span><span class="o">++;</span>
    <span class="o">}</span>
    <span class="k">while</span> <span class="o">(</span><span class="n">d</span><span class="o">.</span><span class="na">isLessThanOne</span><span class="o">())</span> <span class="o">{</span> <span class="c1">// 1未満なら1以上になるまで2を掛ける</span>
      <span class="n">d</span><span class="o">.</span><span class="na">multiplyByTwo</span><span class="o">();</span>
      <span class="n">exponent</span><span class="o">--;</span>
    <span class="o">}</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">exponent</span> <span class="o">&gt;</span> <span class="mi">127</span><span class="o">)</span> <span class="k">return</span> <span class="n">d</span><span class="o">.</span><span class="na">isNegative</span><span class="o">()</span> <span class="o">?</span> <span class="nc">Float</span><span class="o">.</span><span class="na">NEGATIVE_INFINITY</span> <span class="o">:</span> <span class="nc">Float</span><span class="o">.</span><span class="na">POSITIVE_INFINITY</span><span class="o">;</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">exponent</span> <span class="o">&gt;=</span> <span class="o">-</span><span class="mi">126</span><span class="o">)</span> <span class="o">{</span>
      <span class="c1">// 正規化数</span>
      <span class="c1">// バイアスを足す</span>
      <span class="n">exponent</span> <span class="o">+=</span> <span class="mi">127</span><span class="o">;</span>
    <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
      <span class="c1">// 非正規化数</span>
      <span class="k">while</span> <span class="o">(</span><span class="n">exponent</span> <span class="o">&lt;</span> <span class="o">-</span><span class="mi">126</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">d</span><span class="o">.</span><span class="na">divideByTwo</span><span class="o">();</span>
        <span class="n">exponent</span><span class="o">++;</span>
      <span class="o">}</span>
      <span class="n">exponent</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
    <span class="o">}</span>
    <span class="n">d</span><span class="o">.</span><span class="na">discardNumberPart</span><span class="o">();</span>
    <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">22</span><span class="o">;</span> <span class="n">i</span> <span class="o">&gt;=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span><span class="o">--)</span> <span class="o">{</span>
      <span class="n">d</span><span class="o">.</span><span class="na">multiplyByTwo</span><span class="o">();</span>
      <span class="k">if</span> <span class="o">(!</span><span class="n">d</span><span class="o">.</span><span class="na">isLessThanOne</span><span class="o">())</span> <span class="o">{</span>
        <span class="n">mantissa</span> <span class="o">|=</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">i</span><span class="o">;</span>
        <span class="n">d</span><span class="o">.</span><span class="na">discardNumberPart</span><span class="o">();</span>
      <span class="o">}</span>
    <span class="o">}</span>
    <span class="c1">// 仮数部はすべて詰め終わっており、dには0.xxxxという形でそれより下の小数部が格納されている</span>
    <span class="c1">// 最近接偶数丸め</span>
    <span class="k">if</span> <span class="o">(!</span><span class="n">d</span><span class="o">.</span><span class="na">isZero</span><span class="o">())</span> <span class="o">{</span>
      <span class="n">d</span><span class="o">.</span><span class="na">multiplyByTwo</span><span class="o">();</span>
      <span class="k">if</span> <span class="o">(!</span><span class="n">d</span><span class="o">.</span><span class="na">isLessThanOne</span><span class="o">())</span> <span class="o">{</span> <span class="c1">// dは1.xxxxという形</span>
        <span class="n">d</span><span class="o">.</span><span class="na">discardNumberPart</span><span class="o">();</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">d</span><span class="o">.</span><span class="na">isZero</span><span class="o">())</span> <span class="o">{</span>
          <span class="c1">// ちょうど中間なので偶数になるように丸める</span>
          <span class="k">if</span> <span class="o">((</span><span class="n">mantissa</span> <span class="o">&amp;</span> <span class="mi">1</span><span class="o">)</span> <span class="o">==</span> <span class="mi">1</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">mantissa</span><span class="o">++;</span>
          <span class="o">}</span>
        <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
          <span class="c1">// 0より遠い側なので最近接にするために仮数部を1増やす</span>
          <span class="n">mantissa</span><span class="o">++;</span>
        <span class="o">}</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">mantissa</span> <span class="o">==</span> <span class="mh">0x800000</span><span class="o">)</span> <span class="o">{</span>
          <span class="c1">// 仮数部が23ビットを超えている</span>
          <span class="n">mantissa</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
          <span class="n">exponent</span><span class="o">++;</span>
          <span class="k">if</span> <span class="o">(</span><span class="n">exponent</span> <span class="o">&gt;</span> <span class="mi">127</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span> <span class="n">d</span><span class="o">.</span><span class="na">isNegative</span><span class="o">()</span> <span class="o">?</span> <span class="nc">Float</span><span class="o">.</span><span class="na">NEGATIVE_INFINITY</span> <span class="o">:</span> <span class="nc">Float</span><span class="o">.</span><span class="na">POSITIVE_INFINITY</span><span class="o">;</span>
          <span class="o">}</span>
        <span class="o">}</span>
      <span class="o">}</span>
    <span class="o">}</span>

    <span class="kt">int</span> <span class="n">bits</span> <span class="o">=</span> <span class="n">mantissa</span> <span class="o">|</span> <span class="o">(</span><span class="n">exponent</span> <span class="o">&lt;&lt;</span> <span class="mi">23</span><span class="o">)</span> <span class="o">|</span> <span class="o">(</span><span class="n">sign</span> <span class="o">&lt;&lt;</span> <span class="mi">31</span><span class="o">);</span>
    <span class="k">return</span> <span class="nc">Float</span><span class="o">.</span><span class="na">intBitsToFloat</span><span class="o">(</span><span class="n">bits</span><span class="o">);</span>
  <span class="o">}</span>
</code></pre></div></div>

<h1 id="今後の課題">今後の課題</h1>
<p>冒頭にも書いたが、この実装はパフォーマンスを無視している。たとえば <code class="language-plaintext highlighter-rouge">BigDecimal</code> の演算はその桁数を \({n}\) とすれば毎回 \({O(n)}\) かかってしまう。したがって\({2}\)で割って\({2}\)未満にする操作は \({O(n \log n)}\) かかるので “1e+400000” みたいな文字列を与えられると処理が現実的な時間で終わらない。</p>

<p>また、これは手抜きなのだが E 表記や p 表記の指数部は <code class="language-plaintext highlighter-rouge">Integer.parseInt(String)</code> を呼んでいるだけなのでそこでオーバーフローする。</p>

<p>今回は<a href="https://golang.org/src/strconv/atof_test.go">Goのテストケース</a>を<a href="https://github.com/hkurokawa/FloatingPointNumberParser/blob/main/src/test/java/ParserTest.java">そのままコピー</a>したが、これらの指数部が大きすぎるテストケースはコメントアウトせざるを得なかった。</p>

<p>ひとまず考えられる改善としては指数部のパース時にfloatもしくはdoubleで表せる範囲を超えていることが確実になった時点でパースを打ち切ることだろう。そのうえで無限精度の10進数型の演算についてはいくつかアルゴリズムがあるようなので、それを試してみようと思う<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>。</p>

<h1 id="感想">感想</h1>
<p>というわけで、自分で浮動小数点数のパーサを書いてみた。ハマりポイントは最近接偶数丸めで仮数部が桁あふれをするところで、これはGoのテストケースを実行するまで気付かなかったので、自分で実装してよかったと思うポイントだ。</p>

<p>あと、自分でテストケースを書こうとすると、たとえば無限精度で \({2^{-53}}\) の値が欲しくなることがある。適当な電卓では表示が打ち切られてしまうので <a href="https://keisan.casio.com/calculator">https://keisan.casio.com/calculator</a> のような有効桁数を指定できる電卓を使う必要があった。</p>

<p>最後に、面識はありませんが、きっかけとなった記事を書いてくれたSukesan1984に感謝します。この記事が読者のなにかしら参考になれば幸いです。</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>たとえばJavaの浮動小数点数リテラル表記としては正しくない <code class="language-plaintext highlighter-rouge">1.p</code> といった表記も今回は受容している。 <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>厳密には無限精度である必要はないのだが、最終的に10進数表記でどれだけの有効桁数を考慮すればよいかというのはそれほど自明ではない。今回の実装では簡単のために無限精度で扱う。 <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p>0.15625 → 0.3125 → 0.625 → 1.25 → 0.25 → 0.5 → 1.0 → 0.0 <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p>有名なのは Will Clinger, “How to Read Floating Point Numbers Accurately”, ACM SIGPLAN ‘90, pp.92–101, 1990. らしい。 <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[先日、こういうツイートを見かけた。で、さいきん浮動小数点数づいているのもあって自分も浮動小数点数の文字列表記をパースして単精度および倍精度浮動小数点数に変換するパーサを書いてみた。]]></summary></entry><entry><title type="html">浮動小数点数の二段階丸め誤差</title><link href="https://hydrakecat.net/2020/10/18/fma.html" rel="alternate" type="text/html" title="浮動小数点数の二段階丸め誤差" /><published>2020-10-18T01:21:55+00:00</published><updated>2020-10-18T01:21:55+00:00</updated><id>https://hydrakecat.net/2020/10/18/fma</id><content type="html" xml:base="https://hydrakecat.net/2020/10/18/fma.html"><![CDATA[<p>さいきん<a href="https://techbookfest.org/product/6018416139829248?productVariantID=4928943930998784">『浮動小数点数小話』</a>という同人誌を読んでFMA (Fused Multiply-Add)の二段階丸め誤差（double rounding error）について色々と知る機会があったのでまとめておく。ついでにFMAに関するOpenJDKのバグっぽい挙動を見つけたのでそれも併せて記しておく。</p>

<h1 id="fma-fused-multiply-addとは">FMA (Fused Multiply-Add)とは</h1>
<p>FMAは以下のような演算のことを呼ぶ。</p>

\[fma(x, y, z) = xy + z\]

<p>この演算自体は行列の乗算やベクトルの内積の計算でよく現れるものであるが、通常の浮動小数点数の乗算と加算を別々に行うと誤差が出るので一度の演算で正確な値を算出したいときに用いる。たとえばC言語（C99）では <code class="language-plaintext highlighter-rouge">fma</code>、<code class="language-plaintext highlighter-rouge">fmaf</code>、<code class="language-plaintext highlighter-rouge">fmal</code>という3つの関数が導入されているらしい。</p>

<h1 id="fmaの実装における二段階丸め誤差">FMAの実装における二段階丸め誤差</h1>
<p>FMAはターゲットとなるCPUのアーキテクチャがFMA命令をサポートしていればその命令を直接呼び出すことで（バグがなければ）誤差なく求める答えを得ることができる。一方でそうでない場合はソフトウェア的にFMAをエミュレートしてやる必要がある。『浮動小数点数小話』では、高精度の型で演算して低精度の型に変換する実装をよくある実装ミスとして挙げている。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">static</span> <span class="nf">float</span><span class="o">(</span><span class="kt">float</span> <span class="n">x</span><span class="o">,</span> <span class="kt">float</span> <span class="n">y</span><span class="o">,</span> <span class="kt">float</span> <span class="n">z</span><span class="o">)</span> <span class="o">{</span>
  <span class="k">return</span> <span class="o">(</span><span class="kt">float</span><span class="o">)((</span><span class="kt">double</span><span class="o">)</span> <span class="n">x</span> <span class="o">*</span> <span class="o">(</span><span class="kt">double</span><span class="o">)</span> <span class="n">y</span> <span class="o">+</span> <span class="o">(</span><span class="kt">double</span><span class="o">)</span> <span class="n">z</span><span class="o">)</span>
<span class="o">}</span>
</code></pre></div></div>

<p>このコードは一見すると正しそうに見える。むかし授業で習った数値誤差の話でも計算過程で有効桁数の2倍を担保していれば計算で誤差が入らなかったような気がするし、実際、単精度（float）の桁数は24で倍精度（double）の桁数は53なので十分と思うかもしれない。しかし、これは無限精度で計算してfloatに一度だけ丸めたものとは結果が異なってしまう。</p>

<p>以下のコードを実行してみよう。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">Test</span> <span class="o">{</span>
  <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
    <span class="kt">float</span> <span class="n">x</span> <span class="o">=</span> <span class="mh">0x1</span><span class="o">.</span><span class="na">fffffep23f</span><span class="o">;</span>
    <span class="kt">float</span> <span class="n">y</span> <span class="o">=</span> <span class="mh">0x1</span><span class="o">.</span><span class="mo">000004</span><span class="n">p28f</span><span class="o">;</span>
    <span class="kt">float</span> <span class="n">z</span> <span class="o">=</span> <span class="mh">0x1</span><span class="o">.</span><span class="na">fep5f</span><span class="o">;</span>
    <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">printf</span><span class="o">(</span><span class="s">"%a\n"</span><span class="o">,</span> <span class="n">fma</span><span class="o">(</span><span class="n">x</span><span class="o">,</span> <span class="n">y</span><span class="o">,</span> <span class="n">z</span><span class="o">));</span> <span class="c1">// 0x1.000004p52</span>
  <span class="o">}</span>

  <span class="kd">public</span> <span class="kd">static</span> <span class="kt">float</span> <span class="nf">fma</span><span class="o">(</span><span class="kt">float</span> <span class="n">a</span><span class="o">,</span> <span class="kt">float</span> <span class="n">b</span><span class="o">,</span> <span class="kt">float</span> <span class="n">c</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="o">(</span><span class="kt">float</span><span class="o">)((</span><span class="kt">double</span><span class="o">)</span> <span class="n">a</span> <span class="o">*</span> <span class="o">(</span><span class="kt">double</span><span class="o">)</span> <span class="n">b</span> <span class="o">+</span> <span class="o">(</span><span class="kt">double</span><span class="o">)</span> <span class="n">c</span><span class="o">);</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>出力される結果は <code class="language-plaintext highlighter-rouge">0x1.000004p52</code> だが、これは本来の結果とは異なる。無限精度で計算すると <code class="language-plaintext highlighter-rouge">x * y + z</code> は <code class="language-plaintext highlighter-rouge">0x1.000002fffffffcp52</code> となり、これをfloatに丸めると <code class="language-plaintext highlighter-rouge">0x1.000002p52</code> となるはずだ。</p>

<p>この差がどこに由来するか考えると、じつは <code class="language-plaintext highlighter-rouge">+ (double) c</code> の加算においてdouble型に丸めたときと、そこからさらに <code class="language-plaintext highlighter-rouge">(float)</code> でfloat型に丸めたときとで二段階に丸めていることが問題であることがわかる。つまり <code class="language-plaintext highlighter-rouge">0x1.000002fffffffcp52</code> -(double丸め)→ <code class="language-plaintext highlighter-rouge">0x1.0000030000000p52</code> -(float丸め)→ <code class="language-plaintext highlighter-rouge">0x1.000004p52</code> となっている。これを図示するとつぎのようになる。</p>

<figure class="figure-image figure-image-fotolife" title="二段階丸め誤差"><img src="/assets/images/doubleroundingerror.png" /><figcaption>二段階丸め誤差</figcaption></figure>

<p>図の数直線上の短い直線はdoubleで表せる実数を、長い直線はfloatで表せる実数を示している。無限精度での答え <code class="language-plaintext highlighter-rouge">0x1.000002fffffffcp52</code> はfloatの観点からは <code class="language-plaintext highlighter-rouge">0x1.000002p52</code> により近いが、doubleに丸めるときに <code class="language-plaintext highlighter-rouge">0x1.0000030000000p52</code> になり、さらにfloatに丸めるときは最近接偶数丸めにより <code class="language-plaintext highlighter-rouge">0x1.000004p52</code> になってしまう。なお、floatの仮数部は23桁なので <code class="language-plaintext highlighter-rouge">0x1.000002p52</code> の仮数部の最下位1ビットは <code class="language-plaintext highlighter-rouge">1</code> となることに注意。</p>

<h1 id="openjdkのfma実装のバグ">OpenJDKのFMA実装のバグ</h1>
<p>ついでなので、Java 9から追加された <code class="language-plaintext highlighter-rouge">Math.fma(float, float, float)</code> でどうなるかやってみる。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">Test</span> <span class="o">{</span>
  <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
    <span class="kt">float</span> <span class="n">x</span> <span class="o">=</span> <span class="mh">0x1</span><span class="o">.</span><span class="na">fffffep23f</span><span class="o">;</span>
    <span class="kt">float</span> <span class="n">y</span> <span class="o">=</span> <span class="mh">0x1</span><span class="o">.</span><span class="mo">000004</span><span class="n">p28f</span><span class="o">;</span>
    <span class="kt">float</span> <span class="n">z</span> <span class="o">=</span> <span class="mh">0x1</span><span class="o">.</span><span class="na">fep5f</span><span class="o">;</span>
    <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">printf</span><span class="o">(</span><span class="s">"%a\n"</span><span class="o">,</span> <span class="nc">Math</span><span class="o">.</span><span class="na">fma</span><span class="o">(</span><span class="n">x</span><span class="o">,</span> <span class="n">y</span><span class="o">,</span> <span class="n">z</span><span class="o">));</span> <span class="c1">// 0x1.000002p52</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>こんどは <code class="language-plaintext highlighter-rouge">0x1.000002p52</code> と期待する結果が出力された。めでたしめでたし、となりたいところだが、じつはこの結果は環境依存である。もし使っているCPUアーキテクチャがFMA命令を実装していない場合は、おそらく現時点でのOpenJDKベースのJava（たとえばOracle Java 11.0.8）では <code class="language-plaintext highlighter-rouge">0x1.000004p52</code> と出力されるはずだ。試みにVM引数で <code class="language-plaintext highlighter-rouge">-XX:-UseFMA</code> と付けてみよう。おそらく <code class="language-plaintext highlighter-rouge">0x1.000004p52</code> という不正確な結果が出力される。</p>

<p>これはOpenJDKの現時点での実装が上で指摘されていたようにdouble型で計算してfloatに戻すという実装になっている（<a href="https://github.com/openjdk/jdk/blob/1c2754bfe3294fb3c80defe5479bdd85b0d07e29/src/java.base/share/classes/java/lang/Math.java#L1858">GitHubの該当個所</a>）ためである。</p>

<blockquote>
  <p>Since the double format moreover has more than (2p + 2) precision bits compared to the p bits of the float format, the two roundings of (a * b + c), first to the double format and then secondarily to the float format, are equivalent to rounding the intermediate result directly to the float format.</p>
</blockquote>

<p>コメントにはこのように書いてあるが、この <code class="language-plaintext highlighter-rouge">(2p + 2)</code> だから大丈夫という前提が間違っているように思える。OpenJDKには問題を報告しておいた（<a href="https://bugs.openjdk.java.net/browse/JDK-8253409">https://bugs.openjdk.java.net/browse/JDK-8253409</a>）が、priorityも低いのですぐには直らないかもしれない（2021-03-22 追記：修正されたらしい。バックポートもされたらしいので最新版では直っているはず。ちなみに修正方法はBigDecimalを使うというもので面白みはないけれど、そうだろうなという感じでした）。</p>

<h2 id="奇数丸めをつかった実装">奇数丸めをつかった実装</h2>
<p>さて、二段階丸め誤差を出さないFMAのソフトウェア実装はどうすればよいだろうか。前出の『浮動小数点数小話』によれば <a href="https://ieeexplore.ieee.org/document/4358278">“Emulation of a FMA and Correctly Rounded Sums: Proved Algorithms Using Rounding to Odd”</a> という論文に奇数丸めを利用した方法が紹介されているらしい。</p>

<p>これは中間計算を行う際に奇数丸めを常にして最終的な解は通常の最近接偶数丸めをすることで二段階丸め誤差をなくすという話である。実際にさきほどの例で考えると、無限精度での答え <code class="language-plaintext highlighter-rouge">0x1.000002fffffffcp52</code> をdoubleへ奇数丸めすると <code class="language-plaintext highlighter-rouge">0x1.000002fffffffp52</code> となるので、そこからfloatへ最近接偶数丸めをすれば <code class="language-plaintext highlighter-rouge">0x1.000002p52</code> となる。</p>

<p>これがうまくいくのは不思議だが、直観的には最近接偶数丸めで偶数側に寄りがちなのを奇数丸めでバランスを取るということだろう。あるいは、こうも考えることができる。奇数丸めをするということは、doubleの<code class="language-plaintext highlighter-rouge">0x1.000002fffffffp52</code>が表す範囲は <code class="language-plaintext highlighter-rouge">[0x1.000002fffffffp52, 0x1.0000030000000p52)</code> である。これによって本来の値が <code class="language-plaintext highlighter-rouge">0x1.0000030000000p52</code> より左ならfloatの <code class="language-plaintext highlighter-rouge">0x1.000002p52</code> に、右ならfloatの <code class="language-plaintext highlighter-rouge">0x1.000004p52</code> に丸められるので、最後に最近接偶数丸めをするときに <code class="language-plaintext highlighter-rouge">0x1.0000030000000p52</code> を飛び越すことがない。</p>

<figure class="figure-image figure-image-fotolife" title="奇数丸め"><img src="/assets/images/oddrounding.png" /><figcaption>奇数丸め</figcaption></figure>

<p>さて、奇数丸めをどう実装するかだが、論文によればDekkerのエラーなし加算器（error-free adder）を使うことでソフトウェア的に奇数丸めを実装できる<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>。これはつぎのようなアルゴリズムで \({a + b}\) の結果floatに丸めたものを \({s}\) に格納しつつ、本当の値との誤差を \({r}\) で計算できる。そうするとオーバーフローやアンダーフローが発生しなければ \({a + b}\) と \({s + r}\) が厳密に一致することになる。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">float</span> <span class="n">s</span> <span class="o">=</span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span><span class="o">;</span>
<span class="kt">float</span> <span class="n">z</span> <span class="o">=</span> <span class="n">s</span> <span class="o">-</span> <span class="n">a</span><span class="o">;</span>
<span class="kt">float</span> <span class="n">r</span> <span class="o">=</span> <span class="n">b</span> <span class="o">-</span> <span class="n">z</span><span class="o">;</span>

<span class="c1">// a + b = s + r</span>
</code></pre></div></div>

<p>あとは <code class="language-plaintext highlighter-rouge">s</code> の仮数部の偶奇と <code class="language-plaintext highlighter-rouge">r</code> の符号を見て奇数丸めを行えばよい。コードにするとこんな感じだろうか。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">public</span> <span class="kd">static</span> <span class="kt">float</span> <span class="nf">oddRoundedAdd</span><span class="o">(</span><span class="kt">float</span> <span class="n">a</span><span class="o">,</span> <span class="kt">float</span> <span class="n">b</span><span class="o">)</span> <span class="o">{</span>
    <span class="kt">float</span> <span class="n">s</span> <span class="o">=</span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span><span class="o">;</span>
    <span class="kt">float</span> <span class="n">z</span> <span class="o">=</span> <span class="n">s</span> <span class="o">-</span> <span class="n">a</span><span class="o">;</span>
    <span class="kt">float</span> <span class="n">r</span> <span class="o">=</span> <span class="n">b</span> <span class="o">-</span> <span class="n">z</span><span class="o">;</span>
    <span class="kt">int</span> <span class="n">sx</span> <span class="o">=</span> <span class="nc">Float</span><span class="o">.</span><span class="na">floatToIntBits</span><span class="o">(</span><span class="n">s</span><span class="o">);</span>
    <span class="k">if</span> <span class="o">((</span><span class="n">sx</span> <span class="o">&amp;</span> <span class="mi">1</span><span class="o">)</span> <span class="o">==</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">r</span> <span class="o">!=</span> <span class="mf">0.0</span><span class="o">)</span> <span class="o">{</span>
      <span class="n">s</span> <span class="o">=</span> <span class="nc">Float</span><span class="o">.</span><span class="na">intBitsToFloat</span><span class="o">(</span><span class="n">sx</span> <span class="o">+</span> <span class="o">(</span><span class="n">s</span> <span class="o">&lt;</span> <span class="mf">0.0</span> <span class="o">^</span> <span class="n">r</span> <span class="o">&gt;</span> <span class="mf">0.0</span> <span class="o">?</span> <span class="mi">1</span> <span class="o">:</span> <span class="o">-</span><span class="mi">1</span><span class="o">));</span>
    <span class="o">}</span>
    <span class="k">return</span> <span class="n">s</span><span class="o">;</span>
  <span class="o">}</span>
</code></pre></div></div>

<p>以上をまとめて、論文に書かれているFMAのソフトウェア実装をJavaで書くとつぎのようになる。なおfloatのエラーなし乗算はdoubleを使って手を抜いている。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">public</span> <span class="kd">static</span> <span class="kt">float</span> <span class="nf">fma</span><span class="o">(</span><span class="kt">float</span> <span class="n">a</span><span class="o">,</span> <span class="kt">float</span> <span class="n">b</span><span class="o">,</span> <span class="kt">float</span> <span class="n">c</span><span class="o">)</span> <span class="o">{</span>
    <span class="kt">float</span> <span class="n">uh</span> <span class="o">=</span> <span class="o">(</span><span class="kt">float</span><span class="o">)</span> <span class="o">((</span><span class="kt">double</span><span class="o">)</span> <span class="n">a</span> <span class="o">*</span> <span class="o">(</span><span class="kt">double</span><span class="o">)</span> <span class="n">b</span><span class="o">);</span>
    <span class="kt">float</span> <span class="n">ul</span> <span class="o">=</span> <span class="o">(</span><span class="kt">float</span><span class="o">)</span> <span class="o">((</span><span class="kt">double</span><span class="o">)</span> <span class="n">a</span> <span class="o">*</span> <span class="o">(</span><span class="kt">double</span><span class="o">)</span> <span class="n">b</span> <span class="o">-</span> <span class="n">uh</span><span class="o">);</span>
    <span class="kt">float</span> <span class="n">th</span> <span class="o">=</span> <span class="n">c</span> <span class="o">+</span> <span class="n">uh</span><span class="o">;</span>
    <span class="kt">float</span> <span class="n">tl</span> <span class="o">=</span> <span class="n">uh</span> <span class="o">-</span> <span class="o">(</span><span class="n">th</span> <span class="o">-</span> <span class="n">c</span><span class="o">);</span>
    <span class="k">return</span> <span class="n">th</span> <span class="o">+</span> <span class="n">oddRoundedAdd</span><span class="o">(</span><span class="n">tl</span><span class="o">,</span> <span class="n">ul</span><span class="o">);</span>
  <span class="o">}</span>
</code></pre></div></div>

<p>ここで <code class="language-plaintext highlighter-rouge">th</code> と <code class="language-plaintext highlighter-rouge">tl</code> は <code class="language-plaintext highlighter-rouge">c</code> と <code class="language-plaintext highlighter-rouge">uh</code> のエラーなし加算の結果である。上記の実装で先ほどの例を実行すると正しく<code class="language-plaintext highlighter-rouge">0x1.000002p52</code>が出力されるので、たぶん合っているだろう。なお実際に実装する場合はオーバーフローやアンダーフローも気にする必要があるので、double版のFMAのようにBigDecimalでやってしまう方がらくかもしれない。</p>

<p>ところで、この実装のテストケースを作るのはすごく大変そうだ。floatだとちょうど結果が0.5ULPだけ離れたあたりになるようにサンプルをつくるのが面倒なのだ。doubleなら \({a = 1-2^{-27}, b = 1+2^{=27}, c = 2^{-150}}\)とでもすれば簡単につくれるのだけれど、floatは仮数部の桁数が奇数なのですこしめんどうくさい。</p>

<h1 id="感想">感想</h1>
<p>浮動小数点はやはりおもしろい。やはり人間の直感とかなりずれるあたりがおもしろさだろう。floatの四則演算はdoubleを中間表現に使えば誤差なく計算できる、というのは定説であるし、実際に証明もされているが、FMAのような複数の四則演算の組み合わせになると成り立たない、というのは一見不思議にみえる。誤差のない演算を何回繰り返しても誤差が積もらない気がするからだ。しかし、よく考えるとfloat同士の演算をした結果はfloatで表せるとは限らず、さらにその数とfloatとの演算は、すでにfloat同士の四則演算の範囲を出ていると考えれば、「float同士の四則演算は」という部分が成り立たないので、これは矛盾しないわけだ。一方で、doubleは最終的な結果を出すのに十分な表現力を有しているので、途中の演算では奇数丸めを採用するというただそれだけで結果が保証されるというのもおもしろい。Dekker-Knuthのエラーなし加算器も不勉強にして知らなかったが、これもだいぶ興味深い。機会を見つけてもうすこし調べてみたいと思っている。</p>

<p>最後に、今回の記事は内容の多くを<a href="https://techbookfest.org/product/6018416139829248?productVariantID=4928943930998784">『浮動小数点数小話』</a>に依っている。タイトルに惹かれてなんの気なしに購入したのだが、たいへん面白かった。著者の荒田さん、ありがとうございます。ここで謝意を表して伝わるか分かりませんが、執筆いただいて感謝いたします。なお、この記事に間違いがあった場合は当然ながら全面的に自分の責任です。気軽にご指摘ください。</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p><a href="https://link.springer.com/article/10.1007/BF01397083">https://link.springer.com/article/10.1007/BF01397083</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[さいきん『浮動小数点数小話』という同人誌を読んでFMA (Fused Multiply-Add)の二段階丸め誤差（double rounding error）について色々と知る機会があったのでまとめておく。ついでにFMAに関するOpenJDKのバグっぽい挙動を見つけたのでそれも併せて記しておく。]]></summary></entry><entry><title type="html">中年プログラマの競プロ事始</title><link href="https://hydrakecat.net/2020/07/26/middle-age-competitive-programming.html" rel="alternate" type="text/html" title="中年プログラマの競プロ事始" /><published>2020-07-26T06:25:53+00:00</published><updated>2020-07-26T06:25:53+00:00</updated><id>https://hydrakecat.net/2020/07/26/middle-age-competitive-programming</id><content type="html" xml:base="https://hydrakecat.net/2020/07/26/middle-age-competitive-programming.html"><![CDATA[<h1 id="これはなに">これはなに</h1>

<p>自分がここ2年ほど趣味として競技プログラミング（いわゆる競プロ）をやった経緯と感想です。いわゆるプログラマの定年と呼ばれる35歳を過ぎてから始めたのですが、思ったよりも楽しめました。自分のようなシニアと呼ばれるプログラマが競プロに興味を持ってくれたらいいなと思って書きました。</p>

<!-- more -->

<h1 id="競技プログラミングとは">競技プログラミングとは</h1>

<p>競技プログラミング（以後、競プロ）は、プログラミングをして競うコンテストです。コンテストはたいていオンラインで毎週のように開かれており、誰でも参加できます。形式としては、与えられた時間内にいくつかの問題を解くコードを提出して、その正解数と提出までにかかった時間を競うというものです。たいていは、コードの実行時間および使用メモリに制限があり、その制限内で実行できるコードを書く必要があります。またコードが正解かどうかは出題者が用意したテストケースをパスするかどうかで判定されます。</p>

<p>多くのコンテストでは結果に応じて参加者のレーティングが変化し、参加者はレーティングを上げるために鎬を削る、という構図になっています。</p>

<h1 id="始めた経緯と2年の振り返り">始めた経緯と2年の振り返り</h1>

<p>2年ほどまえに外資系の会社に転職しようとして始めました。その会社は面接対策用のドキュメントをリクルータが共有してくれるのですが、その中にコーディング面接の対策として<a href="https://www.topcoder.com/">Topcoder</a>のEasyの問題をウォームアップとして解くとよい、とありました。</p>

<p>Topcoderは海外の競プロコンテストですが、その過去問は誰でも見れます。ただ、自分は何を勘違いしたか、レーティングを上げてDiv. 1と呼ばれる上位のコンテストに参加できるようにならないとDiv. 1の過去問は見れないと思いこんでしまい、とりあえずレーティングを上げようと参加しました。</p>

<p>結果的にはその面接はうまくいかなかったのですが、競プロのおもしろさに気付き、また将来コーディング面接の対策にもなるかなと思って始めた次第です。</p>

<p>競プロを始めようと思い立って、まず選ばなければならないのがどのコンテストに参加するかです。Topcoderをそのままやってもよかったのですが、サイトが分かりにくいこと、開催時刻が日本時間に優しくないことにためらいがありました。</p>

<p>定期的にコンテストを開催しているサイトでは次のものがありましたが、この中ではAtCoderが日本時間に優しく、また素人目での判断ですが、いい問題が多いように思ったので、AtCoderに参加することに決めました。問題文が日本語かつ短めなので、とっつきやすそうに見えたというのもあります。</p>

<ul>
  <li><a href="https://www.topcoder.com/">Topcoder</a></li>
  <li><a href="https://atcoder.jp/">AtCoder</a></li>
  <li><a href="https://codeforces.com/">Codefoces</a></li>
</ul>

<p>ほかにも<a href="https://codingcompetitions.withgoogle.com/codejam">Google Code Jam</a>や<a href="https://icfpcontest2020.github.io/#/">ICFPC</a>のように年に1回開催されるもの、1週間などの長期間かけてヒューリスティクスな問題を解くいわゆるマラソン形式のコンテストなどもあります。</p>

<p>さて、AtCoderに参加することに決めたわけですが、最初はAtCoder Beginner Contest（ABC）とAtCoder Regular Contest（ARC）の違いも分かっておらず、初めて参加したコンテストがARCで4問中1問しか解けなくて自分はこんなにできないのかと落ち込みました<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>。</p>

<p>コンテストの違いが分かってからはABCに狙いを定め、最低月に1度は参加するという目標を立ててコンテストに参加しました。ABCは初級者向けのコンテストで現在はAからFまで6問の問題がほぼ難易度順に並んでいます。プログラマになって10年ほど働いたシニアな方だったら、おそらく問題AとBはすぐに解けて、問題Cはなんとか解けるくらい、Dは時間内だと厳しい、という感じではないでしょうか。</p>

<p>自分も当初はそのような感じでしたが、過去問を解いて勉強しているうちに、半年ほどで問題Dはほぼ見た途端に解けるようになり、そのあたりで水色レート（1200）に、最終的に2年かけて青レート（1600）に到達しました。2年間のレーティングの推移はつぎのようになります<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>。</p>

<p><img src="/assets/images/atcoder_rating.png" alt="AtCoderレーティングの推移" /></p>

<p>自分の勉強法ですが、まず蟻本と呼ばれる<a href="https://www.amazon.co.jp/dp/B00CY9256C">『プロラミングコンテストチャレンジブック』</a>を買い、初級編まで読みました。出てくるアルゴリズムは自分で実装して、自分用のライブラリを作っておくといいと思います。</p>

<p>あとは<a href="https://kenkoooo.com/atcoder#/table/">AtCoder Problems</a>というサイトを利用してひたすら過去問を解いて、解けない問題は解説を読むということの繰り返しです。なお、AtCoder Problemsはたいへんお世話になりました。過去のコンテスト問題一覧だけでなく、難易度で問題を絞ることもできるので便利です。他にも何日連続でAC（正解を提出することです）したかを表示してくれる<a href="https://kenkoooo.com/atcoder/#/user/hkurokawa">Streakという機能</a>があるので励みになります。</p>

<p>[asin:B00CY9256C:detail]</p>

<p>自分の場合は最初の壁がABCの問題Dだったので、最初は毎朝ABCの問題Dを1問解くことを目標にしました。数時間かけても解けないということはザラで、そういうときは解けるまで数日かけたりもしていました。ちまたの競プロの勉強法を見ると、簡単な問題をとにかく大量に解くことや、20分かけて解けなかったら解説を見ろ、というものが多いですが、自分はむかしから1つの問題を長い時間をかけて解くのが好きなので、効率は無視してそういう方法をとっていました。このあたりは向き不向き、好き嫌いが分かれるところだと思います。趣味なので、好きな勉強法でたのしく続けるのがよいでしょう。</p>

<p>問題Dがあっさり解けるようになってからは、問題EやF、あるいはそれと同程度の難易度の過去問を解いていました。また、蟻本の中級編を読み進めました。</p>

<p>それに加えて、解いた問題の解法や学んだ知識や証明を<a href="https://scrapbox.io/hkurokawa-cp/">Scrapboxにまとめていました</a>。これは自分の考えを整理するうえでもとてもよかったですし、ときには記事を書いている最中に自分の証明に穴があることに気付いたりもしました。もしよければこういう記事を書いてみることをおすすめします。</p>

<p>他には制限時間内に解く練習をするために<a href="https://kenkoooo.com/atcoder#/contest/recent">AtCoder Problemsのバーチャルコンテスト</a>も利用しました。これは誰でも過去問をいくつか組み合わせてコンテストを作れる機能なのですが、有志によって「あさかつ」という名前のコンテスト（7/26は<a href="https://kenkoooo.com/atcoder#/contest/show/5de03ba4-f9b5-4088-b145-bdb7f63b0ab9">これ</a>）が毎朝開かれています。どうも制限時間内に解くのが苦手だと思ったので、時間内に解く練習にこのコンテストにしばらく参加していました。すでに知っている問題が出ることもありましたが、分かっている解法をさっとコードに落としこむ訓練になりました。</p>

<h1 id="競プロをやってよかったこと">競プロをやってよかったこと</h1>

<p>箇条書きに書いてみます。</p>

<ol>
  <li>問題を考えることがたのしい</li>
  <li>ちょうどよい暇つぶしになる</li>
  <li>容赦なく現実を突き付けられる</li>
</ol>

<h2 id="1-問題を考えることがたのしい">1. 問題を考えることがたのしい</h2>
<p>そのまんまですが、自分は競プロのたのしさの大部分は問題を考察すること、解くことのたのしさだと思います。</p>

<p>これは自分の中のぼんやりしたイメージなのですが、自分が問題を解くときはこんなふうです。まず、問題というのはじっと見つめていても解けることはありません。手当たり次第に解法を試すのも（すくなくとも自分は）うまくいきません。そのかわり、シンプルな入力についてどうなるか手で確かめる、2次元の問題だったら1次元の場合を考えてみる、制約を外す、あるいは制約を足す、似た問題に変換できないか考える、問題を分割する、などなどといったことをします。</p>

<p>そのようにさまざまな角度から眺めると徐々に問題の輪郭がわかってきます。それはなにかよく分からない物体の表面を撫でている感じに近いでしょう。ずっと撫でていると次第に輪郭がわかっていき、いくつかの引っかかりが見つかります。そしてその引っかかりをうまく掴むことができれば、問題を理解することができ、そこからおのずと解法も導かれます。</p>

<p>この、引っかかりを掴む感覚がとても気持ちよい。これは数学パズルでも同じですが、問題の芯の部分、背骨をがっしり捉えることが大事で、なんとなく、こういう感じで解けるかなと解法ありきで考えると（すくなくとも自分は）必ず間違えるのがおもしろいところです。</p>

<p>さらに競プロの場合はアルゴリズムに落としこむという点で計算量やメモリの制約が効いてきます。たとえば、問題のサイズが小さいときは単純に3重ループで解ける問題があったとしても、問題のサイズが大きくなると飛躍的に難しくなる。同じ問題でも制約によって解き方がガラリと変わるのは競プロの魅力の1つでしょう。</p>

<h2 id="2-ちょうどよい暇つぶしになる">2. ちょうどよい暇つぶしになる</h2>

<p>これは自分だけかもしれませんが、歩いているときや電子レンジを待っているとき、温泉につかっているときに、なにか考えることがほしくなります。たとえば仕事で考えている問題だったり詰将棋でもいいのですが、競プロの問題というのは、問題を覚えることは容易で考えるのに時間がかかるという点で、まさにそのような暇つぶしにうってつけです。</p>

<p>コードを書いて確認するにはコンピュータが必要ですが、考えるだけなら紙とペンがあれば十分なので、たとえば旅行中のちょっとした時間や長距離フライトの機内で考えるのにもよいでしょう。</p>

<p>とある新潟の雪深い温泉で露天風呂につかりながら競プロの問題が解けるまで出ないぞと決心してずっと考えていたことがあるのですが、世界に自分しかいないような感じがしてとてもたのしかったのを覚えています。なお、解けたと思って部屋に戻ってから確認したら全然ダメだったのもいい思い出です。</p>

<h2 id="3-容赦なく現実を突き付けられる">3. 容赦なく現実を突き付けられる</h2>
<p>現実は非情です。コンテストでうまくいかなければ、順位は悪くなりレーティングも下がります。多少の運もありますが、基本的には自分の実力通りの結果しか出ません。</p>

<p>時間が足りなくて焦ることもあります。ABCは100分ですが、6問すべてを解こうとすると、ほぼ寄り道している暇はありません。いつもはすんなり解けるはずの問題Cでつっかかって、そのつっかかったことに焦るということもあります。</p>

<p>これは10年くらいプログラマをやっているとちょっと新鮮です。というのは、それくらい経験を積むと、よくいえば総合力で対応、悪くいえば誤魔化しが効くようになるのです。シンプルに自分の一部の能力を評価されるということは減り、ほかの能力や経験でカバーすることを覚えていきます。これは悪いことではありませんが、ともすると自分ができる方なんじゃないかと錯覚してしまいます。</p>

<p>しかし競プロのコンテストに出ると、競プロという一点において自分よりはるかに出来る人たちを目の当たりにして軽く自信を喪失します。そういった経験を定期的にするということは、シニアな人ほど大事で得難いものであると思います。</p>

<p>コンテストに失敗した夜はとても悔しくて、なぜ解けなかったんだろうと歯噛みする経験も数えきれませんでしたが、それはよい刺激であったと思います。競プロに限らず、ある程度シニアな人はそういう機会を意識的に持つとよいかもしれません。</p>

<h1 id="合わなかったところ">合わなかったところ</h1>

<p>とくに自分にとって競プロが合わないというか、むずかしいなと思うことはつぎのものでした。</p>

<ol>
  <li>参加時間帯が厳しい</li>
  <li>早解きに興味が持てない</li>
</ol>

<h2 id="1-参加時間帯が厳しい">1. 参加時間帯が厳しい</h2>
<p>ABCは土日どちらかの21:00 - 22:40というスケジュールですが、家庭持ちにとってはやや辛い時間です。外食すると21:00に帰ってくることは難しいですし、家で食べるとしても自分は家人に料理してもらっているので夕食の時間を調整してもらわなければなりません。</p>

<p>このあたりは家族の理解を得て、月に1回程度という頻度だったのもあってなんとかなりましたが、一人暮らしの方がもっと楽だろとう思います。</p>

<p>なお<a href="https://codeforces.com">Codeforces</a>のコンテストは23:30に始まることもあるので、夜型の人はそちらが都合いいかもしれません。</p>

<h2 id="2-早解きに興味が持てない">2. 早解きに興味が持てない</h2>
<p>これは、ほぼ愚痴ですが、自分はあまり早解きは得意ではありません。頭の回転が遅いのは確かなので単純に不得手のようです。自分はどちらかというと1週間でもずっと同じ問題を考えるのが好きで、そのせいか、あまり早解きのテクニックを磨くのに興味を持てませんでした。また、数学的に自分の解法が最適であることを証明しようとして、むやみに時間をかけてしまうこともありました。</p>

<p>これはとくに問題セットが易しめのコンテストではかなり不利です。とはいえ、趣味でやっているものなので、自分はこういうスタイルで楽しもうと割り切って、早解きは諦めて参加していました。</p>

<p>なお、いままで参加したことはほぼないのですが、マラソンコンテストやヒューリティクスコンテストと呼ばれるタイプのコンテストだと、また事情は変わってくるかもしれません。</p>

<h1 id="よくある想定質問と回答">よくある想定質問と回答</h1>
<p>最後に、シニアプログラマから受けそうな競プロについての想定質問とそれに対する自分の回答を載せます。なかには実際に受けたことがある質問も入っていますし、自分が競プロをやる前に抱いていた疑問もあります。</p>

<h2 id="q-コーディング面接で有利ですか">Q. コーディング面接で有利ですか？</h2>
<p>端的に言えばYesです。</p>

<p>経緯で、とある外資系の企業のコーディング面接のために始めたと書きましたが、その企業からは1年後にまた受けないかというお誘いを受け、2回目では通りました。この会社はコーディング面接の比重が高く、面接では競プロの知識と技術は有利に働いたと思います。とはいえ、AtCoderのレーティングでいえば、水色くらいあれば十分だと思います。</p>

<p>また、自分は個人的な信条から利用していませんが、コーディング面接に特化した問題を提供しているサービスもあります。競プロをやるよりそっちの方が効率的だといわれたらそうかもしれません。</p>

<p>ただ1つ言えることは、コーディング面接で出そうな問題を片っ端から解いて解法を暗記して、運良く面接に通ったとしても、それはプログラマとしては得るものが少ないだろうということです。</p>

<p>それよりも、問題をどう理解するか、アルゴリズムをどう問題に適用するかといったことを競プロを通して学べれば、コーディング面接に通ること以上の糧になるでしょう。老害っぽい物言いですが、コーディング面接に通ることだけを目標に競プロをやるのはややもったいないと思います。</p>

<h2 id="q-業務で役に立ちますか">Q. 業務で役に立ちますか？</h2>
<p>直接的に役立つことはそれほど頻繁にはないでしょう。前職では、たまたま競プロの知識が直接役立てられそうなタスクがあったのですが、とてもいい経験になりました。</p>

<p>その経験をまとめた記事：<a href="https://medium.com/mixi-developers/%E5%8B%95%E7%9A%84%E8%A8%88%E7%94%BB%E6%B3%95%E3%81%AB%E3%82%88%E3%82%8Bdvd%E3%81%AE%E3%83%87%E3%82%A3%E3%82%B9%E3%82%AF%E5%88%86%E5%89%B2%E3%81%AE%E6%94%B9%E5%96%84-438875f65528">動的計画法によるDVDのディスク分割の改善</a></p>

<p>とはいえ、こういうタスクが頻繁にあるかというと、仕事にもよりますがふつうはそんなにないでしょう。とくに短時間のコンテストに出てくるような問題はどちらかというと数学パズルに近く、業務で扱う問題とはやや方向性が違うといえます。</p>

<p>ではまったく役に立たないかというと、そんなことはありません。たとえばアルゴリズムをいくら学んでも、それを問題に適用できるかどうかはまた別の話です。競プロをやることによって、そのあたりの嗅覚は鍛えられるように思います。また、頭に浮かんだアルゴリズムをコードに落としこむところも、バグなく実装するにはある程度の慣れが必要です。たとえば二分探索は誰でもアルゴリズムは知っていますが、バグなくシンプルに書くにはちょっとコツがいります。しかし、ほとんどの教科書はアルゴリズムの概要や疑似コードだけ書いて細かい実装の留意点までは書いていません。これも実際に競プロに取り組んで始めてわかったことでした。</p>

<p>競プロと業務の関係は、将棋で例えると詰将棋と実戦の違いのようなものかもしれません。詰将棋だけ強くても実戦が強いとは限りませんが、それでも実戦で間違いなく役立つ能力です。競プロもすぐに役立つわけではありませんが、ある程度やると業務でも「あ、これはこういうアルゴリズムが適用できそう」という問題が見えるようになります。そういうチャンスを得やすくなるという意味でも有用なのではないかと思います。</p>

<p>なお余談ですが業務ではNP困難かどうか見抜くことがたまに求められます。言い換えると、ある問題の最適解を現実的な時間内に得る方法があるか、それともヒューリスティクスに近似解しか得られないのかを判断する必要に迫られることがあります。これは競プロのコンテストではあまり求められないことの1つな気がします<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>。</p>

<p>個人的には”The Algorithm Design Manual”という本でそのあたりは勉強してとてもためになりました。とてもいい本なので、興味がある人はぜひ読んでみてください。”War Story”という筆者の経験談コラムもおもしろいですし、内容はやや古いですが、とても実際的な内容だと思います。なお日本語訳は評判が悪いので自分は英語版を読みました。また、辞書的に使う本なのでKindleよりは紙版の方が読みやすいかもしれません。</p>

<p>[asin:1849967202:detail]</p>

<h2 id="q-競プロerはコードが汚い">Q. 競プロerはコードが汚い？</h2>

<p>これはたまに同世代のソフトウェアエンジニアから聞く話です。「競プロをやっていた人はコードが汚いので採用したくない」と言い放っていた同世代のエンジニアも見たことがあります。</p>

<p>とりあえず、自分のコードは汚くないと信じています。そして、まわりに競プロをがっつりやっている同僚がいたことがないので、自分のこの質問に対する答えは「分からない」です。</p>

<p>たとえば競プロのコードでは時間的な制約もあるため <code class="language-plaintext highlighter-rouge">i</code> や <code class="language-plaintext highlighter-rouge">j</code> などの1文字変数を多用しますし、人によってはメソッド名もかなり適当です。では業務でそういうコードを書くかといったら、そういうことはないでしょうし、そういうコードがあったらレビューで指摘すればよいことです。レビュー上のコミュニケーションに難があるとしたら、それはおそらく競プロとはまた別の問題でしょう。</p>

<p>もう少し本質的な問題として、競プロで書くようなコードが他の人にはぱっと理解できない可能性はあると思います。たとえば、整数 <code class="language-plaintext highlighter-rouge">a</code> を <code class="language-plaintext highlighter-rouge">b</code> の倍数へ切り下げるのを <code class="language-plaintext highlighter-rouge">a / b * b</code> と書いたり、切り上げるのを <code class="language-plaintext highlighter-rouge">(a + b - 1) / b * b</code> と書くのは競プロに慣れている人にはぱっと分かるかもしれませんが、慣れていない人には分からないかもしれません。あるいは動的計画法を知らない人にいきなり動的計画法のコードを見せたら、たぶん理解できないと言われるでしょう。</p>

<p>ただ、これも競プロどうこうよりは、どういうコードを「読みやすい」とするかの合意を得る問題でしょう<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>。競プロに限らず、たとえばReactのようなフレームワークに慣れているかどうか、関数型プログラミングの経験があるかといったそれぞれのバックグラウンドによって「読みやすい」コードは異なります。このあたりのコミュニケーションは経験によって得るものなので、もしかすると競プロだけやってチーム開発をやったことがない人は不得手なのかもしれません。</p>

<p>それでも、できることなら面接官を担当する方は「競プロをやっていた人だから」と色眼鏡で見ないでくれるといいなと思います。それよりも、自身で競プロを経験してよりよい面接の形式や採用基準を考えてみるのはいかがでしょうか。</p>

<p>なお、自分が前職で書いた動的計画法のコードは、コメント以外に解説記事を書いて、できるだけ動的計画法を知らない同僚も理解できるように努力しました。</p>

<h2 id="q-家族とくに子供がいてコンテストに参加できないんだけれど">Q. 家族とくに子供がいてコンテストに参加できないんだけれど？</h2>

<p>ちいさい子供がいると、おそらくコンテストに出るのはほぼ不可能だろうと思います。Codeforcesのように日本時間深夜に開催されるコンテストもありますが、120分程度とはいえ、そのあいだは一切邪魔が入らない環境が必要ですし、勉強する時間も取りづらいと思います。</p>

<p>自分は子供を持ったことがないので推測でしかいえませんが、たとえば、子供の面倒をみているときに問題を考えることはできるかもしれませんし、コンテストに参加しなくても、隙間時間で過去問を解いたり本を読んで勉強することはできるかもしれません。</p>

<p>そして機会があればぜひコンテストに参加してみてください。コンテストはコンテストで楽しいものです。</p>

<h1 id="最後に">最後に</h1>
<p>冒頭にも書きましたが、この記事は自分のようなシニアなプログラマが競プロに興味を持ってくれたらと思って書きました。</p>

<p>自分が学生のころは競プロはまだそれほどメジャーではなく、CS専攻でなかった自分は存在も知りませんでした。一方でいまは競プロがメジャーになったものの、どちらかというと若い人、とくに大学生がもっぱら参加しているイメージです。</p>

<p>しかし、そういったことに気後れせずに参加してみると、シニアはシニアなりにたのしめることに気付けるのではないかと思います。なにより、シニアにとっては就活を気にせずに純粋に競プロを楽しめるというアドバンテージがあります。もし興味をもったらぜひ趣味として始めてみてください。</p>

<p>さて、自分はというと、競プロを2年前に始めるときに「AtCoderで青レートになる」を目標にしたのですが、それを達成したいまはつぎをどうしようかなと考えているところです。ここ数ヶ月はスプラトゥーン2にハマっていてほとんど競プロをやっていなかったのですが、つぎにやるならCodeforcesに参加するか、ヒューリスティクスコンテストに参加しようかと考えています。</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>ABCの方が初級者向けでARCの方が上級者向けです。一般に問題の難易度もARCの方が高いです。 <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>途中で3ヶ月くらい空いているのは2回目の転職活動で忙しかったためです <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p>自分が知らないだけで、もしそういう問題があるなら教えてください <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p>個人的にはコードを「読みやすいかどうか」判断するのはそんなに簡単なことではないと思いますが、その話はまたの機会に。 <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[これはなに]]></summary></entry></feed>