えびちゃんの日記

えびちゃん(競プロ)の日記です。

Writeup for True or False 2, Daily AlpacaHack 2026/8/13

Daily AlpacaHack 2026/8/13–2026/8/15 の write-up です。 alpacahack.com

True or False の続編です。

考えたこと

続編あるあるとして、まずは diff を見ます。

% diff true-or-false{,-2}/chal.py
4c4
< MAX_EVALS = 28
---
> MAX_EVALS = 17 # No more ternary search
9c9
<     "+-*/()<>="
---
>     "+-*()<>=" # No more division-by-zero errors

おいおいおいという感じです。得られる出力が $3$ 通りしかなく、$3^{17} \lll 2^{44}$ なので困ります。 $6.013 \lt 2^{44/17} \lt 6.014$ なので $6^{17} \approx 2^{44}$ とわかります。$6^{17}$ の方がわずかに小さいため困りますが、何回か試す前提で無視できる範囲だと思いました。

$6 = 3\times 2$ なので、timing attack によって「すぐに終わる 3 択」と「ゆっくり終わる 3 択」をすればよさそうです。 $\{0, 1, 2, 3, 4, 5\}$ の値を取る変数を $z$ とし、$x = z \ge 3$ かつ $y = z\bmod 3$ とします。取りうる値をわかりやすくするため、コード上ではそれぞれ z012345, x01, y012 とします。

sleep() などは使えないしなぁと思いつつ ALLOWED_CHARS* を見ていると 10**10000000 という気分になります。 一生待たされるのは困るので適宜チューニングしていると 10**8**7 とかがちょうどよさそうだなぁとなりました。 よって、x01 の値によって待ち時間を生じさせる gadget として ((x01*10)**8**7)*0+... ができあがります。

次に、3 択の値から Error を生じさせる方法について考えます。 Error を出せないなら 2 択を 3 つ作って $8^{17} \ggg 2^{44}$ にするか?という気持ちにもなりつつ ALLOWED_CHARS を見ながら考えていると、シフト演算の幅を負数にすることを思いつきます。

>>> 1 >> -1
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    1 >> -1
    ~~^^~~~
ValueError: negative shift count

よーしよしということで、3 択を行う gadget として 1>>(y012-1) ができあがります。 上記と合わせて、((x01*10)**8**7)*0+(1>>(y012-1)) の形になりました。Error が 0 で True が 1 で False が 2 です。 評価順序の関係で、ゆっくり gadget を左に置いて、エラー gadget を右に置く必要があることに一応注意しておきます。

あとは $z$ を表す式について考えればよいです。 ということで、a を $6$ 進法で表したときの $i$ 桁目がわかればうれしいなとなります。前回は $3^{28} \gt 2^{44}$ だったので $3$ 進法での $i$ 桁目を下記のようにして求めました。整数 $x$, $y$ ($y\gt 0$) に対して $x\bmod y = x - \floor{x/y}\cdot y$ が成り立つことを用いています。

# b = a // 3**i % 3
# 1 / b < 1: [0, 1, 2] => [Error, False, True]
code = f"1/((a//3**{i})-(a//3**{i})//3*3)<1"

/ を使わずに除算がしたいがちょっと大変そうということで、上位桁について適当に三分探索しながら下位ビットを決めていくというようなことも思いつきましたが、なんか面倒そうだし心がときめかなかったので捨てました。 コメントにも “No more ternary search” と書かれていたので忖度です(?)。

note: たとえば f"(a-((a>>{i+1})<<{i+1}))>>{i}" とすることで $i$ ビット目を返せると思います。

さて、/ を使わない除算といえば Barrett reduction です。 過去に示したもの を思い出しながら雰囲気で端数合わせをします。条件分岐はしないでできるようにしたいです(condition*ifTrue+(1-condition)*ifFalse のようにすれば別に分岐はできると思いますが)。

雰囲気.py

def div(num, den):
    m = (~(~0 << 128) + den) // den
    res = (num * m) >> 128
    assert res == num // den, f"{num} // {den}, {res}"
    return res


for num in range(2, 1000):
    for den in range(2, 1000):
        div(num, den)

というわけで整ったので、全体のコードを書きます。

solve.py

from time import time

from rsk0315.remote import nc

MAX_EVALS = 17
ALLOWED_CHARS = "0123456789a+-*()<>="


def div(a, k):
    m = (~(~0 << 128) + k) // k
    return f"(({a}*{m})>>128)"


def mod(a, k):
    return f"({a}-({div(a, k)})*{k})"


guess = 0
with nc("nc 34.170.146.252 5440") as p:
    for i in range(MAX_EVALS):
        z012345 = mod(div("a", 6**i), 6)
        x01 = f"({z012345}>=3)"
        y012 = mod(z012345, 3)

        code = f"(({x01}*10)**8**7)*0+(1>>({y012}-1))"
        p.sendlineafter(b"Eval > ", code.encode())

        tic = time()
        resp = p.recvline(drop=True)
        toc = time()
        x01 = toc - tic > 1
        y012 = {b"Error": 0, b"True": 1, b"False": 2}[resp]
        cur = x01 * 3 + y012

        guess += cur * 6**i

    p.sendlineafter(b"Guess > ", f"{guess}".encode())
    print(p.recvall())

これをサーバに投げればおしまいです。一発で動くか不安でしたが、ローカルで特にテストもせず一発でフラグが出たので気持ちよかったです。

Well done! Here's your flag: Alpaca{truth_dare_double_dare_torture_kiss_or_promise}

あとがき

ところで、write-up を書きながら ALLOWED_CHARS を見返していて下記に気づきました。

>>> 0**-1
Traceback (most recent call last):
  File "<python-input-1>", line 1, in <module>
    0**-1
    ~^^~~
ZeroDivisionError: zero to a negative power

0**00**1 がそれぞれ 1 (True) と 0 (False) になるのでエラー gadget として使えます。 コメントに “No more division-by-zero errors” と書いてあるので控えておきましょう(?)。

思いつきパートと実装うまくいきパートがいくつかあり、気持ちよかったです。

おわり

おわりです。

ABC 誤差ジャッジまとめ

<2000-rated として開催されている ABC(通称 A6C、A8C、A7C)、すなわち ABC 126 以降の誤差ジャッジのまとめです。執筆時点では ABC 470 までが開催されたところです。更新は気まぐれになると思います。

一覧

出力内容の分類はお気持ちです。「長さ」や「高さ」などはすべて「距離」にまとめています。

