カメヲラボ

主にプログラミングとお勉強全般について書いてます

静かな記念日

先日、義母(つまり妻のお母さん)が急逝したんですよ。 本当に突然でした。

早朝、娘の高校生活が始まったばかりで、慌ただしく準備をしている最中の訃報でした。 「頭が真っ白」という言葉がありますが、今まで生きてきて、初めて本当にこの状態になりました。

少し前に、LINEで子どもの入学のお祝いやこれからのことを少しやりとりしたところだったんですよね。 他の誰よりも、娘の合格を喜んでくれていました。

私の両親は随分前に他界していて、それくらいの時期に、大切な友人や仕事のパートナーを失うということも立て続けにあって、この先誰かが亡くなったとしても、そういうことに慣れっこになってしまうんだろうなと思っていました。

でも今は、こんなに悲しいことがあるなんてという気持ちです。 妻と娘が並んで焼香台に立つ姿を見て、本当にやるせ無い気持ちでした。 自分の父と母は長い闘病生活の末での最期でしたから、心の準備が十分にできていました。だから今回は余計に辛く感じます。

娘が生まれてから、義母は本当によく娘のことを可愛がってくれて、娘もおばあちゃんが大好きで、母娘3代でよくおでかけをしていました。 仲の良い3人でした。

これからも、まだまだそんな暮らしが続いていくものだと思っていましたが、それが突然、本当に突然終わってしまいました。

妻と娘の悲しみは、私には想像もできないほどのものだと思いますが、正直なところ、実は私自身も相当こたえています。 常に娘の成長と共にいてくれたからなのでしょうね。 優しく、色々と気配りができて、穏やかで、かわいい人でした。

このまでの幸せな日々に想いを馳せながら、これから自分はどうするべきなのか、何度となく考えてみるのですが、まだ頭が回りません。 ただ一つ言えるのは、義母がこれまで与えてくれた愛情を胸に、家族に対して自分のできる限りのことをやっていきたいということです。 何か特別なことをしたり、自分の身を削ったりということではなく、自分を含めた家族みんなが毎日元気で楽しく過ごせるように、無理せず助け合って生きていきたい。そう思います。

こんな日記を書いている今日、実は私たちの結婚記念日なのです。 記念日だし、何かお祝い…と思いながら、しかしこんな時期だしどうしようと考えていたら色々と考え込んでしまって、結果的に日記を書くことになりました。 そういえば、結婚式の披露宴でケーキ入刀というよくあるイベントをやったのですが、妻の発案で私の両親と妻の両親も一緒にケーキ入刀をやったんですよ。 皆元気な時に、お揃いでやることができたのは、今となっては良い思い出です。

取り留めのない文章になりつつあるので、今日はこれで終わりにしようと思います。 娘が高校生になり弁当持ちになり、毎日妻と協力してお弁当を作るようになったのですが、なかなか楽しいものですね。かなり眠いですが。

ではおやすみなさい。

大規模データセットのためのアルゴリズムとデータ構造

大規模データセットのためのアルゴリズムとデータ構造という書籍

とても良い本が出ます

『大規模データセットのためのアルゴリズムとデータ構造』という本が7月26日発売に発売されます。原書はAlgorithms and Data Structures for Massive Datasetsというタイトルです。

この本を見たときに「これは良い!!🤩」と思って、2023年の11月頃からかなり時間をかけて日本語版を作りました。良い本に仕上がったと思いますので、発売前に本書の魅力をお伝えしたいと思います。

概要

本書では、大きく3つの分野「確率的データ構造」・「ストリーミングデータ処理」・「外部記憶アルゴリズム」について、概念 ⇒ 理論 ⇒ 実践がわかりやすく説明されています。また、全体的なイメージを掴んだり、逆に細部を具体的に理解するために、どの章にも豊富な図表がちりばめられています。大規模データセットを効率的に扱うためのアルゴリズムとデータ構造に関する専門知識を解説した書籍ではありますが、この分野の専門家でなくてもよく理解できるように細かな工夫がなされています。

構成

第1部:確率的で簡潔なデータ構造

このパートでは、大規模データを効率的に保存・処理するための省スペースデータ構造を紹介しています。具体的には、ハッシュテーブルやブルームフィルター、カウントミンスケッチ(Count-Min Sketch)、ハイパーログログ(HyperLogLog)といったデータ構造がどのように機能し、どのように適用されるかを解説しています。

第2部:ストリーミングデータ構造とアルゴリズム

このパートでは、リアルタイムで流れるデータ(ストリーミングデータ)を効率的に処理するためのアルゴリズムとデータ構造について説明しています。ストリーミングデータの統合方法やサンプリング技術、近似分位数の計算方法などを取り上げ、現実世界での応用例も紹介しています。

第3部:外部記憶データ構造とアルゴリズム

このパートでは、ディスクや外部記憶を活用して大規模データを効率的に処理する方法を解説しています。外部記憶モデルの基本概念から始め、データベースで使用される B^\epsilon木や LSM木などのデータ構造、さらに外部メモリーを使ったソートアルゴリズムについて具体例を交えて説明しています。

具体的なコードは少な目

最近の書籍は付属のコードがWebページで閲覧できるようになっています。本書でもそのようになっていますので、本文ではコード自体が少な目になっています。特に重要な部分をピックアップしたものや疑似コードのみの掲載にとどめ、かなりコンパクトにまとまっていて何ページにもわたってコードが張り付けられているというようなことはありません(基本的に1ページに収まる程度のサイズになっています)。

参考文献がしっかり書いてある

「当たり前でしょ!」と思う方も多いかもしれませんが、そうとも限らないんですよ。個人的にはこれが一番気に入っていてアルゴリズムとかデータ構造、レビューなどの文献がきちんと示されていて、特に電子書籍版は使い勝手が非常に良いです。

数式は最低限

いくらか数式が出てきますが、わからない人は読み飛ばしてもあまり困らないと思います。もっと詳しく知りたいという方は、多くの箇所で参考文献が示されていますので、参照がしやすくなっています。全体的に、数式・コード・文章・図表のバランスが良く、読んでいてしんどくなりにくい作りになっています。

