From 6dedddc545e2f1930bdc2256784eb1551bd4231d Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 1 Feb 2026 00:49:15 +0900 Subject: feat(nuldoc): rewrite nuldoc in Ruby --- .../posts/2025-11-27/anybatross-writeup/index.html | 92 ++++++++++++---------- 1 file changed, 52 insertions(+), 40 deletions(-) (limited to 'services/nuldoc/public/blog/posts/2025-11-27/anybatross-writeup') diff --git a/services/nuldoc/public/blog/posts/2025-11-27/anybatross-writeup/index.html b/services/nuldoc/public/blog/posts/2025-11-27/anybatross-writeup/index.html index f106611e..d4e8f183 100644 --- a/services/nuldoc/public/blog/posts/2025-11-27/anybatross-writeup/index.html +++ b/services/nuldoc/public/blog/posts/2025-11-27/anybatross-writeup/index.html @@ -132,7 +132,8 @@

回答 (45 byte)

-
print$a+=$\=y/8B/0/+y/0469ADO-R//.$/,","for<>
+
print$a+=$\=y/8B/0/+y/0469ADO-R//.$/,","for<>
+

Hole 1 については同一言語・同一スコアの回答が複数あるので詳細は省略する。 @@ -150,10 +151,11 @@ 最終スコアを見ると 4 位タイ (107 byte) が多く、3 位以上の回答と明確にアルゴリズムの差があるのでここから解説をスタートしようと思う。

-
s=gets
-?A.upto(?Z){(b,),m=s.scan(/(?=(.\B.))/).tally.max_by{_2}
-m>1&&(s.gsub!b,it;$*<<it+?:+b)}
-puts$**?,,s
+
s=gets
+?A.upto(?Z){(b,),m=s.scan(/(?=(.\B.))/).tally.max_by{_2}
+m>1&&(s.gsub!b,it;$*<<it+?:+b)}
+puts$**?,,s
+

変数名などの細かい差異を除けば他の 107 byte 回答と同じだが、 String#scan に渡す正規表現にこれを採用していたのは私だけだったのではないだろうか。 /(?=(\S\S))//(?=(\w\w))/ と比べて短くはならないので意味はない。 @@ -168,9 +170,10 @@ Enumerable#max_by で最頻値を取ってきた後は、多重代入を使って必要な値を取り出している。

-
x = [["la"], 3]
-(b,),m = x
-# => b = "la", m = 3
+
x = [["la"], 3]
+(b,),m = x
+# => b = "la", m = 3
+

置換テーブルのデータは $* へと追加しているが、これは Ruby の特殊変数で、本来は Object::ARGV を指す。ここでは単に最初から空配列で初期化されている便利な入れ物として用いている。 @@ -185,10 +188,11 @@ 回答 A をぐっと睨むと、m>1&&(...) の括弧を削りたくなる。しかしそれには m>1&& がどうしても邪魔になる。というわけで終了条件を工夫することでなんとか m を排除できないかを考えた。それがこちら。

-
s=gets
-?A.upto(?Z){(b,),=(?_+s).scan(/(?=(.\B.))/).tally.max_by{_2}
-$*<<it+?:+b if s.gsub!b,it}
-puts$**?,,s
+
s=gets
+?A.upto(?Z){(b,),=(?_+s).scan(/(?=(.\B.))/).tally.max_by{_2}
+$*<<it+?:+b if s.gsub!b,it}
+puts$**?,,s
+

s の先頭に番兵 _ を置くことで、bi-gram の出現頻度がすべて 1 になったとき、b へと代入される値が「_ + (s の先頭の文字)」になる。これを String#gsub! で置き換えようとすると、そのような文字列は s 中にないので置換が発生しない。String#gsub! は置換が起きなかったとき nil を返すので、これを使って条件分岐ができる。&& だと優先度の関係から String#gsub! の括弧が省略できないが、後置 if なら省略できる。 @@ -203,10 +207,11 @@ Kernel#gets は、入力を特殊変数 $_ へ代入する。これは Perl 由来の挙動で、Ruby にはいくつか $_ を参照するものがある。これを使って変数 s を置き換えると次のようになる。

-
gets
-?A.upto(?Z){(b,),="_#$_".scan(/(?=(.\B.))/).tally.max_by{_2}
-$*<<it+?:+b if$_.gsub!b,it}
-puts$**?,,$_
+
gets
+?A.upto(?Z){(b,),="_#$_".scan(/(?=(.\B.))/).tally.max_by{_2}
+$*<<it+?:+b if$_.gsub!b,it}
+puts$**?,,$_
+