問題 出力内容 基準 出力規定
ABC 126 C 確率 (なし) 絶対誤差または相対誤差が $10^{-9}$ 以下
ABC 130 C 面積 (なし) 絶対誤差あるいは相対誤差が $10^{-9}$ 以下
ABC 130 F 面積 ジャッジプログラムの出力 絶対誤差または相対誤差が $10^{-9}$ 以下
ABC 138 B 総和 ジャッジの出力 絶対誤差または相対誤差が $10^{-5}$ 以下
ABC 138 C その他 ジャッジの出力 絶対誤差または相対誤差が $10^{-5}$ 以下
ABC 139 F 距離 実際の答え 相対誤差または絶対誤差が $10^{-10}$ 以内
ABC 142 A 確率 ジャッジの出力 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 144 D 角度 ジャッジの出力 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 144 F 期待値 ジャッジ 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 145 C 平均値 ジャッジの出力 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 151 F 円の半径 想定解答 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 154 D 期待値 想定解答 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 157 F 時間 想定解答 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 159 C その他 想定解答 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 163 A 円の周長 想定解答 絶対誤差または相対誤差が $10^{-2}$ 以下
ABC 168 C 距離 正しい値 絶対誤差または相対誤差が $10^{-9}$ 以下
ABC 180 B 距離 正しい値 絶対誤差または相対誤差が $10^{-9}$ 以下
ABC 181 F 円の半径 想定解答 絶対誤差または相対誤差が $10^{-4}$ 以下
ABC 183 B 座標 想定解答 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 184 D 期待値 正しい値 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 189 F 期待値 想定解答 絶対誤差または相対誤差が $10^{-3}$ 以下
ABC 193 A 比率 想定解答 絶対誤差または相対誤差が $10^{-2}$ 以下
ABC 193 D 確率 想定解答 絶対誤差または相対誤差が $10^{-5}$ 以下
ABC 194 D 期待値 想定解答 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 197 D 座標 想定解答 絶対誤差または相対誤差が $10^{-5}$ 以下
ABC 205 A 比率 想定解答 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 211 A その他 想定解答 絶対誤差または相対誤差が $10^{-5}$ 以下
ABC 223 C 距離 想定解答 絶対誤差または相対誤差が $10^{-5}$ 以下
ABC 224 G 期待値 想定解答 絶対誤差または相対誤差が $10^{-5}$ 以下
ABC 231 A その他 想定解答 絶対誤差または相対誤差が $10^{-3}$ 以下
ABC 234 B 距離 想定解 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 236 E 平均値 正しい値 相対誤差または絶対誤差が $10^{-3}$ 以下
ABC 239 A 距離 想定解 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 242 A 確率 想定解 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 246 B 座標 想定解 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 255 B 円の半径 想定解 絶対誤差または相対誤差が $10^{-5}$ 以下
ABC 259 B 座標 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 263 Ex 距離 想定解答 絶対誤差または相対誤差が $10^{-4}$ 以下
ABC 266 E 期待値 真の解 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 274 E 時間 想定解答 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 275 G 極限値 想定解答 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 279 D 時刻 真の値 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 286 Ex 距離 真の値 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 291 B 平均値 真の値 絶対誤差または相対誤差が $10^{-5}$ 以下
ABC 292 F 距離 真の値 絶対誤差または相対誤差が $10^{-9}$ 以下
ABC 292 Ex 平均値 真の値 絶対誤差または相対誤差が $10^{-9}$ 以下
ABC 294 F 比率 真の値 絶対誤差または相対誤差が $10^{-9}$ 以下
ABC 313 F 期待値 真の値 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 314 E 期待値 真の値 相対誤差もしくは絶対誤差が $10^{-5}$ 以下
ABC 314 Ex 円の半径 真の値 相対誤差もしくは絶対誤差が $10^{-5}$ 以下
ABC 315 F その他 真の値 絶対誤差または相対誤差が $10^{-5}$ 以下
ABC 319 C 確率 真の値 絶対誤差が $10^{-8}$ 以下
ABC 324 F その他 真の値 相対誤差もしくは絶対誤差が $10^{-9}$ 以下
ABC 327 E その他 真の値 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 332 E 分散 真の値 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 334 F 距離 真の値 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 341 G 平均値 真の値 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 342 F 確率 真の値 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 350 E 期待値 真の解 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 356 G 時間 正答の真の値 絶対誤差または相対誤差が $10^{-9}$ 以下
ABC 374 D 時間 真の値 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 375 B その他 真の値 相対誤差または絶対誤差が $10^{-6}$ 以下
ABC 382 E 期待値 真の答え 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 385 F 距離 真の解 絶対誤差または相対誤差が $10^{-9}$ 以下
ABC 392 D 確率 真の解 相対誤差または絶対誤差が $10^{-8}$ 以下
ABC 393 G その他 (特殊) (特殊)
ABC 401 G 時間 真の値 相対誤差が $10^{-6}$ 以下
ABC 402 E 期待値 真の値 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 404 F 確率 真の解 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 407 B 確率 真の値 絶対誤差が $10^{-9}$ 以下
ABC 418 B 比率 真の値 絶対誤差が $10^{-9}$ 以下
ABC 421 E 期待値 真の解 相対誤差または絶対誤差が $10^{-5}$ 以下
ABC 424 E 距離 真の解 絶対誤差または相対誤差が $10^{-9}$ 以下
ABC 426 E 距離 真の値 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 434 B 平均値 真の解 絶対誤差または相対誤差が $10^{-5}$ 以下
ABC 448 G 確率 真の値 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 449 A 面積 真の解 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 456 B 確率 真の解 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 469 E 確率 真の答え 絶対誤差または相対誤差が $10^{-6}$ 以下
ABC 470 E 期待値 真の解 絶対誤差または相対誤差が $10^{-5}$ 以下

分析?

基準は「真の値」が一番多く、次点で「想定解答」でした。

出力規定は、「絶対誤差または相対誤差が $10^{-6}$ 以下」が一番多く、次点の「絶対誤差または相対誤差が $10^{-5}$ 以下」と比べて 3 倍以上の個数でした。 確率のように真の値が $1$ を超えないものについては相対誤差への言及がない問題もいくつかありました。

閾値としては $10^{-6}$ が一番多く、$10^{-5}$ や $10^{-9}$ はその半数ほどでした。 接続詞としては「または」が一番多く、「もしくは」「あるいは」が少しだけあります。 また、「絶対誤差」が先に書かれるものが多数派ですが、たまに「相対誤差」が先に書かれているものもあります。

ABC 144 F の「出力は、ジャッジと出力との絶対誤差または相対誤差が $10^{-6}$ 以下のとき正解と判定される。」は「ジャッジの出力との〜」の typo かもしれないし、そうでないかもしれません。

ギャラリー

Twitter の古のオタクの方々を紹介します。

こういう問題 があり、許容誤差として $2e-3$ と書かれていました。

あとがき

合わせて読みたい

259-momone.hatenablog.com

多少誤差が大きくても AC とする仕様については、AtCoder のような full-feedback 形式であれば特に問題ないと思いますが、Codeforces のような hack を前提としたシステムだとうれしくない気もします。 許容誤差が $10^{-6}$ の問題で、defender の出力の相対誤差が $1.01\times 10^{-6}$ となる(真の値は $1$ 以上)ような入力を与えて Unsuccessful hacking attempt が返されたら、お気持ち表明記事が発生すると思います。

エイプリルフールコンで、「出力の相対誤差が $0.10^{-6}$ 以上のときに正答となる」とか書いてみるのもありかもしれません*1

おわり

おわりです。こんなことをしている場合ではない。

*1:$0.10^{-6}$ と $10^{+6}$ だったらどっちの方が読み飛ばしやすそうですか?

ABC 470 C についての雑談

atcoder.jp

えびちゃんは、先日述べた事情 やそれ以外の事情でここ数ヶ月 ABC から離れていたのですが、今回久々に参加しました。 直前の練習では、以前のえびちゃんではしないようなミスを散々して自信を失くしていましたが、本番でも C で散々詰まったのでしょんぼりになりました。一応コンテスト中には通せましたし、内容としても面白かったです。複数の解法で実装したのでいろいろと書きます。

おはなし

諸々のトピックについて述べます。

そういうデータ構造を用いる解法には特有のキャッチーさ(?)があり、惹かれてしまう気がするのですが、まずは基本を押さえるのが大事だと思います。 計算量解析をできるようになりましょう。そういうデータ構造のお勉強をするのも楽しいと思うので、両方やりましょう。

ならし解析

データ構造について何らかの処理を行う際、一回のクエリの最悪計算量は大きくなってしまう場合でも、一連の操作全体の計算量はそこまで大きくならないというシチュエーションはしばしばあります。そうした際には、最悪計算量の代わりに 償却計算量 あるいは ならし計算量 (amortized complexity) というのを考えます。

各 $i$ ($0\le i\lt Q$) について $i$ 回目のクエリの実際の計算量を $t_i$ とします。任意の $i$ ($0\le i\lt Q$) について、$\sum_{j=0}^i t_j \le \sum_{j=0}^i a_j$ となる $a_i$ ($0\le i\lt Q$) を定められるとき、$i$ 回目のクエリの償却計算量を $a_i$ と呼びます。 追加と削除など複数の種類のクエリがある場合には、どのような順序でそれらを行った場合でも $\sum_j t_j \le \sum_j a_j$ の条件が満たされている必要があります。

たとえば、$Q$ 回クエリを行った際に $t_i = 1$ ($0\le i\lt Q-1$) かつ $t_{Q-1} = Q$ となるようなデータ構造があったとして、$a_i = 2$ ($0\le i\lt Q$) と定めると上記の条件を満たします。このデータ構造を使った際の計算量を見積もるにあたって、一回あたりの計算量として $t_i$ の代わりに $a_i$ を用いても全体としては困らず、かつ $a_i$ の方が小さく見積もれているのでうれしいことを示しやすいわけです。 動的配列(C++ の vector や Rust の Vec のようなもの)に対して末尾に新しい要素を追加する際の計算量が、償却計算量として $O(1)$ 時間であることなどが身近な例です。