図がモリモリ

本書では、説明のための図表がフルカラーで100箇所以上で使われています。 イラストもバリエーションに富み、データ構造やアルゴリズム概念を表す抽象的なものからアルゴリズム詳細を表す具体的なものまで、さまざまです。ここで何点か紹介しておきます。

これらはほんの一部ですが、本書の雰囲気は何となくつかんでいただけるかと思います。

翻訳版特有の情報

内容的な修正

数式や図表、その説明など、誤植や内容的な間違いはかなり修正できていると思います。

カタカナ表記

本書では、人名や専門用語について原書の表記を用いている箇所とカタカナ表記を用いている箇所がありますが、主に2つの理由があります。

検索のしやすさ

専門用語や人名について、より詳しく調べてみたいということがあると思います。その際、カタカナ表記で検索しても問題なく情報が得られる場合はカタカナ表記にしていて、そうでない場合は原書の表記のままにしています。たとえば、「カウントミンスケッチ」をWeb検索すると、「Count-Min Sketch」に関する情報が得られますが、「tダイジェスト」をWeb検索しても「t-digest」に関する情報がほとんど得られず関係のない情報をたくさん拾ってしまいます。人名についても基本的には同じ理由ですが、どちらかというと次の理由が大きいです。

読む際のリズム

本書は日本語で書かれたものですので、文章中に英単語が出現し過ぎて読み進める際に流れが一瞬止まってしまうことをできるだけ避けようとしています。実際に声に出して読む読者はあまりいないと思いますが、基本的には音読して引っかかりが少ない文章を意識しました。音以外にも、文章の見た目で引っかからないように表記を注意しました。たとえば、「ブルームフィルタ―、カウントミンスケッチ、HyperLogLog」と3つの用語を並べた場合、HyperLogLogの部分が浮いてしまうため、「ブルームフィルタ―、カウントミンスケッチ、ハイパーログログ」のように表記しています。

表現について

日本語訳版では、比喩的・示唆的な表現は極力シンプルで直接的な表現に変えています。これによりオリジナルの作者が持つ情緒的なニュアンスが変化する可能性がありますが、内容のわかりやすさを重視しました。

訳注について

ここで述べた説明は本文中に直接反映していますが、本文の流れが途切れるような長い説明が必要になった場合は訳注として記述しています。

音引きについて

これは最後までかなり悩んだのですが、語尾の長音符号を極力省略せずに表記しました。私自身が音引きを省略(プログラマ、サーバ等)に慣れきっているので、校正時にだいぶ違和感がありました。しかし世の中の音引きルールが徐々に長音符号を省略しない方向へ進んでいるような気がしたので、そのようにしました。読んでいて気持ち悪いを感じる方がいらっしゃったらスミマセン。

いきなりでごめんなさい(誤植情報)

実は見本誌が出来上がった時点でミスを見つけてしまいました。 本書の図2.2で、スペルチェックに単語をハッシュ化するという例のイラストがあり、初版では

と表示されると思いますが、実際に修正したかったのは、図を として、さらにキャプションを『スペルチェックとハッシュ化』から『スペルチェックとハッシュ化(「あなた」を「あたな」とタイプミスしている)』という感じです。

「あなた」という単語を誤って「あたな」としてしまった結果、スペルチェッカーが反応したというイラストなのですが、最終チェック段階で伝達ミスによってちゃんとしたスペルに修正されてしまいました😅

正誤表等の情報は『大規模データセットのためのアルゴリズムとデータ構造』サポートサイト | マイナビブックス

にまとめられると思いますので、上記のリンクも合わせてご確認ください🙇

線形計画法のテスト問題(netlib)を読み込んで解いてみる

背景と概要

線形計画法のテスト問題が欲しいー!とつぶやいたところ、

と、具体的に教えていただいたので、実際にデータを入手してPuLPを用いて解いてみました。MPSファイルを生成するのに少し手間がかかるので、メモ的に書き残しておきたいと思います。

問題データを手に入れる

データはhttps://www.netlib.org/lp/data/からダウンロードできます。 ただ、データフォーマットが圧縮MPSになっているのでまずこれをMPSファイルに展開する必要があります。

empsというプログラムが必要なので、まずこれを生成します。FortranとCのコードがありますが、私はCの方を使いました。

curl -L -O http://netlib.org/lp/data/emps.c

で入手して、

gcc -o emps emps.c

みたいな感じでコンパイルしてください。実行ファイルができたら、

emps < 圧縮MPSファイル名 > MPSファイル名

という感じで使います。試しにページの一番上にある25fv47というデータを使ってみます。

emps < 25fv47 > 25fv47.mps

みたいにすればOKです。

PuLPで解いてみる

次にこのデータを適当なソルバで解いてみることにします。 今回はPuLPで解いてみました。Pythonがすでに使えるものとして、

pip install pulp

PuLPを使えるようにしておきます。コードは以下の通りです。第一引数にmpsファイルのパスを取るようにしておきます。

import sys
import pulp

mps_file_path = sys.argv[1]
variables, problem = pulp.LpProblem.fromMPS(mps_file_path)
problem.solve()

たったこれだけ。超簡単ですね!実際に動かしてみると、次のような出力が得られました。

...
最初の方は省略
...
Optimal - objective value 5501.8459
After Postsolve, objective 5501.8459, infeasibilities - dual 0 (0), primal 0 (0)
Optimal objective 5501.845888 - 1718 iterations time 0.152, Presolve 0.01
Option for printingOptions changed from normal to all
Total time (CPU seconds):       0.17   (Wallclock seconds):       0.20

問題データのダウンロード元にあるhttps://www.netlib.org/lp/data/readme:readmeを見ると、次のように書かれていました。

Name       Rows   Cols   Nonzeros    Bytes  BR      Optimal Value
25FV47      822   1571    11127      70477        5.5018458883E+03

ちゃんと解が得られたようです。やったー!