これで 1 byte 縮む。 @@ -218,9 +223,10 @@ 回答 C を眺めると、b への代入に文字を費やしすぎている。これを String#gsub! の第一引数に直接書いてはどうか。更に、直前のマッチしたパターンを指す特殊変数 $& を使えば、変数 b を排除できる。それがこちら。

-
gets
-?A.upto(?Z){$*<<it+?:+$&if$_.gsub!"_#$_".scan(/(?=(.\B.))/).tally.max_by{_2}[0][0],it}
-puts$**?,,$_
+
gets
+?A.upto(?Z){$*<<it+?:+$&if$_.gsub!"_#$_".scan(/(?=(.\B.))/).tally.max_by{_2}[0][0],it}
+puts$**?,,$_
+

これにより 2 bytes も一気に縮まった。 @@ -232,9 +238,10 @@ 回答 D を提出したことで tompng 氏のスコアを越え、氏のコードを閲覧できるようになった。そこから少し変更したものが、mame 氏と (変数名などの些事を除いて) 同じ以下のコードである。

-
s=gets
-?A.upto(?Z){s.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or($*<<it+?:+b;s.gsub!b,it)}
-puts$**?,,s
+
s=gets
+?A.upto(?Z){s.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or($*<<it+?:+b;s.gsub!b,it)}
+puts$**?,,s
+

ここまでとは大きく異なる戦略で終了条件を判定している。使われているのはパターンマッチで、in がマッチの有無を true / false で返すことを利用している。or を用いて、最頻値の出現回数が 1 でないなら置換処理を継続する。 @@ -243,10 +250,11 @@ パターンマッチの利用については途中何度か検討したが、1 でないときに処理を実行するという方針で実装しようとしてしまい、上手く短縮できなかった。

-
# これは 106 byte
-s=gets
-?A.upto(?Z){s.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],2..and($*<<it+?:+b;s.gsub!b,it)}
-puts$**?,,s
+
# これは 106 byte
+s=gets
+?A.upto(?Z){s.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],2..and($*<<it+?:+b;s.gsub!b,it)}
+puts$**?,,s
+
@@ -266,10 +274,11 @@ ruby-p を付けると、以下のようなコードを書いたかのように動作する。

-
while gets
-  ... # 記載したコードの処理
-  puts $_
-end
+
while gets
+  ... # 記載したコードの処理
+  puts $_
+end
+

また、Kernel#gsub という $_ = $_.gsub(...) と同様の処理をおこなうメソッドが生えてくる。今回は String#gsub! も使うので、shebang の分を回収できれば短縮になりそうだ。 @@ -278,18 +287,20 @@ というわけで、実はこれまでも shebang での短縮は何度か試していた。しかし、いずれも 1 byte 増えたり変化しなかったりで成果を上げられずにいた。回答 E についても同様に、以下のようなコードを作っていた。

-
#!ruby -p
-?A.upto(?Z){$_.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or($*<<it+?:+b;gsub b,it)}
-puts$**?,
+
#!ruby -p
+?A.upto(?Z){$_.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or($*<<it+?:+b;gsub b,it)}
+puts$**?,
+

しかしこれは 103 byte で縮められない。にっくきは gsubb の間のスペースである。せっかく s.gsub!gsub にしたのに、後ろが記号でなくなったことでスペースが生じている。といって、括弧を付けるのも上手くはいかない。

-
# これも同じく 103 byte
-#!ruby -p
-?A.upto(?Z){$_.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or$*<<it+?:+b&&gsub(b,it)}
-puts$**?,
+
# これも同じく 103 byte
+#!ruby -p
+?A.upto(?Z){$_.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or$*<<it+?:+b&&gsub(b,it)}
+puts$**?,
+

外側の括弧を移動させてくれば gsubb の間のスペースを消せるが、;&& にせねばならず失敗する。この問題を解決したのが最終回答の 102 byte コードである。 @@ -298,9 +309,10 @@

最終回答 (102 byte)

-
#!ruby -p
-?A.upto(?Z){$_.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or$*<<it+?:+b%gsub(b,it)}
-puts$**?,
+
#!ruby -p
+?A.upto(?Z){$_.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or$*<<it+?:+b%gsub(b,it)}
+puts$**?,
+

String#% は文字列のフォーマット処理をおこなう演算子だが、ここでは特にフォーマット目的で呼んでいるわけではない。ここで重要なのは、この演算子が特に副作用を持たず、どんな型でも右辺に取れることである。b の中身にフォーマット指定子はない (% などの記号が入力されないことが問題文から分かる) ので、誤って動作を壊してしまうおそれもない。 -- cgit v1.3-1-g0d28