こうしたことを示すための解析を ならし解析 (amortized analysis) などと言います。 ABC でよく見る論法の例としては、「各要素に着目すると、その要素を消す処理を行う前にその要素を入れる処理をしている必要がある。その要素が消される回数は高々 $1$ 回なので、$n$ 個まとめて消す処理を $O(n)$ 時間でできるのであれば削除処理は $1$ つあたり $O(1)$ 時間と見なせる」といったものがあります。

keywords: banker’s method, physicist’s method

今回の想定解法はこの手法によるものです。他の出題例(C 問題とは限らず、網羅的でもない)としては下記があります。

今回の問題への提出コードは 提出 #78230021 です。

種類数

さて、要素数 $n$ の非負整数列 $a = (a_0, a_1, \dots, a_{n-1})$ が $a_0 + a_1 + \dots + a_{n-1} = S$ を満たすとき、$a$ に含まれる整数の種類数は $O(\sqrt S)$ 個であるという話があります。 すなわち、今回の問題でいう $A$ は $O(\sqrt Q)$ 種類の値しか入っておらず、これを平衡木などで管理すればよさそうということになります。hiro1729 による ユーザ解説 でも触れられています。

コンテスト中は頭が終わっていて「$A$ に含まれる値を平衡木で管理するなら log がつくんだから $O(N+Q^{1.5}\log(Q) )$ 時間で、$Q\le 5\times 10^5$ でそれは無茶だろ〜 🤪」となっていました。冷静になるとクエリ 1 回ごとの処理は平衡木への追加・削除で $O(\log(Q) )$ 時間と平衡木の走査で $O(\sqrt Q)$ 時間であり、$O(\log(Q)+\sqrt Q) = O(\sqrt Q)$ なので問題ないわけですね。

平衡木に関して、要素数を $n$ として全要素を順に走査するのが $O(n)$ 時間でできるのも、ならし解析によるものだと思います。

AtCoder に限らない出題例や記事としては次のものがあります。

今回の問題への提出コードは 提出 #78265187 です。

ビットごとにカウント

XOR といえばビットごとに数える典型というのはまぁあって、それに固執してうまくいかなかった人も多いかもしれません。 うまくいく解法について述べます。SSRS によるものです。

正整数の多重集合 $S$ について、$T_k = \{\!\{ x \bmod 2^{k+1} \mid x\in S\}\!\}$ を考えて、$T_k$ に含まれる $2^k, 2^k+1, \dots, 2^{k+1}-1$ の個数の偶奇を考えて、奇数なら $2^k$ が答えへの寄与になります。 Dec クエリに対しては、$1$ から $0$ になる要素($2^{k+1}$ で割ったあまりではなく)をまず除外します。$T_k$ に関しての差分は、$x \equiv 0 \pmod{2^{k+1}}$ または $x\equiv 2^k \pmod{2^{k+1}}$ なる $x$ に限られることがわかるので、$O(1)$ 種類の要素について更新すればよいです。 更新にあたっては、Dec クエリの個数を覚えておくなり deque を使うなりするのがよさそうです。個数の管理に適切なデータ構造を用いれば、Inc クエリが一般の Add クエリだった場合でも対応できそうでう。

ビットごとに数える問題と、特定のクエリの個数を覚えておく問題の例です。

今回の問題への提出コードは 提出 #78266354 です。

Binary trie

$\gdef\symdiff{\operatorname{\triangle}}$

別の解法についての noshi91 のツイートです。

$5\cdot 10^5$ 回 $A_1 \xgets+ 10^{18}$ で更新させることで 64-bit 整数信者を落とすことができる(?)

これを実装して面白いな〜となったので書きます。 非負整数の集合 $S$ について、下記を処理できるデータ構造を作れます。任意の二項演算 $\circ$ と引数 $T$ について、$S\xgets\circ T$ は $S$ を $S\circ T$ で更新することを意味するとします。

  • 与えられた非負整数 $x$ について $x\in S$ を判定する。
  • 与えられた非負整数 $x$ について $S \xgets{\cup} \{x\}$ で更新する。
    • $S$ に $x$ を追加する。
  • 与えられた非負整数 $x$ について $S \xgets{\smallsetminus} \{x\}$ で更新する。
    • $S$ から $x$ を削除する。
  • 与えられた非負整数 $x$ について $S \xgets{\symdiff} \{x\}$ で更新する。
    • $S$ に $x$ が含まれるか否かを反転させる。
    • あるいは、$\{x\}$ との対称差で更新する。
  • $0\notin S$ を事前条件とし、$S \gets \{x-1\mid x\in S\}$ で更新する。
  • $\bigoplus_{x\in S} x$ を出力する。
    • 全部の要素の XOR を求める。

Add クエリ $a_x \xgets+ y$ は、$S \xgets{\symdiff} \{a_x\}$ と $S \xgets{\symdiff} \{a_x+y\}$ で処理できます。 Dec クエリは、$S \gets \{x-1\mid x\in S\}$ と $S \xgets{\smallsetminus} \{0\}$ で処理できます。

以下、$S \gets \{x-1\mid x\in S\}$ の処理を説明します。 $x\gt 0$ を非負整数 $q$, $k$ を用いて $x = (2q+1)\cdot 2^k$ と表せるとき、$x-1 = 2q\cdot 2^k + (2^k-1)$ です。すなわち、$x\xgets-1$ の更新は下位 $k+1$ bits の反転と見なせます。 $$ q\cdot 2^{k+1} + 1\underbrace{00\ldots 0}_{k~\text{bits}}{\;\!}_{(2)} \mapsto q\cdot 2^{k+1} + 0\underbrace{11\ldots 1}_{k~\text{bits}}{\;\!}_{(2)} $$ これを binary trie に含まれるすべての整数に対して行うのは、$0$ の枝と $1$ の枝を交換する操作を(元々の)$0$ の枝に沿って行うことに相当します。 これは $O(\log(\max A) )$ 時間で処理できることがわかります。

この処理に相当する図を下記に示します。破線で $0$ の枝、実線で $1$ の枝を表します。また、三角形は(空でもよい)任意の部分木とします。

$\{x-1\mid x\in S\}$ に相当する binary trie

$S = \{1, 3, 4, 6, 7\}$ と $\{x-1\mid x\in S\}$ に対応する binary trie の図を下記に示します。 変更される箇所を赤で塗っています。

$S = \{1, 3, 4, 6, 7\}$ に対する例

Xor クエリに関しては、部分木が持つ値たちの XOR への寄与や、部分木が持つ要素数の偶奇を管理することで、$O(1)$ 時間で答えられます。

ふつう binary trie を使うときは上位ビットから分岐させがちな気がしますが、これは大小関係をうまく扱いたいからで、$\pm 1$ による一括更新を扱いたいときは下位ビットから分岐させるのがいいのかな〜と思いました。

今回の問題への提出コードは 提出 #78251019 です。

適当な二項演算としての XOR

特にいい性質があることを期待していない適当な二項演算(可換モノイド程度?)として XOR が使われることはしばしばあります。

FFT をするときだけ $998244353$ で割ったあまりを求めさせるとバレちゃう的な話同様、XOR 特有の性質を使うときだけ XOR を使うとバレちゃう的な見方もあるかもしれません。

あとがき

どうせ unrated で参加しているし、C を飛ばして D を解いて順位を上げても特にうれしいことはなく、解けない問題を投げ出さずに根気強くやるのが(現時点での)えびちゃんへの訓練としてはいいんだろうな〜という気持ちがあります。結果としてきちんと解けましたし、D も 5 分くらいで通せたのでよかったな〜と思っています。 E 以降もそのうち読もうと思っています。

記事中の図を描くにあたっては TikZ を使っているのですが、やっぱりなかなか曲者でして、ドキュメントや Stack Overflow を読みながらがんばっています。非公式の HTML 版 もあるらしいです。過去の記事 で NP-complete な問題に関してのおえかきをしたときのことを思い出しました。これも楽しかったです。

おわり

おわりです。

いつまでもあると思うなという話

あるいは、終わりがまだ先だろうという油断について。