というわけで、この情報を教えてくださったimai_yoshiyaさんと、最初にアドバイスをくださったオノウェイ先生に感謝!!

今後自分のソルバで何か成果が出たら改めてきちんと記事にしたいと思います。

追記

結構面倒だったので、まとめて処理しておきました。 https://github.com/ozy4dm/lp-data-netlib

しかし以下の問題について、データの生成ができていません。

  • QAP8
  • QAP12
  • QAP15
  • STOCFOR3

QAP8, QAP12, QAP15については、こちらからジェネレータを手に入れる必要があります。

また、STOCFOR3については実行時にエラーが発生します。トライしてみた結果、以下のようなエラーメッセージが表示されました。

% ./std2mps

 Process TIME file:
       1    TIME          STOCHFOR
      10    ENDATA

 Process CORE file:
       1                                  0.0000                   0.0000
 XXX -  FATAL  - Illegal record in CORE file

 STD2MPS: execution terminated.

解決したら更新しておこうと思いますが、今のところよくわからないのでそのままにしておきます。

競プロ日記013: 日立北大コンの表彰式&意見交換会

表彰式&意見交換会に行ってきました

Hitachi Hokudai Lab. & Hokkaido University Contest 2022(日立北大コン)のB問題の成績で(ギリギリ)お呼ばれして、参加することができました。

経緯

意見交換会で発表しませんか?

オンラインで視聴できるかなーという程度で考えていたのですが、2月27日になって意見交換会ご講演依頼」というメールが届きました。表彰式と意見交換会の3月3日まで結構日が迫っている中でのメールということで、「もしかして人がいなくて人数合わせ的な!?」と思ったりもしましたが、せっかくなので発表の機会をいただくことにしました。

最後とか罰ゲームじゃね?

YESの返事をして、大急ぎでスライドを作ってる最中に当日のスケージュールが来たのですが、発表の順番が成績順だったのですよ。上の方が優れているに決まっているし、特にB問題の発表者は1, 2, 3, 4, 6位とトップクラスの人しかいないので、自分の順番が回ってきた時点でもう話せることないんじゃないかと思って10分中8分くらい自己紹介したろかなと考えたりもしてたのですが、結果的にやらなくて良かったです(後述)。

当日

初手・遅刻

いきなりやらかしまして、12時30分までの表彰式を12時30分からだと思い込んで行った結果、大遅刻しました。終了10分くらい前に、こそこそっと入室して最後尾から見ていたのですが、眼が悪すぎてあまり見えないし動揺して話も全然入ってこないしでホント情けない状態でしたが、声を書けてくださった方が少しいたおかげで落ち着きを取り戻すことができました。

バス移動・お昼ご飯

表彰式が終わったらマイクロバスで別の場所(地理は全くわかっていない)に移動ということで、言われるがまま乗り込みました。作問者の方と少しお話しすることができました。なんと私のことをご存じだったとのこと!どうやら昔書いていたショートコーディング関係のブログをご覧いただいていたようで…。何でもやってみるものですね😅

会場についてから、美味しいお弁当をいただきました。

意見交換会

上位の方々の発表を「なるほどなー」と思いながらボケっと聞きながら、少しずつ「あれ?」と思いはじめました。みんなオンライン参加なんですよね。じゃあ自分の周りにいる人々はどちらさま???

終わって確認してようやく気が付きましたが、自分以外は出題側の方々でした…。「あっ、あなたがid〇〇さんなんですね!どうもはじめまして!!」というのを想像しながら参加していたので、これは衝撃でした。と同時に、最初「自己紹介8分ぐらいやろかな」とか思っていた自分を思い出して、とても申し訳ない気持ちになりました。できるだけ真面目モードに軌道修正したつもりですが、ホントすみませんという感じです。

内容的なお話と感想

意見交換会では、基本的には問題内容やこれまでのコンテストをよくご存じのsimanさんが、発表者の方々に重要なポイントを質問なさっていて、私はふんふんなるほどしていただけなのでとてもありがたいお話を賜っただけでした。

コンテスト中考えもしなかったのですが、今回の問題って受託必須ジョブなのに完了できないものがあったのですね。自分の解答はあきらかに最適化性能が低い自覚があったので、未完であっても「まあしかたないよね」としか思っていませんでした。

他の方々にとってはあまり役に立たなかったような気がしていますが、自分にとって結構大きな収穫がありました。simanさんが天気予測に関する質問をたくさんなさっていて、自分もそれがとても気にはなっていたんですが、上位の方々は最終的に何か特別なことをするというよりもシンプルな期待値を設定しているだけのようで、ここは自分の想像と大きく異なりました。

帰宅してから上位陣のコードを読んだり動かしたりちょっといじってみたりして確認してみたところ、自分がコンテスト中に思いついた調整を試してみてもほとんど効果が無いんですよね。最上位のレベルになると、そもそも探索性能が高くてよくわからないパラメータをどうこうするより単純に高速化とかしてた方がずっと効果的であるようでした。

思い返せば自分が今まで参加したマラソンマッチでも、「雰囲気パラメータ」を作って何となくいじって何となくスコアは上がるものの、上位からは程遠い結果に終わるケースが多々ありました。

つまり何となくコードを変更してみて何となくスコアが上がっている状況というのは、やってる感は多少あるものの根本的な問題から遠いところで何かやっているに過ぎないということです。これは自分にとって大きな収穫だったと思います。

とにかくレベルアップを

今回は現地に行って、参加者でない人々に補足的な説明をしたり多少の賑やかしには貢献できたような気もしますが、やっぱりもっと問題そのものに対する議論に参加できるレベルになっていたかったというのが正直なところです。最近は参加しっぱなしで復習もまとめの記事も書けていないことが多いので、ちょっとなんとかしないとという気持ちはより強くなりました(と言いつつコンテストの予定が発表されるとすぐに飛びついてしまいますが…)。

しばらくは何でも積極的にやるしかなさそうです。

ショートコーディング完結編

はじめに

ショートコーディングという本を書いてから、16年近く経ちました。当時のスキルを思えば120%の力で書ききったものではありましたが、ずっと心残りだったことがあります。

それはCのマクロについてです。マクロを使ってより短いコードを書くことができるのではないかと思いつつも適当な問題が見つからず、また自分で作ることもできず、「今のところできないけどできるかもしれない。」という結論にせざるを得なかったわけです。

しばらくはそのことも記憶の隅に追いやっていたのですが、年に1回くらいは思い出してしまい、Anarchy Golf(あなごる)のCの記録を覗いてみるということを繰り返していました。そして2016年、すばらしい作品に出合いました。

…とまあ、感想文を書いていても仕方ないので、そういうのは最後に書くこととして、ショートコーディング本の最後のピースとして、マクロを利用した超記録とその解説をしたいと思います。紹介する問題は「Fractal X」というフラクタルのAAを出力する問題と「Big Fibonacci Number」という大きなフィボナッチ数を計算する問題の2つです。

この2つの解は、いずれもゴルフ神のtailsさんによるもので、ここでの紹介・解説することにも快く応じてくださいました。心より感謝いたします。 なお、本ブログは私Ozyが勝手に書いたものですので、内容に誤りがあったとしても私が間違えたものであり100%私の責任です。ご了承ください(もし誤りがあった場合はご指摘いただけるとありがたいです)。

では始めましょう!

Fractal X

問題概要

このFractal Xという問題は、入力なしで次のようなAAを出力するコードを書くというものです。

Fractal X
いたってシンプルな問題ですね(見た目の都合で画像にしています)。

基本的な考え方

まずは小さなX

X X
 X 
X X

を作ってみたいと思います。

#include<stdio.h>

void f(int x) {
    if(x >= 4) {
        printf("%d ", x);
    } else {
        f(x * 2);     // X_X
        f(x * 2 + 1); // _X_
        f(x * 2);     // X_X
    }
    if(x / 2 == 1) puts(""); 
}

int main(int x) {
    f(1);
    return 0;
}

これを実行すると、次のような出力が得られます。

4 5 4
6 7 6
4 5 4

フラクタルの問題なので、再帰的な関数呼び出しがよくマッチしていますね。再帰関数内で3回再帰することで、AAの縦横サイズを3n倍にすることができます。

上記の4と7の部分をXにしたいので、出力部分を次のように書きます。

        printf(x % 3 == 1 ? "X" : " ");

これで小さなXを作ることができました。次はこれを拡大していきます。

void f(int x) {
    if(x >= 16) { // ←ココ!
        printf(x % 5 == 1 ? "X" : " "); // ←ココ!
    } else {
        f(x * 2);
        f(x * 2 + 1);
        f(x * 2);
    }
    if(x / 4 == 1) puts(""); // ←ココ!
}

こんな感じでひと回り大きなXができました。

X X   X X
 X     X
X X   X X
   X X
    X
   X X
X X   X X
 X     X
X X   X X

こんな感じで必要な大きさまにまで育ててやったものが次のコードです。

void f(int x) {
    if(x >= 256) {
        printf(x % 17 == 1 ? "X" : " ");
    } else {
        f(x * 2);
        f(x * 2 + 1);
        f(x * 2);
    }
    if(x / 16 == 1) puts(""); 
}

main再帰と基本短縮

この問題では入力の心配をする必要がありませんので、main関数自体を再帰することも難しくありません。

int main(int x) {

    if(x >> 8 != 0) { // x >= 256
        if(x % 17 == 1) putchar('X');
        else putchar(' ');
    } else {
        x *= 2;
        main(x);
        main(x + 1);
        main(x);
    }

    if(x / 32 == 1) puts("");
}

引数なしの実行ではmain関数の第1引数が1になることも利用すれば、非常にスッキリとしたコードになります。これを基本通りに短縮してやると、次のようになります。空白文字や改行を取り除くと82バイトと、かなり短いコードになりました。

main(x){
    x>>8?putchar(x%17-1?32:88):main(x*=2)+main(x+1)+main(x);x/32-1||puts("");
}

マクロの出番が来た!

main(x」という記述が4回も現れました。文字数にして24文字です。これをマクロで置換すると、以下のようになります。

#define m main(x
m){x>>8?putchar(x%17-1?32:88):m*=2)+m+1)+m);x/32-1||puts("");}

一見コンパイルすら通らなさそうな見た目ですが、ちゃんとコンパイルできます。そして先のコードより3バイト短い79バイトのコードになりました。

マクロを使うと、#define ...の部分がそれなりの文字数になるため利用できるケースが少ないですが、このように数多く繰り返すケースであれば利用できることが確認されました。

では次に、大きなフィボナッチ数の問題について解説していきます。

Big Fibonacci Number

問題概要

Big Fibonacci Numberでは、大きなフィボナッチ数を出力します。

フィボナッチ数列を、

f(0) = f(1) = 1

n > 2のとき

f(n) = f(n - 1) + f(n - 2)

と定義します。

このときnが入力として与えられるので、f(n) mod 1000000000(1e9)を出力するという問題です。 なお、入力の最大値は1000000000(1e9)となっています。

Cにはきつい問題

Cにおけるゴルフの基本的なテクニックとして、 型名を省略→(32bit)int型として扱う というものがあります。しかしこの問題では扱う値が大きいので、少なくとも(long longのような)64bit整数が欲しいところです。また、入力も大きめですので愚直に計算すると制限時間内に解答することができません。

つまり、ポイントとして次の3点を考慮する必要があります。

  • 大きな整数値を扱う
  • 計算速度も必要
  • 最短コードを狙う

基本的なアイデア

変数について

long longのような記述は基本的に避けたいので、64bit整数型の変数は使いません。幸い出力はmod 1e9で構わないので、うまくやれば32bit整数型の変数だけで何とかできるはずです。

計算方法について

フィボナッチ数を高速に計算する方法で一般的なものとして、

\begin{pmatrix}F_{n+1} &F_n\\F_n &F_{n-1}\end{pmatrix}=\begin{pmatrix}1 &1\\1 &0\end{pmatrix}^n