今回は、競プロも CTF も(少なくとも直接的には)関係ない話で、感情のまま書き散らすレア回です。 というのも、えびちゃんが約 9 年間続けていたゲームが終わりを迎えたことに伴い、いろいろと感じるところがあったためです。

老人語り

えびちゃんがリステップを始めたのは 2017 年の 8 月 22 日で、3240 日ログインしていました。ちょこちょこ忘れている日がありますね。 当時は、ソシャゲあるあるの AP 制でキャンディというアイテムがあったり、譜面のレベルが☆ 5 つで示されていたりしていました。 また、難易度も EXPERT がなくて HARD が一番上で、Stage of Star の HARD が一番難しいものでした。楽曲数も当時はあまり多くなかったので物足りなくなってきて、画面上半分を隠してプレイしたりしていました。 その後、宣誓センセーションの HARD が追加され、明確に「むずかしい」という感覚が芽生えました。それでも何時間かプレイしていると見えるようになってきて、リステップからしか得られない栄養素がないと生きられない身体になっていきました。

あらそいに関しては周回があまりにも大変だったので敬遠することも多かったですが、フルコンチャレンジやハイスコアチャレンジは楽しんでやっていました。 プリズムダービーや tap only などが出てきたりなくなったり、EXPERT が追加されたり、さまざまな変化があったなあと思い出します。メドレーも好きだったなあ。 EXPERT が追加されるまでの間、ある程度慣れてきてまた物足りない期になっていましたが、それでもリステップから離れられなかったので片手プレイに勤しんだりしていました。たしかカナリアの HARD を片手でやっていたと思います。

何度かのオンゲキコラボも印象深いです。当時最大ノーツ数だった宣誓センセーション EXPERT の 948 を 1 超える 949 ノーツの Splash Dance!! も、新しい時代を感じさせるものの一つでした。その後は当然のように 1000 ノーツを超える譜面が出てきて、物足りなさを感じることは少なくなっていきました。 ところで Flavor Youth の EXPERT のラストはなんですか? 結局一度しかフルコンできませんでした。シンプルに音ゲーが弱いだけという気もします。

物足りなさを感じることが少なくなっても身体が縛りプレイを求めていて、グレースケールにしてプレイするのを最近はやっていました。私たち、四季を遊ぶんです!! がわりとやりやすくて、譜面職人の気持ちを信じながらパズルをすることで色が見えないのになぜかコンボが繋がる快感には、驚きを隠せませんでした。クリア後、色を戻したときに「世界に色がある...!」と感じたのも面白かったです。直近は、遡行的インサイドやレーイレーイの EXPERT もグレースケールで all perfect できました。黄色はグレースケールでも比較的見やすいのでずるいです(?)。

そういえば、当時はタイトル画面の楽曲が Do it!!PARTY!! だったのですが、懐かしさからか Do it!!PARTY!! のイントロを聴くだけで泣きそうな気持ちになります。あれ別に泣き曲ではなくないですか?

えびちゃんはこういう懐古話が好きなんだろうということをしばしば思います。老人適性がある(?)。

音ゲー側の話とは別で、陽花ちゃんの声優が変わったり、環ちゃんが加わったり、陽花ちゃんが海外に行かれたり、苑ちゃんやアルシュトーンの皆さま方が加わったりといろいろなことがありました。正直な話をすると、花守ゆみりさんが陽花ちゃんを演じていた時代に心を置き去りにされているえびちゃんもいます。もちろん新しく入ってきてくださる方々のことも応援していて、そこに嘘はないのですが、忘れがたいものもあるということですね。

アルシュトーンの皆さま方は本当にすごくて、遡行的インサイドや WHITE ADDixiON に STAND BY YOU、リメンバーズ! のカバーなど好きなものがたくさんあります。リステの楽曲は名曲しかないのですが、そんな中でもやはり KiRaRe が実家という感じがあり、「やっぱこれなんだよなあ〜〜」という気持ちになります。

キャラとしては(えびちゃんはお姉さんが好きなので)環ちゃんが好きなのですが、環ちゃんのソロ曲やユニット曲などが作られるご予定はありますでしょうか...? 新衣装もお麗しいことですのに 🥺 中学生アイドルコンテンツであるところに高校生が入るのが異端とされているのか、全体集合絵でも環ちゃんがご不在であられることがしばしばあり、かなしい気持ちになることもあります。 すみっコぐらしでも同様にえびふらいのしっぽちゃんがご不在なことがしばしばあり、似たような気持ちになります。

思い出話パートはこのくらいにいたしましょう。

より広くコンテンツの話

えびちゃんのリステップへの総課金額はあまり少なくないと思いますが、リステップからしか得られない栄養素はリステップからしか得られないのであり、リステップが生まれなかった世界線で同じだけのお金を費やしても得られない体験を得ることができたと思っています*1。 こう思えるようなコンテンツに出会えたこと自体が幸福だと感じます。

リステップが終わって唯一無二のゲーム性が失われることは人類にとっての多大な損失だと思うわけですが、リステップのゲームスタイルが唯一無二であったこと自体も寂しいものという気もします。似たタイプの思考型リズムアクションゲームは今後出てくるのでしょうか。

そのコンテンツを好きであり続ける前提で、コンテンツの終了と同時に自決しないのであれば、コンテンツを最後まで見届けられずに自分が死ぬか、コンテンツが終わる悲しみに耐えながら生きるかの二択しかありません。どちらの択であれ後悔は生じるものと思います。 あるいはコンテンツが終わらないうちにそのコンテンツから離れるというのもありますが、そういう離れ方自体が悲しいなぁとも感じます。

えびちゃんと相容れない教えとして「自立とは依存先を増やすこと」というものがありますが、依存先のそれぞれがかけがえのないものなので、ただ単一障害点を増やしているだけということになりがちです*2。 コンテンツを好きになるたびに終わったときの悲しみを想像するような生き方が幸福か?というと、そうではないというのは自覚しています。 自覚していることと実践できるかは別の話です。

他にもえびちゃんを救わないフレーズとして「止まない雨はない」というものがありますが、陰らない日もまたないことであるなぁと思うわけです。 あるいは、雨が止む瞬間の存在性だけを示して満足されても困るという気持ちにもなります。雨が止む前に死んでしまう場合もあり、主観的には止まない雨と同じです。 という話を書いていて “Life is a mosaic of pleasure and pain – grief is an interval between two moments of joy. [...]” のことを思い出しました。えびちゃんが以前よく遊んでいた Getting Over It の中でも引用されているフレーズです。

お見送り

今の会社に入社してから数年経ち、先輩や先輩以外などを見送る機会が多くありました。 会社以外でも、Twitter で見ていて尊敬していた人の更新がもう止まってしまったりして、名前を見る機会があるたびに気持ちが込み上げます。昔の写真を見ていたら、その方とお食事したときのものが出てきて、同様の気持ちになりました。

追記:何人かの方が名前を挙げてくださっていました。その方であっています。やはり想っているのはえびちゃんだけではないみたいです。

数年前、狩生かりゅというゲーム実況者がおられました。目隠しでポケモンルビーを攻略する動画が特に好きでした。 消えた動画が戻ってきてまた観られるようになる夢を見たのは、一度や二度ではありません*3。 キャモメやワンリキーの鳴き声、ポケモンルビーの BGM や効果音を聞くたびに記憶が蘇り、どうしていなくなってしまったのかという気持ちに襲われます。 心構えができていないかった別れは、特有の苦しみを伴うものです。 上で言及した Getting Over It についても彼の動画がきっかけで始めたもので、馬の鳴き声が聞こえるたびに切なくなります。

大学に入って最初にお世話になっていた宿では宿側の事情で 1 年経たずに退去することになったり、研究室時代は B3 から M2 まで 1 年ごとに指導教官が変わったり(おめでたい事情のことが多かったので、これはそういうものかなという気がしていますが)、「お見送り」という観点で思い返すといろいろあったかなぁという気持ちになったりします。

そういえば、歌丸師匠が旅立たれた後、録画していた笑点を見返してなにか虚しい気持ちになっていた時期もあったなあと思い出しました。

他にも惜しまれつつ幕を閉じたコンテンツとして、Google Code Jam もありました。Meta Hacker Cup はいつまで続いてくれるのでしょうか。