のように行列を使う方法があります。

しかしこれをCでそのまま実装すると、次のように結構なコード量になります。

#include<stdio.h>
#include<string.h>

#define ll long long
#define M 1000000000

ll p[30][4] = {{1, 1, 1, 0}};

void mul(ll *a,ll *x,ll *y)
{
  ll t[4];
  t[0] = (x[0] * y[0] + x[1] * y[2]) % M;
  t[1] = (x[0] * y[1] + x[1] * y[3]) % M;
  t[2] = (x[2] * y[0] + x[3] * y[2]) % M;
  t[3] = (x[2] * y[1] + x[3] * y[3]) % M;

  memcpy(a,t,sizeof(t));
}

int main() {
    int n;

    for(int i = 1; i < 30; ++i) {
        mul(p[i], p[i - 1], p[i - 1]);
    }

    for(;~scanf("%d",&n);) {
        ll k[4] = {1, 0, 0, 1};

        for(int i = 0; i < 30; ++i) {
            if((n + 1) & (1 << i)) mul(k, k, p[i]);
        }
        printf("%lld\n", k[1]);
    }
    return 0;
}

他に高速化する方法としてよく使われる方法は配列を使ったDPやメモ化再帰で、実用上は問題ないことが多いのですが、この問題の場合は入力の最大値が1000000000です。4バイト整数の配列を用意するとなると、4GB近く必要になりますので、ちょっとしんどいですね。

周期性を利用できるか

フィボナッチ数列周期性を利用できるかもしれませんので、調べてみました。

桁数 周期
下1桁 60
下2桁 300
下3桁 1500
下4桁 15000
下5桁 150000
下6桁 1500000
下7桁 15000000
下8桁 150000000
下9桁 1500000000

桁数が増えると周期はかなり長くなるため、出力サイズが9桁の場合は必要な配列サイズを超えてしまっていて利用するのは難しそうです。

この時点で行列を使った方法をベースに気合で短縮していきたい気分になるところですが、本物は違いました

最短コードに向けて

まず計算量を落とす

フィボナッチ数列には、

f(m + n) = f(m - 1) \times f(n) + f(m) \times f(n + 1)

が成立するという面白い性質があります。証明・解説は省略しますので、参考として高校数学の美しい物語 > フィボナッチ数列の7つの性質(一般項・黄金比・互いに素)をご覧ください。

この式において、m = nとすると

f(2n) = f(n - 1) \times f(n) + f(n) \times f(n + 1)

となり、m = n + 1とすると

f(2n + 1) = f(n) \times f(n) + f(n + 1) \times f(n + 1)

となります。

つまり、入力が偶数の場合と奇数の場合で計算方法が少し異なりますが、同じような式の形で計算できそうです。 またこの方法を使うとfの呼び出し毎に引数の値が約1/2倍になり、定義通りの漸化式と比べて計算量が格段に少なくなります。メモ化なしのコードは次のようになります。

#include<stdio.h>

#define ll long long
#define M 1000000000

ll fib(ll n) {
    if(n <= 2) return 1;
    int m =  n / 2; // nが奇数の場合は小数点以下を切り捨てる
    if(n & 1) {
        return (fib(m) * fib(m) + fib(m + 1) * fib(m + 1)) % M;
    } else {
        return (fib(m - 1) * fib(m) + fib(m) * fib(m + 1)) % M;
    }
}

int main() {
    int n;

    for(;~scanf("%d",&n);) {
        ll ans = fib(n + 1);
        printf("%lld\n", ans);
    }
}

これで入力が5桁くらいまで動くようになります(定義通りだと2桁程度が限度です)。かなり高速になりましたが、より大きな入力にはメモ化せざるを得ません。

消費メモリを少なくする

先のアルゴリズムでは、関数の呼び出し回数を大幅に少なくする効果がありましたがそれはメモ化回数も少なくしているということになります。入力が最大(1000000000)の場合でも、実際に配列に書き込まれる値は100個程度しかありませんC++であればmapを使ってかなりコンパクトに書くことができます。Cではmapが使えないため、小さめの配列にマッピングすることで実現可能です。

#include<stdio.h>

#define ll long long
#define M 1000000000
#define W 99999
ll memo[W];

ll fib(ll n) {
    if(n <= 2) return 1;
    int w = n % W;
    if(memo[w] > 0) return memo[w];
    int m =  n / 2; // nが奇数の場合は小数点以下を切り捨てる
    if(n & 1) {
        memo[w] = (fib(m) * fib(m) + fib(m + 1) * fib(m + 1)) % M;
        return memo[w];
    } else {
        memo[w] = (fib(m - 1) * fib(m) + fib(m) * fib(m + 1)) % M;
        return memo[w];
    }
}

int main() {
    int i, n;

    for(;~scanf("%d",&n);) {
        for(i = 0; i < W; ++i) memo[i] = 0;        
        ll ans = fib(n + 1);
        printf("%lld\n",ans);
    }
}

ハッシュ計算が雑過ぎるので競プロ的にはアウトだと思いますが、すべての入力がわかっているあなごるでは衝突の心配をあまりする必要はありません。もし衝突したら配列サイズや計算方法を変えれば良いですからね。

これで基本的な準備は終わりました。

偶奇の分岐を無くす

小数点以下の切り捨てを上手く利用すれば、偶奇による分岐を無くして1行で書くことができます。

ll fib(ll n) {
    if(n <= 2) return 1;
    int w = n % W;
    if(memo[w] > 0) return memo[w];
    memo[w] = (fib((n - 1) / 2) * fib(n / 2) + fib((n + 1) / 2) * fib((n + 2) / 2)) % M;
    return memo[w];
}

計算結果は32ビット整数に収まることがわかっていますので、そろそろint型のみの変数を使ったものに書き替えて、コード全体を手直しします。

#include<stdio.h>

#define M 1000000000
#define W 99999

int z[W],i;
int f(int n) {
    if(n < 5) return 1;
    int w = n % W;
    z[w] = z[w] ? z[w] : (1LL * f((n + 1) / 2) * f((n + 2) / 2) + 1LL * f((n + 3) / 2) * f((n + 4) / 2)) % M;
    return z[w];
}

int main(int a) {
    for(;~scanf("%d",&a);) printf("%d\n",f(a + 3));
}

計算途中で32ビットを超えてしまいますので、関数fの戻り値に1LLをかけてlong long型に変換しておきます。あとはここから基本的なテクニックや基本的じゃないテクニックを駆使してコードを短縮していくのですが、かなり複雑になりますので詳細は省いておきます。興味のある方は手元でやってみて、動くかな?と思ったらあなごるのCheckerで動作確認してみてください。

マクロを活用する

関数呼び出しの部分と、64ビット型にキャストする部分がどうしても冗長になってしまいますが、ここでマクロを利用します。

#include<stdio.h>

#define M 1000000000
#define W 99999
#define F 1LL * f(++n / 2)

int z[W],i;
int f(int n) {
    if(n < 5) return 1;
    int w = n % W;
    z[w] = z[w] ? z[w] : (F * F + F * F) % M;
    return z[w];
}

int main(int a) {
    for(;~scanf("%d",&a);) printf("%d\n",f(a + 3));
}

す、すばらしい…。

至高の129B

#define F 1ll*f(++n/2)
m=1e9;h[5686];i;f(n){h[i=n%5686]=h[i]|n<5?:(F*F+F*F)%m;}main(){for(;gets(&i);printf("%d\n",f(atoi()+3)));}

getsの部分はhに読み込むことで1B減らすことができるみたいですが、あえてtailsさんのコードをそのまま掲載します。

全てが調和した上で仕上げのマクロという、私が夢見ていたコードにやっと出会ったと思いました。本当にすばらしいです。

ショートコーディング完結編

ここから感想文です。

コードゴルフの認知度って結構上がりましたよね。いろんなところで目にするようになりました。ショートコーディング本に書かれていない変態的テクニックもいろいろと存在しています。私はというと、あなごるは定期的に観察しているものの提出まで至っておらず、年に一回程度POJで遊ぶくらいです。ご存じの方もいらっしゃるかもしれませんが、最近はマラソンマッチにお熱です。

そんな私でも、今回紹介した大きなフィボナッチ数の問題だけはいつも頭の中にありました。もしショートコーディング本を書いている頃にこの問題に出会っていたとしても、マクロを使ったコードはまったく思いつかなかったと思います。いつ見ても、何度見ても素晴らしい。今日もこのコードを観ながらご飯食べてます

無知が原動力

アルゴリズム・データ構造・処理系等々、知らないことが多すぎて、何を見ても何を聞いても何をやっても、すべてが新鮮で本当に面白い。そんな状況でショートコーディング本を書いていました。そもそもショートコーディングなんてワードはコードゴルフというものを良く知らないが故に産まれたようなものですからね。

さて、今はと言いますと、当時と比べてそれなりに知識も技術もある程度レベルアップしました。もちろんまだまだ知らないことはあるのですが、新しいモノを生み出すモチベーションになるのかというとそれは少し微妙です。むしろコードゴルフを最近知って「面白い!」と感じた若い人がたくさん発信した方が、絶対良いと感じています。

記事も本も書かないのか

書かないということはたぶんなくて「Cゴルフ」みたいなもうちょっとコンパクトでお気持ち少なめの何かとか、シェル系のゴルフには興味があります。何よりPOJで遊ぶのは私の楽しみのひとつですので、ときどき何かやりたいなーと思っています。

本当に完結なのか

完結しない可能性が1つあります。今回紹介した2つのコード以外でもhttp://golf.shinh.org/reveal.rb?Fire+Emblem+4+RNG/tails_1551928236&cのようにマクロで最短を取っているものがあるのですが、これらの記録がすべてマクロを使わないコードに破られてしまった場合、「やっぱマクロ意味ないじゃん!」と言われてしまう可能性があります。その時はまたマクロの可能性について考える旅が始まるかもしれません(お願いだから始まらないで欲しい)。

というわけで

ここまで読んでくださった方はありがとうございました。どうしても今日投稿したい、今日しなかったら一生しないと思い、超特急で書きあげました。記述に雑な部分はいろいろとありますので、後日修正したいと思います。何か問題点等あればご指摘いただけると嬉しいです。ではまた!

おまけ

毎年2月11日は、ゆるっとPOJで遊んでいます。今年は3970番をやっていました。そしてまたushiodaさんに負けました…くやちいぃ!

POJ 3970 ランキング

競プロ日記012: AHC017の解法解説

AHC017 最終50位(暫定49位: 40.3G / 50.0G)の解法

THIRDプログラミングコンテスト2022(AHC017)の解法と,約一週間のコンテスト期間のアプローチについて解説(というか記録)しておきます.ヒューリスティック系コンテストの初心者・中級者や未来の自分の役に少しでも役立てばと思います.

問題の概要

N地点と,各地点を結ぶM個の道があります.この道のいくつかを補修工事するのに通行止めする必要がありますが,そうなると移動が不便になり,不満度が上がります.不満度はある地点から他の地点を移動する際の移動距離が増えるほど大きくなります.工事を行う日数Dと,一日に工事できる(通行止めできる)道の数の上限Kが与えられるので,これらの制約の中で可能な限り不満度を小さくするような工事計画を立てるという問題でした.

最初と2番目のアプローチ

まず最初にやったことは,1 1 1 ... 2 2 2 ... 3 3 3 ...のように適当に出力するコードの提出です.一度提出しておくと実際のスコアが得られるので,それを基準にローカルでのテスト環境を作ります.これをしておくと何度も提出してスコアの確認をする必要がなくなるので,基本的にやっておいた方が良いと思います.単にスコアボードを見て,似たような解答だろうなーという人のスコアを参考にするのも良いのですが,今回は同じ得点が何パターンかあったので,より正確に把握するために提出しました.