ぽかいこちゃんが Twitter から去ってしまったことについては、「どうして...」となっていますが、今でもえびちゃんのことを見守ってくださる(今日も記事が上がっていました。どうして? いつもありがとうございます)ので、これに関しては泣かずに済んでいます。

最近の話

最近は、ちょっとしたきっかけから VTuber の方を何人か応援していますが、これもいわゆる卒業が起こりがちな業界なので、心が弱っているえびちゃんには厳しいものがあるかもしれません*4? 卒業して数ヶ月・数年経った方に対してのロスを抱えたまま生き続けている人をしばしば見かけ、(時期的に、その方のことはえびちゃんはあまり知らないのですが)やはりそうした感情は普遍的に生じうるものだよなぁと思ったりもしました。 えびちゃんは、大空スバルさん、星街すいせいさん、白銀ノエルさん、雪花ラミィさん、儒烏風亭らでんさんを応援しています。

これは V ではないのですが、TL で見かけてたまたま観た動画で memento mori について言及があり、えびちゃんが思っていたのと似たような部分があるかなぁと思いました。 memento mori は remember (that you have) to die のこと(ref) ですが、えびちゃんが抱えているものは you ではなく特定の第三者なので、少し違った側面があるなとも思っています。 他にも、carpe diem, quam minimum credula postero.Gaudeamus igiturDe brevitate vitae などの言葉を知りました。 brevitas は abbreviation の真ん中あたりの部分と同根で、brevis は短いという意味です。しばしば “for the sake of brevity” などの言い回しも見ますが、その brevity も同様です。ラテン語のお勉強を始めてから、こういうところに意識が向くようになりました。

あとがき

えびちゃんだけではなくフォロヮの皆さま方も何かのロスに苦しんでおられることでしょうか? あるいは、えびちゃんがツイートや記事たちを全部消していなくなる可能性も否定はできないわけで、そうした状況への準備はできていますか? あとで読もうと思っている記事があるなら、URL をブックマークするのではなくローカルに保存しておく方が安心だと思います。

そういえば、びーとくんは数年前に実行していましたが、余裕を持って予告をなさっていてえらかったなぁと思いました。

おわり

おわりです。土日だっていつまでもあると思ってはいけません。あっという間に夕方です。

*1:音ゲーの部分が好きで、それ自体は無料で遊べるものではあります。

*2:このような教訓は各々の解釈によるところが大きいだろうので、えびちゃん以外の人にとって救いであるなら特に言うことはありません。

*3:三度か四度だったと思います。

*4:いま応援している人が卒業しそうとかそうでないとか、そういう話には言及していません。

3 の倍数と 3 がつく数のときだけ、浮動小数点誤差でアホになる世界のナベアツ

def f(i):
    A0 = [1.1819055680010597, 1.431565918540171, 1.0642546737906344]
    A1 = [1.7027727811689999, 1.5993649864648791, 1.025135482175168, 1.0916806072811036]
    A = A0 if i < 26 else A1
    for x in A:
        i /= x
        i *= x
    return i


def is_aho(i):
    return i % 3 == 0 or "3" in str(i)


N = 40
for i in range(1, N + 1):
    assert (f(i) != i) == is_aho(i)
    print(f(i))

いきます。

1.0
2.0
2.9999999999999996
4.0
5.0
5.999999999999999
7.0
8.0
8.999999999999998
10.0
11.0
11.999999999999998
12.999999999999998
14.0
15.000000000000002
16.0
17.0
17.999999999999996
19.0
20.0
21.000000000000004
22.0
23.000000000000004
23.999999999999996
25.0
26.0
27.000000000000004
28.0
29.0
30.000000000000004
30.999999999999996
31.999999999999996
33.00000000000001
33.99999999999999
35.00000000000001
36.00000000000001
36.99999999999999
37.99999999999999
38.99999999999999
40.0

👏👏


想定質問

可搬性は?

Python のまともな処理系であれば上記通りの結果になると思います。IEEE 754 準拠で、binary64 かつ tiesToEven 丸めであれば他の言語でも同様です。

どう見つけたのか?

$n\oslash k_1\otimes k_1\oslash k_2\otimes k_2\oslash\cdots$ の形式でランダム生成して条件に合うものを探しました。

$N\gt 40$ の場合はどうか?

知りません。本家が $40$ で止まってくれてよかったと思っています。

場合分けは減らせないのか?

$1\le n\le 25$ で条件を満たす $(k_1, k_2, \dots)$ について、$n = 26$ でも成り立つものを見つけられませんでした。たぶん厳しいんじゃないかと思いますが証明はしていません。

たぶん下記の記事に出てくるのと似たような議論をするといいんじゃないかなぁと思います。

rsk0315.hatenablog.com

あるいは、上記の形式に限らない式を考える必要があります。

本家が $25$ で止まってくれたらよかったのにと思いましたが、それだとネタとして成立しないんですよね。

参考文献

FF 外の知らない方ですが、検索していて見つけました。

他の先行研究は特に調べていません。

応援

がんばえー

あとがき

ありません。

おわり

おわりです。

浮動小数点型の除算に対する第二原像攻撃

こういう表現をすることは稀だと思いますが、構造としては同じだなと思ったのでそういう言い方を選びました。

16.0 / 9.0 として計算した double の値 $$ \begin{aligned} 16\oslash 9 &= \tfrac1{9}\cdot(16-2^{-50}) \\ &= {\small 1}.{\small 777777777777777}{\footnotesize 679091286699986}{\scriptsize 085295677185058}{\tiny 59375} \\ \end{aligned} $$ があったとします。このとき、(主に誤差に対して甘いプログラムの撃墜ケースを見つけたい文脈で)$x\oslash y = 16\oslash 9$ かつ $\gcd(x, y) = 1$ かつ $(x, y) \ne (16, 9)$ なる組 $(x, y)$ を探したいことはしばしばあります*1

用語

集合 $\mathcal X$, $K$ と関数 $h\colon \mathcal X\to K$ を考えます。 与えられた $k\in K$ に対し、$h(x) = k$ なる $x$ を $k$ の 原像 (preimage) または 第一原像 (first preimage) と言います[Glossary | CSRC]。 また、$x_1\in\mathcal X$ に対し、$h(x_1) = h(x_2)$ かつ $x_1\ne x_2$ なる $x_2\in\mathcal X$ を、$x_1$ の 第二原像 (second preimage) と言います[Glossary | CSRC]

また、これらの原像を求めることを、それぞれ (第一)原像攻撃 (​(first-)preimage attack)・第二原像攻撃 (second-preimage attack) と言います。セキュリティの文脈の用語なので、攻撃という言い方がなされるんだと思います。

この文脈の第一・第二というのは、原像として一つ目・二つ目という意味合いであって、甲種・乙種とか ①・②のような意味合いではないと思っています*2

さて、特定の浮動小数点型に含まれる正の正規化数全体からなる集合を $F$ とし、$\Z_F = \Z\cap F$ とします。 このとき、$f\colon \Z_F^2\ni (x, y) \mapsto x\oslash y\in F$ で定義される $f$ に関しての第一・第二原像を求めていこうかな〜という話です。

考察

まず第一原像攻撃について考えていきます。 与えられた $z$ に対して $x\oslash y = z$ なる $(x, y)$ を求めたいです。