次にやったことはテスタの自作です.通常は付属のテスタをそのまま使うのですが,スコア計算の速度が遅すぎてストレスになることが目に見えていたからです.どうせ自分の解答プログラムを作るときにスコア計算を実装しなければならないですから,ここで慌てずある程度高速で正確にスコアが出せるように実装しました.高速とは言っても,まずは特別なことをせず愚直に実装します.

公式のテスタと同じスコアが出力できるようになったことを確認したらひとまずヨシとします.この時点では公式テスタと大して速度差はありませんが,問題本体のアイデアに詰まったときに気分転換として高速化をしていこうと決めました.

今回の自分ルール

本格的にマラソンマッチを始めて1年近く経ち,なんとなく心構えのようなものができてきたということもあり,今年は安定して結果を残すということを基本的な目標にしています.途中でくじけないように,簡単なルールを決めました.

  • スコアが満点に対して7割程度になるまでは貪欲解法を中心に
  • 高速化は煮詰まったときの気分転換としてやる作業とする
  • 直感に反することでも必ず試す
  • 最後まで諦めない

最初のアイデア

最初の時点でスコア計算に1秒近くかかっている状態だったので,まずは少ない計算量で良い計画/悪い計画を調べる必要がありました.いきなり躓いたので速い気分転換ということで,あまり頭を使わない高速化(2次元配列を1次元にとか)をちまちまと行いながら,「とりあえず到達不能な経路を作らないようにしよう」ということだけ目指すようにしました.

そう考えると割と簡単な話で,どこか1地点にだけ着目すればよいことになります.1地点からそれ以外の点に全て到達可能なら,他の点は調べなくてもちゃんと到達可能だとわかるからです.ではどんな点を選ぶべきかと考えました.少なくとも最初に1回,全ての点からの最短経路を求めているので,ついでに各点までの距離の合計も計算しておくことにしました.そして,距離の合計が最も小さい地点はおそらく盤面上の重心近くだろうという結論に至りました.

次の図はseed386を使って,この記事のために作成したものです.は全ての点の重心で,はその地点から他の点への最短距離合計が最も小さくなる点です.思った通り重心近くになっていました.

基準点を1つ決める

この考え方を元に,最初の解答プログラムを作成しました.Dが小さい(5とか)の場合に上手くいかないこともありましたが,あまり気にせずに100ケース程実行してみます.そうすると,最初の解より2~3倍スコアが良くなったので,この方針で大丈夫だろうと判断しました.提出すると,26G程度のスコアが出ましたが,実行時間は4秒くらいかかっていました.

いきなり詰まる

最初の解法ですでに4秒も消費しているのと,正直ノーアイデアに近かったので,早速高速化タイムに突入しました.方針としては,

  • この数日で次のアイデアが何か出るだろう
  • もしそれが上手くいけば,次のステップ(おそらく山登り)に進める
  • いろいろ準備をしておこう

です.つまり,高速化をして初期解を得るまでの時間を短くし,後半の山登り(道を入れ替えてスコア算出を繰り返す)をできるだけたくさんできるようにしておこうと考えました.とはいえこの時点では差分計算まで考えていなくて,小手先の高速化(queueをやめて固定長配列を使う等のチマチマしたもの)で,初期解を2秒くらいで得られるようにしました.さらに入れ替え山登りフェーズも追加して動かしてみると,到達できない点が現れるケースが無くなり,この時点で推定30G程度のスコアになりました.推定というのは,実際に提出せずにスコアボードの状況と自分のローカルテスト環境の結果を比較して算出したということです.

観測地点を複数にしたい

この辺りから実行時間を延長してみるということを試しました.スコアが上がれば単に高速化すればよいことがわかります.実際にやってみると,確かにスコアは上がりましたが少し物足りません.要するに,1点からの距離だけで考えると改善の見込みが無くなってしまうからです.そこで,今後も何らかの高速化によって実行時間を確保するという前提で基準にする点を増やしてみることにしました.ランダムな点も悪くはなかったですが,最終的に重心から同心円状に配置することにしました.次の図は3点を置いた場合と5点置いた場合です.重心からの距離は,半径の70%としました.

基準点を3つ
基準点を5つ

5つ置いた場合が最もスコアが良くなりましたが,制限時間の6秒では収まらなくなってしまいました.

苦し紛れの噓ヒープ - Liekstra's algorithm

本来ならここできちんと経路復元して差分計算を行うべきだったのですが,「早く完成品を仕上げたい」という誘惑にまけて小手先の高速化に徹してしまいました.今振り返って,失敗したと感じたのはこの点ですが,それなりに高速化されたので一応記録として残しておきます.通常のダイクストラ法はpriority_queueを使いますが,私は最初から普通のqueueを使っていました.普通のqueueだとNが大きいときに遅くなってしまうため,取り出す点の優先度は欲しい,しかしpriority_queueは遅い…ということで,妥協案として次のような操作を行いました.

  • 要素を普通にpush
  • 先頭の要素と末尾の要素を比較する
  • 末尾の値の方が先頭の値より小さければswap

割と単純な操作ですが,できるだけシンプルなコードにして実行回数を増やすことで強引に速度アップさせました.ちゃんと差分計算したものよりは圧倒的に遅いですが,これで何とか制限時間の6秒に収まるようになりました.この時点で推定35Gくらいです.

停滞期

噓ヒープを選択することによりアルゴリズム的な工夫をする意欲が無くなってしまい,長い停滞期に入ることになりました.細かい調整をして37~38G程度までは上がりました.順位的にも100位以内に入れそうだし,このままゴールしても良いかな…と一瞬思ったのですが,最初のルール『最後まで諦めない』に違反していると思い直して粘ることにしました.でも焼きなまし的な何かをしようとは思いませんでした.速度の問題で山を登り切れていない状態で焼きなまそうとしても無駄だからです.ここでもう一度最初のルールを思い出します.『スコアが満点に対して7割程度になるまでは貪欲解法を中心に』これに徹するべきだと思いなおしました.

初期解の改善

この時点で締め切りまで3時間程度になっていました.「まだ工夫していないことは何か?」をよく考えた結果,計画に追加する道の順番がずっとランダムな状態のままだったことに気付きました.こういうのはまばらに設置した方が大体良いと思い込んでいましたが,『直感に反することでも必ず試す』というルールに乗っ取って,地点ベースで優先度を与えることにしてみました.

  • 適当に1地点選ぶ
  • そこに接続しているすべて道を日程に組み込む
  • すべての道が組み込まれるまで繰り返す

これで少しだけスコアが上がりました(1.5%くらい).なるほど,では地点の選び方も順序を決めて,起点(とりあえずランダム)の道を全て選んだら,その道に接続する点を次の候補としてさらに道を追加する,つまりBFS的な順序で道を追加していく方針にすると,さらにスコアが上がりました.では起点はどこか?一番最初のアイデアに戻りました.1点だけ選んだ点は,「そこから各地点までの距離の総和が最小になる点」でした.

しかしスコアは上がりませんでした.良さそうなのに….しかしよく考えると,選ぶ道をかなり偏らせてスコアが上がったのだから,起点も距離の総和が最小なのではなく最大になる点を選べば良いのでは?と思いなおし,そのような点(つまり移動に最も不便な点)を起点にすると,最終的に5%くらいスコアが上がり40Gに到達しました.次の図は最終提出の解法で選んだ道の優先順位です.が起点で,明るい(黄色い)辺を優先的に計画に組み込んでいきます.

道の優先順位

反省と感想

やはり差分計算をサボったことが悔やまれます.後から実行時間を10倍にして1000ケーステストしたところ,スコアは8.6%程度増えました.ちゃんと高速化していればもう少し良い結果になったでしょうし,そこまでくるといよいよ焼きなましが使える段階になります.そこまで行きたかったというのが正直な感想ですが,今の自分の実力なら上出来だとも思います.とにかく安定して結果を残せるように,しばらくは今回のルールに加えて

を基本的に守ってコンテストに参加していこうと思います.次の目標はランキング1ページ目!

競プロ日記011: 今年のまとめ

今年からマラソンマッチを本格的に始めました。また、マラソン力強化のためにAtCoderのアルゴにも挑戦してみました。来年以降もしばらく続けてみようと思うので、将来の自分とこれから競プロをやってみようかと思う方々の参考として、これまでのまとめを記録しておきます。

TopCoder Marathon Match(MM)

2022年のMMレーティング

2013年に初めてマラソンマッチに参加して、この時のレーティングが1720だったので、まずはそれを目指しましたがなかなか成績が安定せず、最高でも1625でした。しばらくはギリギリYellow Coderが続きそうです。とはいえ、初参加時の何となく解法と比べると自分が何をしようとしているか理解した上で解答できている感覚はとても大きくなっているので、単純に数値で判断するのも良くないかなと感じています。

現状では問題点が割とはっきりしています。それは、「自分より上位の人とアイデアはさほど変わらないのに、常にスコアが1割くらい低い」ということです。どうしてかというと、最適化云々以前に自分の実装がゆるいからです。たとえばグリッド上で最短経路を求めるようなケースで複数の案がある場合、状況に応じてその中からより良さそうなものを見つける、あるいは優先順位がつけにくい場合に確率的な偏りの少ない案を選ぶとか、その辺りの対処がきちんとできていない場合が多かったです。

これは単なる一例ですが、あらゆる実装面でこのような緻密さが足りないことが多く、その結果探索の精度が落ちるため持ち時間の10秒を十分に活かしきれていません。今後はその辺を丁寧にやる訓練が必要です。スコアが上がらない場合にほぼ100%陥ってしまったのは、「上位の人はきっと天才的なアイデアをもっているからだ。自分のような凡人には思いつけないんだ😢」という思考になって自分の今やっていることを信じられなくなるというミスでした。この辺は自分の実績とか性格の影響が大きいと思いますが、もっとトレーニングと実戦経験を重ねて克服するしかありませんね。

AtCoderアルゴ

MMと短期AHC(AtCoderヒューリスティック)の力を鍛える一環として、短時間でのコーディング力を高めるためにアルゴの方にも何回か参加するようになりました。とりあえず今の実力だと、このまま続けると緑くらいまではいけそうかなーというとことで止まっています。マラソンに役立っているかというとあまり関係なさそうというのが現時点の感想で、どちらかというとこれはこれでちゃんと過去問とか対策をして真剣に取り組んだ方が絶対面白いような気がしています。もう少し時間にゆとりができたら、将来的にこちらも本格的にやってみたいかなという気分です。

現状では3~4問解けるくらいで順位が大体2500~3000辺りですが、同じ解答数でも解くスピードが速いと順位的には1000くらい差が出るみたいです。最初の4問を高速に解答できたと仮定して5問目を解いた場合にACできたりもしているので、基本的にはスピードアップのトレーニングをすべきですね。周囲を見渡すと1000問以上解いている方ばかりなので、来年はできるだけたくさん解いて、500問くらい解いたらABCに数回チャレンジしようと思います。

それから使用言語に結構悩んでいて、Pythonで書いてみたりC++で書いてみたりしていたのですが、当初のMMの力を鍛えるという当初の目的からぶれないようにC++一択でやろうと決心しました。まあMMのことを考えなくても、PythonでTLEしたときに「もしかしたらC++なら通るかも…」みたいな気持ちになりたくないという気分ということもありますが…。

来年に向けて

一番はやはりMMで過去の自分を超えることですね。それから、AHCの方もMMと同じくらい頑張りたいと思います。ただ、スケジュール的にAHCとMMが重なったり、AHCが短期コン(4時間)とかだとなかなか参加できなかったりするので、やっぱりMMよりは優先度が下がると思いますが。まあそれでも青には到達しておきたいと思います。

つい先日Psyhoさんによるヒューリスティック・ボットコンテストのための無料Tipsをまとめたところですが、こういうのを見ているとやはり自分自身でアウトプットを重ねてまとめるという作業は必須化のような気がします。まあ今の時点でもそれなりに自分用のドキュメントを作ってはいますが、どんどん公開していったほうが良いかもですね。

ではまた来年!