$\roundcirc{z'} = z$ なる $z'=\tfrac xy\in \Q$ を求めればよいわけで、そのような $z'$ は $\Q$ 上の区間になっていることに注意します。 特に、$[z-2^{e_-}\lldot z+2^{e_+}]$ のような形をしています。開区間か閉区間かは、仮数部の偶奇によって変わります。

分母が $k$ 以下の分数として表せる有理数全体からなる集合について、相異なる要素の差の絶対値は $k^{-2}$ で下から評価できることを踏まえると、閉区間のケースに帰着できると思います。

ということで、与えられた区間 $I$ に対して $I\cap\Q$ の要素を取ってこれればよいということになります。 取ってくる要素としては、分母が最小のものを持ってこれると実用上うれしそうです。 $I\cap\Z \ne \emptyset$ なら $\min{(I\cap\Z)}$ を持ってくればよいので、以下はそうでないと仮定します。

これはもう Stern–Brocot 木を考えてくれと言われているようなものです。適切に区間を狭めていくことで実現可能です。今回はそういう気分ではないので証明はしません。

続いて、第二原像攻撃について考えます。 $(x, y)$ が与えられて $z = x\oslash y$ としたとき、$[z-2^{e_-}\lldot z) \cap (z\lldot z+2^{e_+}]$ に属する有理数を取ってくればよいです。 二つの区間それぞれについてさっき考えた操作をすればよいので、これもできます。おわりです。

実装

preimage2.py

from fractions import Fraction
from math import ceil, floor, inf, nextafter


def _bisect(f):
    assert f(0)

    hi = 1
    while f(hi):
        hi *= 2
    lo = hi // 2
    while hi - lo > 1:
        mid = lo + (hi - lo) // 2
        if f(mid):
            lo = mid
        else:
            hi = mid
    return lo


def _mediant(x, y):
    fracx, kx = x
    fracy, ky = y
    x0, x1 = fracx.numerator, fracx.denominator
    y0, y1 = fracy.numerator, fracy.denominator
    return Fraction(kx * x0 + ky * y0, kx * x1 + ky * y1)


def interval_fraction(min_: Fraction, max_: Fraction) -> Fraction:
    """Return p/q with minimum q such that min_ <= p/q <= max_."""

    if min_ == max_:
        return min_
    if min_ <= 0 <= max_:
        return Fraction(0, 1)
    if max_ < 0:
        return -interval_fraction(-max_, -min_)

    assert 0 < min_ < max_

    if min_.is_integer():
        return min_
    if ceil(min_) <= floor(max_):
        # min_ <= n <= max_
        return ceil(min_)

    n = floor(min_)
    min_, max_ = min_ - n, max_ - n

    assert 0 < min_ < max_ < 1

    lo = Fraction(0)
    hi = Fraction(1)

    while True:
        assert lo < min_ < max_ < hi
        med = _mediant((lo, 1), (hi, 1))
        if min_ <= med <= max_:
            return n + med

        ky = _bisect(lambda k: _mediant((lo, 1), (hi, k)) < min_)
        lo0 = _mediant((lo, 1), (hi, ky))
        hi1 = _mediant((lo, 1), (hi, ky + 1))
        assert lo0 < hi1
        assert lo0 < min_ <= hi1

        if hi1 <= max_:
            return n + hi1

        lo = lo0
        # ----------------------------------------------------------------
        assert lo < min_ < max_ < hi
        med = _mediant((lo, 1), (hi, 1))
        if min_ <= med <= max_:
            return n + med

        kx = _bisect(lambda k: _mediant((lo, k), (hi, 1)) > max_)
        hi0 = _mediant((lo, kx), (hi, 1))
        hi1 = _mediant((lo, kx + 1), (hi, 1))
        assert hi1 <= max_ < hi0

        if min_ <= hi1:
            return n + hi1

        hi = hi0


def preimage2(frac: Fraction) -> Fraction:
    """Return a second preimage for frac."""

    if frac == 0:
        return Fraction(0, 1)
    if frac < 0:
        return -preimage(-frac)

    alpha = float(frac)
    fl = Fraction(alpha)
    tol = Fraction(1, 2**2048)
    nextdown = nextafter(alpha, -inf)
    lo = (Fraction(nextdown) + Fraction(alpha)) / 2
    if float(lo) == nextdown:
        lo += tol
    nextup = nextafter(alpha, inf)
    hi = (Fraction(nextup) + Fraction(alpha)) / 2
    if float(hi) == nextup:
        hi -= tol

    x1 = interval_fraction(lo, frac - tol)
    x2 = interval_fraction(frac + tol, hi)
    return x1 if x1.denominator < x2.denominator else x2


assert preimage2(Fraction(16, 9)) == Fraction(941929333829127, 529835250278884)
assert preimage2(Fraction(9, 16)) == Fraction(633318697598980, 1125899906842631)
assert preimage2(Fraction(2, 3)) == Fraction(2401919801264265, 3602879701896398)
assert preimage2(Fraction(1, 10)) == Fraction(800639933754755, 8006399337547549)

具体例

冒頭の例について考えます。 $$ \begin{aligned} 16\oslash 9 &= \tfrac1{9}\cdot(16-2^{-50}) \\ &= {\small 1}.{\small 777777777777777}{\footnotesize 679091286699986}{\scriptsize 085295677185058}{\tiny 59375} \end{aligned} $$ に丸められる区間は $$ [\tfrac1{9}\cdot(16-2^{-50}) - 2^{-53} \lldot \tfrac1{9}\cdot(16-2^{-50}) + 2^{-53}] $$ で、 $$ \begin{aligned} &\phantom{{}={}} \tfrac1{9}\cdot(16-2^{-50}) - 2^{-53} \\ &= \tfrac1{9} \cdot (16 - 2^{-50} - 9\cdot 2^{-53}) \\ &= \frac{16\cdot 2^{53} - 2^3 - 9}{9\cdot 2^{53}} = \frac{144115188075855855}{81064793292668928} \\ &= {\small 1}.{\small 777777777777777}{\footnotesize 568068984237470}{\scriptsize 431253314018249}{\tiny 51171875}, \\ &\phantom{{}={}} \tfrac1{9}\cdot(16-2^{-50}) + 2^{-53} \\ &= \tfrac1{9} \cdot (16 - 2^{-50} + 9\cdot 2^{-53}) \\ &= \frac{16\cdot 2^{53} - 2^3 + 9}{9\cdot 2^{53}} = \frac{144115188075855873}{81064793292668928} \\ &= {\small 1}.{\small 777777777777777}{\footnotesize 790113589162501}{\scriptsize 739338040351867}{\tiny 67578125} \end{aligned} $$ です。見つかった第二原像は $(941929333829127, 529835250278884)$ で、 $$ \begin{aligned} &\phantom{{}={}} \frac{941929333829127}{529835250278884} \\ &= {\small 1}.{\small 777777777777777}{\footnotesize 568068984237471}{\scriptsize 269418025815571}{\tiny 204989570324110{\ldots}} \\ &= \tfrac9{16} - \tfrac1{4768517252509956} \end{aligned} $$ でした。他の例は値だけ載せておきます。

$$ \begin{aligned} 9 \oslash 16 &= 633318697598980 \oslash 1125899906842631, \\ 2 \oslash 3 &= 2401919801264265 \oslash 3602879701896398, \\ 1 \oslash 10 &= 800639933754755 \oslash 8006399337547549. \end{aligned} $$

あとがき

下記の記事の補足のような感じです。

rsk0315.hatenablog.com

ゆるゆるっとコードを書いて記事に出すくらいの気軽さを持つことがよさそうです。

Stern–Brocot 木は ABC でそこまで頻出ではないですが、そうした競プロで得た知識を競プロの横道の部分で活かせて面白いなぁと思いました。

反例の構成をする癖を続けていると、「この範囲には反例はあるだろうと思うが、実際に構成するのは自明ではなさそう」とか「既知の方法に帰着させて最小反例を構成できる」とかの感覚がついてきて楽しいなぁという気がします。既知の幅を広げていきたいです。

コンテスタント側であれば浮動小数点型を避ければいいだけですから、わざわざ反例について思いを馳せる必要はないですが、ジャッジ側として変な解法を落としたい場合には有用な内容だと思います。 浮動小数点型が使われているだけで嘘解法扱いして、実際の最小反例が制約の上限よりずっと大きいのに「これは嘘解法だ!」と決めつけるような人にはなりたくないですからね。

数値型として浮動小数点型しか備えていない独自言語でのみ提出可能な地獄のえびちゃんお誕生日コンテストとかどうでしょうか。嫌ですね。 JavaScript にも BigInt があるので、その問題用の言語を作る()必要があるかなと思いました。Educational 浮動小数点型 Contest というのはどうでしょう(?)

今年は 関数型まつり2026 に行こうかなと思っていたのですが、気づいたら一人で浮動小数点型まつり 2026 をやっていました。どうしてでしょう。

おわり

おわりです。

*1:あるのかなあ。あるということにします。こういうのは言い切った者勝ちなので。

*2:なので、「どっちのタイプが第二原像攻撃だっけ?」というような疑問を抱く余地はない気がします。えびちゃんはしばらく誤解していました。

ABC 463 A についての雑談

atcoder.jp

$X:Y = 16:9$ であるかを判定したいです。こういう問題は(たぶん)何も考えずに浮動小数点型の除算で行う人と、何も考えずに整数型の乗算で行う人に大きく二分される気がします。 今回は、何かしら考えて遊んでみましょうという回です。

クイズ

まずはクイズです。次のうち、今回の問題の制約 $X, Y \in [1\lldot 1000]\cap\Z$ において反例が存在するものはどれでしょう?

// A * B == C * D
fn check0_1(x: f64, y: f64) -> bool { 9.0 * x == 16.0 * y }
fn check0_2(x: f64, y: f64) -> bool { 0.0625 * x == 0.1111111111111111 * y }

// A / B == C / D
fn check1_1(x: f64, y: f64) -> bool { x / y == 16.0 / 9.0 }
fn check1_2(x: f64, y: f64) -> bool { x / 16.0 == y / 9.0 }
fn check1_3(x: f64, y: f64) -> bool { y / x == 9.0 / 16.0 }
fn check1_4(x: f64, y: f64) -> bool { 16.0 / x == 9.0 / y }

// (A * B) / C == D
fn check2_1(x: f64, y: f64) -> bool { (x * 9.0) / y == 16.0 }
fn check2_2(x: f64, y: f64) -> bool { (x * 9.0) / 16.0 == y }
fn check2_3(x: f64, y: f64) -> bool { (y * 16.0) / x == 9.0 }
fn check2_4(x: f64, y: f64) -> bool { (y * 16.0) / 9.0 == x }

// A * (B / C) == D
fn check3_1(x: f64, y: f64) -> bool { x * (9.0 / y) == 16.0 }
fn check3_2(x: f64, y: f64) -> bool { x * (9.0 / 16.0) == y }
fn check3_3(x: f64, y: f64) -> bool { y * (16.0 / x) == 9.0 }
fn check3_4(x: f64, y: f64) -> bool { y * (16.0 / 9.0) == x }

上限が小さいので全量を愚直にテストできます。

正解は check3_1 で、$(X, Y) = (784, 441)$ のみが反例でした(!)。 上限が $10^5$ の場合は、check3_3 が反例 $(X, Y) = (1872, 1053)$ を持ちますが、残りの関数たちはいずれも正当でした。 上限が $10^9$ の場合はどうでしょう?

disclaimer: 上限が $10^9$ の場合の正当性について、上記の全部の関数に証明を与えているわけではありません。各関数の最小反例などを挙げているわけでもありません。すいません。

説明

check0_1

fn check0_1(x: f64, y: f64) -> bool { 9.0 * x == 16.0 * y }

check0_1 は整数での判定と同じ形で、$\max{\{9X, 16Y\}}\le 2^{53}$ であれば正当であることがすぐわかります。 $16\otimes Y = 16Y$ から、$\max{\{9X, Y\}}\le 2^{53}$ で正当だと思います。

check2_*

fn check2_1(x: f64, y: f64) -> bool { (x * 9.0) / y == 16.0 }
fn check2_2(x: f64, y: f64) -> bool { (x * 9.0) / 16.0 == y }
fn check2_3(x: f64, y: f64) -> bool { (y * 16.0) / x == 9.0 }
fn check2_4(x: f64, y: f64) -> bool { (y * 16.0) / 9.0 == x }

$2^{53}$ 以下の任意の正整数 $x$, $y$, $m$ について $x/y = m \iff x\oslash y = m$ であることが示せます(下記の記事で示しました(元々は $\floor{x/y} = \floor{x\oslash y}$ だけ示していましたが、今回の記事のために追記しました))。

rsk0315.hatenablog.com

check2_* の右辺が整数であることを踏まえると、check2_* は $\max{\{9X, 16Y\}} \le 2^{53}$ であれば正当であるとわかります。

check1_*

fn check1_1(x: f64, y: f64) -> bool { x / y == 16.0 / 9.0 }
fn check1_2(x: f64, y: f64) -> bool { x / 16.0 == y / 9.0 }
fn check1_3(x: f64, y: f64) -> bool { y / x == 9.0 / 16.0 }
fn check1_4(x: f64, y: f64) -> bool { 16.0 / x == 9.0 / y }

$2^{26}\approx 6.71\times 10^7$ 以下の任意の正整数 $x_1, y_1, x_2, y_2$ について、 $$x_1/y_1 = x_2/y_2 \iff x_1\oslash y_1 = x_2\oslash y_2$$ が成り立つことが示せます(下記の記事で示しました)。

rsk0315.hatenablog.com

よって、check1_* は $\max{\{X, Y\}} \le 2^{26}$ であれば正当であるとすぐわかります。

4 つ中 2 つの値が定数であることを利用すると、もっと広い範囲で正当であることを示せるのかもしれません。

こういうタイプのもの(precision を上げると正当な範囲が広がりそうなもの)は、小さめの precision たちで実験するのが定石です。 実験をしてみましたがよくわかりませんでした。いかがでしたか?

  • check1_1
    • $p$ が $6$ 増えるごとに最小反例が $2^6$ 倍くらいになってそう
    • 最小反例の大きさは $p\bmod 6$ に応じて違う事情になってそう
      • $\tfrac{16}9$ が $2$ 進法で $6$ 桁周期になっているから?
  • check1_2, check1_3
    • $p$ が $1$ 増えるごとに最小反例が $2$ 倍くらいになってそう
  • check1_4
    • $p$ が $1$ 増えるごとに最小反例が $2$ 倍くらいになってそう
    • check1_2, check1_3 ほど整った感じではない

どうやら $2^{26}$ よりはもっと広い範囲で正当っぽそうな雰囲気はありました。

check3_2

fn check3_2(x: f64, y: f64) -> bool { x * (9.0 / 16.0) == y }

任意の正整数 $x\le 2^{53}/9$ に対し、$x\otimes (9\oslash 16) = (x\otimes 9) / 16 = 9x/16$ が成り立つことから、$\max{\{9X, Y\}}\le 2^{53}$ で正当であるとわかります。

check0_2, check3_4

fn check0_2(x: f64, y: f64) -> bool { 0.0625 * x == 0.1111111111111111 * y }
fn check3_4(x: f64, y: f64) -> bool { y * (16.0 / 9.0) == x }

$(1\oslash 16)\otimes x \stackrel?= (1\oslash 9)\otimes y$ と $x \stackrel?= (16\oslash 9)\otimes y$ ですが、$16$ が 2 べきであることに注意すると、これらは同値であることがわかります。

ここで、$16\oslash 9 = \tfrac19\cdot(16 - 2^{-50})$ です。よって、 $$ \begin{aligned} (16\oslash 9)\otimes y &= (\tfrac19\cdot(16 - 2^{-50}) )\otimes y \\ &= \roundcirc{\tfrac19\cdot(16 - 2^{-50})\cdot y} \\ &= \roundcirc{\tfrac{16}9\cdot y - \tfrac y9\cdot 2^{-50}} \\ \end{aligned} $$ となります。$\tfrac y9\cdot 2^{-50}$ の誤差項の影響が無視できるうちは正当となります。

exercise: それはどういう範囲か?

exercise: 分母が $9$ の代わりに $49$ だと $y = 49$ が反例になると思うが、今回はなぜ大丈夫なのか?

実験をしたところでは、check1_1 同様に、$p$ が $6$ 増えると最小反例は $2^6$ 倍になっていて、各 $p$ での大きさは $p\bmod 6$ による感じになっていそうでした。

$$ \begin{aligned} X &= \begin{cases} 2^{p-2}+2, & \text{if~} p \bmod 6 \in\{0, 1\}; \\ 112 \cdot \frac{64^{\frac{p-8}6}-1}{63}+48\cdot 64^{\frac{p-8}6} + 2, & \text{if~} p \bmod 6 = 2; \\ 2^{p-3}+7, & \text{if~} p \bmod 6 \in\{3, 4\}; \\ 7 \cdot \frac{64^{\frac{p-5}6}-1}{63} + 3\cdot 64^{\frac{p-5}6}, & \text{if~} p \bmod 6 = 5 \end{cases} \\ % &= \begin{cases} % 2^{p-2}+2, & \text{if~} p \bmod 6 \in\{0, 1\}; \\ % \frac{112}{63} \cdot (64^{\frac{p-8}6}-1) + 48\cdot 64^{\frac{p-8}6} + 2, & \text{if~} p \bmod 6 = 2; \\ % 2^{p-3}+7, & \text{if~} p \bmod 6 \in\{3, 4\}; \\ % \frac7{63} \cdot (64^{\frac{p-5}6}-1) + 3\cdot 64^{\frac{p-5}6}, & \text{if~} p \bmod 6 = 5 % \end{cases} \\ &= \begin{cases} 2^{p-2}+2, & \text{if~} p \bmod 6 \in\{0, 1\}; \\ (48 + \frac{16}9) \cdot 64^{\frac{p-8}6} + \frac29, & \text{if~} p \bmod 6 = 2; \\ 2^{p-3}+7, & \text{if~} p \bmod 6 \in\{3, 4\}; \\ (3+\frac19) \cdot 64^{\frac{p-5}6} - \frac19, & \text{if~} p \bmod 6 = 5 \end{cases} \\ \end{aligned} $$ かつ $$ Y = \begin{cases} \frac9{16}\cdot(X-2)+1, & \text{if~}p \bmod 6 \in \{0, 1, 2\}; \\ \frac9{16}\cdot(X-7)+4, & \text{if~}p \bmod 6 \in \{3, 4, 5\} \\ \end{cases} $$ が最小反例になっていそうな雰囲気がありました。 $p = 53$ のとき、 $$ \begin{aligned} X &= (3+\tfrac19)\cdot 2^{48}-\tfrac19 \\ &= \tfrac19\cdot(7\cdot 2^{50}-1) \\ &= 875699927544263, \\ Y &= \tfrac9{16}\cdot(X-7)+4 \\ % &= \tfrac9{16}\cdot\left((3+\tfrac19)\cdot 2^{48}-\tfrac19-7\right)+4 \\ &= \tfrac9{16} \cdot\tfrac19\cdot(7\cdot 2^{50}-64)+4 \\ &= 7\cdot 2^{46} \\ &= 492581209243648 \end{aligned} $$ です。実際、この $X$, $Y$ に対して $$ \tfrac XY = 1.{\small 777777777777777}{\footnotesize 552208655314253}{\scriptsize 9092472621372767(857142{\dots})} \ne \tfrac{16}9 $$ ですが、check0_2check3_4True を返しました。最小性は示していません。

check3_1, check3_3

fn check3_1(x: f64, y: f64) -> bool { x * (9.0 / y) == 16.0 }
fn check3_3(x: f64, y: f64) -> bool { y * (16.0 / x) == 9.0 }

$(1\oslash 49)\otimes 49 = 1-2^{-53}$ が有名です。 $(X, Y) = (16\cdot 49, 9\cdot 49) = (784, 441)$ が check3_1 の反例でした。

$(1\oslash 117)\otimes 1053 = 9 + 2^{-49}$ です。 $(X, Y) = (16\cdot 117, 9\cdot 117) = (1872, 1053)$ が check3_3 の反例でした。

$(x\oslash y)\otimes z$ は反例がたくさんある形の式に見えます。

rsk0315.hatenablog.com

反例

見つけたうちで最も小さい反例です。最小性はほぼ示していません。

関数名 $x$ $y$
check0_1 ${\small 1000799917193447}$ ${\small 562949953421314}$
check0_2 ${\small 875699927544263}$ ${\small 492581209243648}$
check1_1 ${\small 941929333829127}$ ${\small 529835250278884}$
check1_2 ${\small 1125899906842631}$ ${\small 633318697598980}$
check1_3 ${\small 1125899906842631}$ ${\small 633318697598980}$
check1_4 ${\small 750600033020791}$ ${\small 422212518574195}$
check2_1 ${\small 1000799917193447}$ ${\small 562949953421314}$
check2_2 ${\small 1000799917193447}$ ${\small 562949953421314}$
check2_3 ${\small 1125899906842631}$ ${\small 633318697598980}$
check2_4 ${\small 1125899906842631}$ ${\small 633318697598980}$
check3_1 ${\small 784}$ ${\small 441}$
check3_2 ${\small 1000799917193447}$ ${\small 562949953421314}$
check3_3 ${\small 1872}$ ${\small 1053}$
check3_4 ${\small 875699927544263}$ ${\small 492581209243648}$

Stern–Brocot 木を用いて、各範囲に含まれる分数を探したりしました。

$$ \begin{aligned} \tfrac{633318697598981}{1125899906842633} &\in [\tfrac9{16}-2^{-54}\lldot \tfrac9{16}), \\ \tfrac{633318697598980}{1125899906842631} &\in (\tfrac9{16}\lldot \tfrac9{16}+2^{-54}], \\ \tfrac{941929333829127}{529835250278884} &\in [\tfrac{16}9-2^{-53}\lldot\tfrac{16}9), \\ \tfrac{16012798675095097}{9007199254740992} &\in (\tfrac{16}9\lldot\tfrac{16}9+2^{-53}]. \\ \end{aligned} $$

check1_4 に関しては、x >> 36 == 0x2AAA となる範囲においては(全探索で確認して)最小でしたが、上位 bit は雰囲気で探索したのでよくわかりません。

おまけ

(y * 16.0) * 0.1111111111111111 == x とか (y * 16.0) * (1.0 / x) == 9.0 とかを考え出すとキリがなくなりそうだったのでやめました。 16.0 / x == 9.0 * (1.0 / y) のように左辺と右辺で形が違う場合などを網羅するのは大変です。

あとがき

コンテスト中にこんなもんを考えるくらいなら整数型を使うだろ
↑ 至極真っ当な指摘だなあ 🙄

過去にそういう記事を書いたえびちゃんとしては、制約を見てそういう気分になった日には x / y == 16.0 / 9.0 で解くこともあるかもしれません。Rustacean であるところのえびちゃん的には、f64 だと .0 をつける必要があるのでやっぱり i32 で書くかもしれません。

${}\cdot\tfrac19\cdot{}$ ← 顔文字っぽい

「浮動小数点型を使いましょう」「浮動小数点型はいいぞ」という主張をする気はないんですが、これくらいの簡単な問題で復習をしながら「ふつうは浮動小数点型を避けて整数型で解くけど、浮動小数点型で解けるんだっけ?」とかを考えて練習しておかないと、実際に浮動小数点型を使うことを要求される問題で下手なミスをやらかすんじゃないか?という気はします。とはいえ浮動小数点型を使わされる問題はあまり多くなく、他の数え上げとかを訓練しておいた方がコスパがいいみたいなところもあり、ン〜〜〜という気持ちになります。数え上げより浮動小数点型を要求される問題が多い世界線の AtCoder を見たいかと言われると、まぁ別にそんなことはないですね。推しが人気になると微妙な気持ちになる厄介オタクみたいな心理ですか?

「既知の lemma から、この制約では大丈夫なことを示せます」というのを言える場合でも、「最小反例はこれこれです」というのを示すのは難しい場合が多いです。 「precision を上げてもいくらでも小さい反例があるパターン」と「precision を上げるのに伴って最小反例が大きくなっていくパターン」があり、後者については小さい precision における実験ベースで「た〜ぶんこれくらいの大きさの反例を構成できるんじゃないか?」という遊びができることは多いですが、きちんと示すのは大変な場合が多いんじゃないかなと思っています。

ABC の A 問題くらいをメインで解いている初心者だと、この手の煩雑な議論を追うのは厳しいところがある気がします。 括弧のつけ方ひとつで計算結果(というか証明の大変さ)が大きく異なることはよくあることなので、議論の中身を把握していない初心者に「今回の問題は浮動小数点型でも解けるらしい」という粒度の事実だけが伝わってしまうことはあまりうれしくないんじゃないかなと思っています。

一方で、なにも考えずに「浮動小数点型は使わない方がいいんだよ」とだけ初心者に伝える風潮もなんか違うんじゃないかなぁというような気もしています。 計算量の記法について怪しい記事がたくさんあるのと同様、ちゃんと理解していない人がちゃんと理解しないまま伝承しがちなトピックのひとつだろうなぁとは思っています。

気が向いたらそのうちポエム記事を書くかもしれません。過去のポエム記事でなにを書いたかはあまり覚えていません。

rsk0315.hatenablog.com

おわり

おわりです。