2020年3月30日月曜日

ARC 049 C - ぬりまーす

タイプ2の条件はひとまず無視して考える。

タイプ1のそれぞれの条件(x,y)(x, y)についてyyからxxに辺を張ったグラフGGを与えたとき、問われていることは「GGから入次数が00の頂点を次々に消していくとき、最大で何個消せるか?」ということだった。この個数に消す順序は関係ないので、実際に自由に消していって確かめればよい。具体的には、入次数が00の頂点をすべてキュー(あるいはスタック)に入れてBFS(あるいはDFS)をすればO(N+A){O}(N+A)でできる。手順としては、キューから頂点を取り出したら、隣接頂点の入次数を1ずつ下げて、入次数が00になったものをキューに入れればよい。あるいは、この制約ならBellman-Fordっぽく入次数が0の頂点の隣接頂点の入次数を下げる操作をNN周繰り返す、でもよさそう。(O(NA){O}(NA))

タイプ2の条件(u,v)(u, v)については、

  1. uuを選ばないとする(=頂点uuは存在しないものとする)
  2. vvを選ぶ場合にはuuを先に選んでいなくてはならないとする。

の2通りを調べれば十分で、後者はタイプ1と同じなので上の問題に帰着できている。タイプ2の条件は十分に少ないので、2B2^B通りのグラフを調べればよい。O((N+A)2B){O}((N+A)2^B)

2020年3月27日金曜日

競技プログラミングでCommon Lispを使っている人とこれから使うかもしれない人のために

Qiitaからこちらに転載した。AtCoderの環境の更新に合わせて、転載当時の内容から多少の変更を加えている。


表題の通り、競技プログラミングに参加しているLisperとこれから参加するかもしれないLisperのために、必要な情報を一通りまとめています。

コンテストサイトの選択

AtCoderではCommon Lispが使えますし、CLで投稿できる大手コンテストサイトはCodeChefyukicoderCS Academyなど他にもいくつかあります。日本語の問題文があって運営の質が高くてコンテストが頻繁に開催されていて……というような点を考えていくと、競プロに興味を持った日本語圏のLisperにとってAtCoderは第一の選択肢といってよいでしょう。第二はyukicoderとCodeChefです。以下の解説は、特に断りがなければAtCoderを前提にしています。

Lispで競プロすることについて

本題ではありませんが、セクションを立てて言及しておくことにします。というのも、競技プログラミングに熱心なLisperはほぼ皆無に見えるからです。競プロに興味のある人の中にも、そもそもLispに向いているジャンルなのかどうか不透明に思う人がいるかもしれません。

端的に言うと、わたしのレベルではパフォーマンスの面でデメリットを感じたことはありません。より高レートの状況でどうなるかは知りえないことですが、悲観する材料は特に見つかっていません。終わり。

……でもよいのですが、いくつかのポイントに絞って説明してみます。

○: 柔軟な構文

素早く・簡潔に・柔軟にDSLを生やすことにかけてはLispは今なお優れた言語であると言えるでしょう。競プロの文脈でもそれは役には立ちます。もっとも、そういったものは競プロにおけるパフォーマンスにさほど影響しないとも思います。競プロで大事なのは考察力です。

○: スピード

最良ではないが、悪くもないです。AtCoderにおける速さという観点で言語を雑に分類すると

  • A群: C/C++/Rustなどの、システムプログラミングのための言語
  • B群: OCaml/Haskellなど、ネイティブコードへのAOTコンパイルを前提とするその他の言語。Java/C#などの高速VMを持つ言語。
  • C群: Python/Ruby/PHPなどのスクリプト言語

で性能はおおむねA>B>Cということになりますが(あくまで雑な分類です)、LispはBの中で速いほう、くらいの位置づけです。状況にもよるので絶対的な比較は難しいですが、速度面では「Java/C#と同程度かわずかに有利」くらいの認識を持っています。そして、Java/C#専のまま強くなる人は特に珍しくもありませんから、スピードの問題はさほど心配しなくてよいです。

実情を言うと、ビギナーコンテストの問題では正しいロジック(=最悪計算量が制約に適っているロジック)で書けていればTLE(Time Limit Exceeded)することはめったにありません。TLEするのは、最適化が足りないからではなく、計算量の考察がおかしいからと思ってほぼ間違いないです。ほとんどの場合、最適化のための基本的な宣言すら必要ないと思います。それ以上のレベルの問題でも、SBCLが遅くて通せないとか、あるいは最適化しても制限時間ぎりぎりになるという事態に陥ったことは一度もありません。そもそもAtCoderではJavaで通らない問題は出題されないそうなので、SBCLで通せないというケースは考えにくいです。

CS AcademyやHackerEarthなど、事前コンパイルなしで--scriptで実行されるサイトではその分の時間(おおむね50ms~200ms程度)が上乗せされますが、このような環境でも問題になることはまれです。

△: 標準機能・ライブラリの充実度

Common Lispの標準ライブラリは貧弱です。平衡二分木もなければ、優先度付きキューも両端キューもないし、それどころかただのキューすらないので、あらゆるデータ構造をいちいち用意しなければなりません。[1] 良い所もそれなりにはあって、多倍長整数が自然に扱えるのでオーバーフローの処理をちょっとくらいサボってもOKなのは悪くない点ですし、有理数や複素数も競プロでは活躍します。stringが単に文字の配列であるという時代遅れな仕様も、競プロではむしろ望ましいです。しかし、総合的に見てやや不十分なのは否めないでしょう。

もっとも、競プロerはどの言語を使っていてもけっきょくは自前実装のデータ構造を少しは用意することになるので、二分ヒープを自分で書くことくらいは誤差レベルとも言えます。ただ、自分でデータ構造を書く場合に、効率のために要素型で静的ディスパッチするようなコードを書くのが大変という別の問題はあります。CLにはパラメータ多相を自然に導入する方法の決定版がいまだにないので…… (競プロに限った話ではありませんが、ジェネリクスのデファクトがないのはCLのアキレス腱だと思っています。)

△: コンテストサイト・オンラインジャッジの選択肢

Common Lispは最大手のCodeforcesで使えませんし、もちろんTopCoderでも使えません。日本の競プロer御用達のAOJも対応していません。したがって、LisperはAtCoderyukicoderCodeChefを中心に利用し、Google Code Jamがあれば出て、足りなければCS AcademyHackerRankなどを使うことになります。それが不満ならC++やJavaを選んだほうがよいでしょう。ちなみにわたしはまあまあ不満です。

✕: 言語コミュニティの規模

Lisperの数が極めて少ないため、参考にできるコードがありません。AtCoderで400点以上の問題だとLispの提出はゼロであることが多いです。 困ります。もちろん、根幹のロジックを理解するために読むのはなんの言語でもよいので、C++やPythonの提出コードを見ればよいです。しかし、他人のコードを読むのは、整理された間違えにくい書き方を学ぶためという側面が強いので、言語が同じであるほうが参考にしやすいのも確かです。

もっとも、この問題はあなたが参加すれば解消されるとも言えます。ぜひやってみませんか。

いくつかの問題と解決策

実践で生じるトラブルとその回避方法を挙げます。「いくつか」と題しましたが、今のところ重要なものは最初の1つだけです。また、入出力関係の話題はまとめて後のセクションで扱います。

スタックサイズが足りない(重要度:◎)

SBCLのデフォルトのスタックサイズは2MBであり[2]、コールスタックがこのサイズを超えるとエラーになります。(末尾呼び出しはジャンプに変わるので安全と思ってよいです。)

2MBというサイズですが、目安としては105の深さで再帰すると確実にcontrol-stack-exhaustedする、104ならたぶんOK、くらいです。深さを見積もって無理そうなら再帰を使わなければ良いのですが、DFSなどを手動スタック+ループで書くのはやや面倒と感じる人も多いでしょう。幸運なことに、この問題については解決策が確立しています。スタックサイズを増やしたSBCLを子プロセスとして呼び出せばよいのです。ファイルの冒頭に次のフォームを置きましょう。

#-swank
(unless (member :child-sbcl *features*)
  (quit
   :recklessly-p t
   :unix-status
   (process-exit-code
    (run-program *runtime-pathname*
                 `("--control-stack-size" "256MB"
                   "--noinform" "--disable-ldb" "--lose-on-corruption" "--end-runtime-options"
                   "--eval" "(push :child-sbcl *features*)"
                   "--script" ,(namestring *load-pathname*))
                 :output t :error t :input t))))

こうすればスタックサイズの大きいSBCLが代わりに処理してくれます。

  • 終了コード絡みの処理は、子プロセスでエラーが起こった時に親プロセスのエラーにするためにやっています。そうしないと子プロセスでエラーが起こってもREと表示されなかったりします。
  • #-swankは必要ではありませんが、わたしはSLIMEでコーディングしているので、編集中にこのフォームが評価されないようにするために書いています。
  • 直接--scriptで実行する仕様でも、コンパイルしてから実行する仕様でも、どちらでも通るようにしています。ほとんどのコンテストサイトで使えます。forkを呼ぶことになるので、DMOJのようにシステムコールを制限しているジャッジでは使えないようです。
    • 使えることを確認: AtCoder、yukicoder、CodeChef、Toph、CS Academy、HackerRank、HackerEarth、SPOJ、Kattis、GCJ
    • 使えないことを確認: DMOJ
  • 今のところ256MBで足りなかったことはありませんが、128MBで足りなかったことはあります。スタックサイズが足りないとき、必ずしもREと表示されるわけではなく、TLE等になることもあるので気をつけましょう。

やや強引なハックではありますが、2年ほど使ってこの方法の欠陥らしきものは見つかっていません。数ms程度ロスしてわずかにメモリ消費が増えるだけですし、常に置いておいても特に問題ない程度のものです。

また、スタックサイズを書き換えてから新たにスレッドを作るという方法も考えられます:

(defun main () ...)
 
(setf (sb-alien:extern-alien "thread_control_stack_size" sb-kernel::os-vm-size-t)
      (* 256 1024 1024))
(sb-thread:join-thread (sb-thread:make-thread #'main))

ただし、SBCLは起動後にスタックサイズを変更する機能を提供していないので「いちおう動く」だけではあります。最近のランタイムを眺めた限り、普通の使い方では壊れないだろうと思っていますが。(こちらもDMOJでは使えません。)

ベクタのソートが遅い(重要度:〇)

(declare (inline sort))しましょう。SBCLのsortsb-ext:maybe-inlineなので、デフォルトではインライン化されません。[3] インライン化されないと、ベクタの要素型を考慮した静的ディスパッチがされないので、常識的なスピードが出ません。

ただ、SBCLのソートはベクタに対してはヒープソートなので、いずれにせよスピード重視ではないし、標準のソートは範囲指定ができないという問題もあります。[4] マージソートなりクイックソートなりを用意しておくのもよいかもしれません。

リストをsortする場合(マージソート)に関してはさらに(declare (inline sb-impl::stable-sort-list))が必要だったりします。(ソートが間に合うか不安なくらいに入力が大きいケースではリストで持たないほうがよいと思いますが。)

また、普段からSBCLのどの関数がどの場合にインライン化(インライン宣言やdefine-source-transformdeftransformdefine-vopによる変換)されるのか、型を考慮した最適化があるのかを気にしておきましょう。SLIME上なら、標準の関数に対してM-. slime-edit-definitionするとSBCLのソースにジャンプできます。

末尾の改行の扱いが一貫していない(重要度:△)

改行の扱いはコンテストによって異なるのですが、AtCoderのratedに出るだけならあまり気にする必要がないでしょう。あらゆるコンテストに出るなら、一般論としては以下のような指針があります。重要な順に

  1. 出力の末尾に改行がなかったり、余分な改行を入れた時に通るかどうかは不定と思ったほうがよいです。必ず正しい位置に正しい数の改行を送っておきましょう。
  2. 入力の改行コードが#\Linefeed限定と思わないほうがよいです。
  3. 入力の末尾に改行があるという仮定を置かないほうがよいです。

2番目については、CodeChefのようにCRLFが入っているかもしれないことを明言しているサイトもあります。SBCLのread-lineはCRLFをうまく扱えないので、場合によってはstring-trimなどをする必要があるかもしれません。(とはいえ、この辺は「ちゃんとした」コンテストになるほど気にする必要性が下がっていくので、わたしもあまり真剣に対処していません。CodeChefでも最近のratedで困ったことはないです。)

CodeChef: ヒープサイズが小さすぎる(重要度:◎)

CodeChefではメモリは問題に関係なく4GBくらいまで使えるという情報がありますが、SBCLはなぜか--dynamic-space-size 48起動されているのでそのままだと実用するのが厳しいです。上述のスタックサイズを増やすヘッダでヒープサイズも増やせます。(さすがにまずそうな仕様ですが、わたし以外のLisperを見たことがないので報告をさぼっています。)

AtCoder: MPFRが使えない(重要度:△)

double-float以上の精度が想定される問題はほとんど見ないですが、あれば役に立つことはよくあります。SBCLにはMPFRのモジュール(sb-mpfr)がありますが、AtCoderの環境ではrequireしてもlibmpfrが見つからなくて使えません。ジャッジ環境にlibmpfr自体は存在していて、requireの前に(sb-alien:load-shared-object "libmpfr.so.6")でロードするとよいです。自分が使っているヘッダの例はここにあります。

SBCLのバージョン間の差異について(重要度:△)

サイトによってSBCLのバージョンが異なるので、メモしておきます。該当のバージョンより上にある項目が注意点になります。重要な差異は特にないのですが、ジャッジのバージョンと近いものを使ったほうが細かいトラブルが起こりにくいです。Roswell等で切り替えるのがよいでしょう。

  • AtCoder: version 2.0.3
  • 負数に対するmodの返り値が正数であると推論されないことがあります。うまくいかない場合はtheを使いましょう。version 1.5.7でたぶん解決。
  • make-arrayで多次元配列を作る場合、element-typeの推論が正しくできないことがあります。sb-c:make-array-header*のderive-typeをコピペすれば解決しますが、そんなことをしなくても必要なら型を手打ちすればよいです。version 1.5.6で解決
  • yukicoder: version 1.5.5
  • *read-default-float-format*rationalを指定できない。version 1.5.2で解決。
    • rationalが使えるのはSBCL独自の仕様ですが、便利なので載せています
  • maphashが事前指定したサイズに対するO(N){O}(N)。詳細はここに書きました。version 1.5.5で過去に到達した最大要素数に対するO(N){O}(N)になりました。どちらの仕様でも、「maphashを何回も呼ぶけど合わせてO(N){O}(N)」みたいな場合に本当にO(N){O}(N)かどうか気を付けたほうがよいです。
  • array-element-typeが定数伝搬しません。インライン関数の中で同じelement-typeの配列を作りたい場合等に困るかもしれません。array-element-typeのdeftransformをコピペすれば解決しますが、そんなことをしなくても必要なら型を手打ちすればよいです。version 1.5.0で解決
  • CS Academy: version 1.4.16
  • HackreRank: version 1.4.15
  • CodeChef: version 1.3.13
  • HackerEarth: version 1.3.1

SBCLはかなり昔から安定しているので、処理系が落ちるようなバグはまず無いと思っていいです。私自身は、AtCoderでdynamic-extent絡みのバグを踏んだことが1回ある程度です。(詳細を忘れました)

入出力のTips

大量の入出力をこなす場合(目安としては106文字くらいから)、全部(read)して一行ずつ書き出すような素朴すぎる方法を使うと無視できないロスになることがあります。このセクションでは効率をある程度考慮した方法を紹介します。しかし、ベストプラクティスと言えるような方法は特に確立していないので、以下のポイントを頭に入れつつ各自で好きなようにしましょう。

read-lineを高速化する(使用頻度: △)

read-lineは遅いとよく言われます。遅い理由として、read-lineは呼び出すたびにコンシングするからという説明を見たことがありますが、わたしの知る限りでは、律速しているのはマルチバイト文字を考慮したread-charです。AtCoderでは(おそらく)ASCIIしか出ないので、バイト単位で読んでsimple-base-stringのバッファに読みだす関数が役に立つかもしれません。SBCLの標準IOはbivalentなので、read-byteが使えます:

(declaim (inline read-line-into))
(defun read-line-into (buffer-string &key (in *standard-input*) (term-char #\Space))
  (declare (inline read-byte))
  (loop for c of-type base-char =
           #-swank (code-char (read-byte in nil #.(char-code #\Newline)))
           #+swank (read-char in nil #\Newline)
        for idx from 0
        until (char= c #\Newline)
        do (setf (char buffer-string idx) c)
        finally (when (< idx (length buffer-string))
                  (setf (char buffer-string idx) term-char))
                (return (values buffer-string idx))))

  • term-charは入力終了を表す文字として使います。例えば#\Spaceの場合、1 2 3という行を読むとbuffer-string"1 2 3 "で上書きするわけです。buffer-stringの長さがちょうど5なら"1 2 3"になります。
  • SWANKの標準IOはbivalentでないため、SLIME上で編集している時は代わりにread-charを使う必要があります。
  • 読み込みの終了位置を多値で返しています。
  • (declare (sb-kernel:ansi-stream in))を宣言するとさらによいかもしれません。

上の関数はread-lineよりずっと速いですが、いちいちバッファを作るのが面倒であれば、サイズだけ指定してバッファは自動で用意するという方法もあるでしょう:

(defmacro buffered-read-line (&optional (buffer-size 30) (in '*standard-input*) (term-char #\Space))
  (let ((buffer (gensym))
        (character (gensym))
        (idx (gensym)))
    `(let* ((,buffer (load-time-value (make-string ,buffer-size :element-type 'base-char))))
       (declare (simple-base-string ,buffer)
                (inline read-byte))
       (loop for ,character of-type base-char =
                ,(if (member :swank *features*)
                     `(read-char ,in nil #\Newline) ; on SLIME
                     `(code-char (read-byte ,in nil #.(char-code #\Newline))))
             for ,idx from 0
             until (char= ,character #\Newline)
             do (setf (schar ,buffer ,idx) ,character)
             finally (when (< ,idx ,buffer-size)
                       (setf (schar ,buffer ,idx) ,term-char))
                     (return (values ,buffer ,idx))))))
  • バッファサイズはコンパイル時に与えるべきなので、マクロにしました。
  • (速度を気にしなければ)終了位置はfill-pointerでコントロールしたいところではあります。読み込み時だけarray-storage-vectorを使う、とかいろいろやり方は考えられますが、今のところわたしはあまり頑張っていません。

read-lineとの比較ですが、105行以上のデータを読み込むなら200msくらいは得できるイメージです。最大で800msほど速くなるパターンを経験しています。

ただ、リードにしろパースにしろ、1文字ずつ読むならストリームから直接処理したほうが良くなりますし、read-sequenceなどを使う手もあるでしょう。以下にそのやり方も紹介しますが、read-lineや上の関数が役に立つこともよくあります。ストリングを介した行単位の処理は柔軟で、変則的な入力にも対応しやすいためです。

整数を高速に読み込む(使用頻度: ◎)

大量の整数を読み込んで処理する問題は頻繁に出ます。整数が10510^5個を超えたあたりからreadに掛かる時間が無視できなくなってきます。

(declaim (ftype (function * (values fixnum &optional)) read-fixnum))
(defun read-fixnum (&optional (in *standard-input*))
  (declare (inline read-byte)
           #-swank (sb-kernel:ansi-stream in))
  (macrolet ((%read-byte ()
               `(the (unsigned-byte 8)
                     #+swank (char-code (read-char in nil #\Nul))
                     #-swank (read-byte in nil 0))))
    (let* ((minus nil)
           (result (loop (let ((byte (%read-byte)))
                           (cond ((<= 48 byte 57)
                                  (return (- byte 48)))
                                 ((zerop byte) ; #\Nul
                                  (error "Read EOF or #\Nul."))
                                 ((= byte #.(char-code #\-))
                                  (setq minus t)))))))
      (declare ((integer 0 #.most-positive-fixnum) result))
      (loop
        (let* ((byte (%read-byte)))
          (if (<= 48 byte 57)
              (setq result (+ (- byte 48) (the (integer 0 #.(floor most-positive-fixnum 10)) (* result 10))))
              (return (if minus (- result) result))))))))

(defun read-bignum (&optional (in *standard-input*))
  (declare (inline read-byte)
           #-swank (sb-kernel:ansi-stream in))
  (macrolet ((%read-byte ()
               `(the (unsigned-byte 8)
                     #+swank (char-code (read-char in nil #\Nul))
                     #-swank (read-byte in nil 0))))
    (let* ((minusp nil)
           (result (loop (let ((byte (%read-byte)))
                           (cond ((<= 48 byte 57)
                                  (return (- byte 48)))
                                 ((zerop byte) ; #\Nul
                                  (error "Read EOF or #\Nul."))
                                 ((= byte #.(char-code #\-))
                                  (setq minusp t))))))
           (mid-result 0)
           (index-mod18 0))
      (declare (fixnum mid-result)
               ((integer 0 19) index-mod18)
               (integer result))
      (loop
        (when (= index-mod18 18)
          (setq result (+ mid-result (* result #.(expt 10 18))))
          (setq mid-result 0)
          (setq index-mod18 0))
        (let ((byte (%read-byte)))
          (unless (<= 48 byte 57) (return))
          (setq mid-result (+ (- byte 48) (* 10 (the (mod #.(expt 10 17)) mid-result))))
          (incf index-mod18)))
      (setq result (+ mid-result (* result (expt 10 index-mod18))))
      (if minusp (- result) result))))
  • 上と同様の理由で、SLIME上とそれ以外で入力方法を変えています。

read-fixnumは、読み込む整数がfixnum(=(signed-byte 63))とわかっている場合のreadの代替であり、わたし自身、ここに挙げた関数の中でもっとも頻繁に使っています。

read-bignumも用意してありますが、ほとんど使ったことがありません。AtCoderでは多倍長整数をそのまま扱えないと厳しい問題はそうそう出ないと思いますし、出ても1つ読むだけならreadが十分速いです。[5] どのくらい大きな整数を扱えるかは感覚で覚えるしかありませんが、目安としては10100000を読むのに200ms掛かると覚えておくとよいです。

出力をstring-output-streamにためる(使用頻度: ○)

出力データが大量にある場合、一行ずつ*standard-output*に出していくとロスが大きくなることがあります。そういう時はstring-output-streamを使いましょう:

(let ((out (make-string-output-stream :element-type 'base-char)))
  (dotimes (i n)
    (princ data out)
    (terpri out))
  (write-string (get-output-stream-string out)))

これは場合によってはかなり効きます。マクロ化したければ次のような感じでしょうか。(わたし自身はマクロではなくスニペットで解決しています。)

(defmacro with-buffered-stdout (&body body)
  (let ((out (gensym)))
    `(let ((,out (make-string-output-stream :element-type 'base-char)))
       (let ((*standard-output* ,out))
         ,@body)
       (write-string (get-output-stream-string ,out)))))

ただし、メモリ制限が厳しい場合は、string-output-streamに全部ためるとMLE(Memory Limit Exceeded)になってしまうことがあります。AtCoderの最近のコンテストでは256MB~1GB以外の制約を見たことがないので、この点を気にする必要はないと思いますが、過去問を解く際には問題になることがあるかもしれません。

小数を出力する(使用頻度: △)

SBCLのformat~F~$は遅いです。特に、~,5F~5$のように小数点以下の桁数を指定した場合、出力だけで時間切れするレベルのボトルネックになったりします。

単純な解決策としてはwriteprincをそのまま使うことが挙げられます。この場合、出力に指数表記が混じることになりますが、AtCoderは基本的には指数表記で回答しても問題なかったはずです。(初期の問題に通らないものがあった気もします。) ただし、指数の記号はもちろんeでないといけません。実数を扱う問題では、精度を考慮するとdouble-floatが必要になることも多いのですが、その場合は*read-default-float-format*を変更する必要があります:

CL-USER> (write 1.4f10)
1.4e10
CL-USER> (write 1.4d10)
1.4d10
CL-USER> (let ((*read-default-float-format* 'double-float))
           (write 1.4d10))
1.4e10

なお、わたし自身はdouble-float通常の表記で高速に出力する関数も用意してはいます。AtCoder以外のサイトだと、writeでも遅すぎたり、あるいは出力桁数を制限しないと出力サイズが上限を上回ってしまったりという経験があります。

小数を高速に読み込む(使用頻度:✕)

小数を大量に入力する問題は意外にもまれです。SBCLのリーダーをいじったものをいちおう用意していますが、あまり試していません。

なお、readで読む場合、デフォルトではsingle-floatでパースされるので意図せず精度が落ちてしまう可能性はあります。これもやはり*read-default-float-format*を変更すればよいです。

小数を有理数として読み込む(使用頻度:✕)

「浮動小数点数の誤差の扱いが面倒だが有理数を使えば解決」という問題はたまにあります。しかし、入力も小数の場合は、readしてからrationalizeするとそもそもreadの時点で誤差が入るので、それについて考える必要が生まれます。SBCL 1.5.2以降であれば*read-default-float-format*rationalが指定できるので、これを使うと何も考えなくてすむかもしれません。


  1. ただし、SBCLではただのキューで良ければsb-queueが十分使えます。これはスレッドセーフなキューなので競プロ用途ではやや「リッチすぎる」わけですが、パフォーマンス上の問題は特にないことを確認しています。また、src/code/target-bst.lisp(バージョン1.5.0より前)かsrc/code/avltree.lisp(1.5.0以降)に平衡二分木が一応あったり、src/code/timer.lispに二分ヒープがあったりしますが、これらはユーザーが使うことを意図したものではないでしょうし、汎用的に作られてはいません。 ↩︎

  2. AtCoder上のSBCLについては、コードテストで(print (sb-alien:extern-alien "thread_control_stack_size" sb-kernel::os-vm-size-t))を送れば確認できます。 ↩︎

  3. SBCLのマニュアルにはなぜか載っていませんがCMUCLのマニュアルにはあります。標準の関数のうち、SBCLでmaybe-inlineなのはacons, both-case-p, digit-char-p, float-precision, get-properties, getf, intersection, keywordp, lower-case-p, nintersection, nset-difference, nset-exclusive-or, nsublis, nsubst, nsubst-if, nsubst-if-not, nthcdr, nunion, remprop, set-difference, set-exclusive-or, sort, stable-sort, sublis, subsetp, subst, subst-if, subst-if-not, tailp, tree-equal, union, upper-case-pです。ソート以外で覚えておきたいのはdigit-char-pでしょうか。また、maybe-inlineと似ていますが、inline宣言されたあとnotinline宣言されている関数としてunread-char, listen, read-char, read-byteがあり、これらも明示すればインライン化されます。 ↩︎

  4. 一応、displaceしたベクタをソートして副作用を利用するという方法はあります。おそらく挙動は未定義ですし、simpleでなくなるので遅いですが、SBCLでは意図通りに動くはずです。 ↩︎

  5. いっぽう、SBCLのparse-integerは多倍長整数をパースするのが遅いので注意。10100000を読むのに2秒以上かかります。 ↩︎

2020年3月18日水曜日

CODE FESTIVAL 2014 予選B C - 錬金術師

最大流で解ける。source, sink, S1, S2と26種類のアルファベットに対応する頂点の計30頂点を用意する。source→S1, source→S2に容量NNの辺を張り、S1から各アルファベットccに向かってS1に含まれているccの総数に等しい容量の辺を張り、S2についても同様にする。最後に、各アルファベットccからsinkに向かってS3S3に含まれているccの総数に等しい容量の辺を張る。このネットワークに容量2N2Nのフローが流せればYESということになる。


第一感がフローなのは成長なのか退化なのか。

2020年3月17日火曜日

TTPC 2015 L - グラフ色塗り

赤い辺だけ張ったグラフに最大フローを流したあとの残余グラフを考える。このグラフにできるだけ多くの青い辺を追加しつつ、sourceがsinkにつながらないようにする必要がある。これは青い辺をすべて張ってから最小カットを求めることと同等である。したがって、2回最大フローを流せばよいことになる。

ただ、赤い辺の容量を1にして流すと、入力例3のように赤い辺がボトルネックになって正しい答えが得られない。赤い辺は十分に大きい容量(1000とか)にする必要がある。


バチャ中に適当にやったら通ってしまったのだけど、最後のポイントを納得できていない感じがする。

TTPC 2015 G - titech分離

文字の数があわないなどの自明ケースは先に考慮しておく。

実際にtitechという文字列をいくつか作りかけながら、文字列SSを先頭から走査していくことを考える。たとえばtitという文字列が2つできているときに次がeだったら、どちらに追加しても区別できない。したがって、長さxxの列をdp[x]\operatorname{dp}[x]個持っているとしてdp[0]:=Sの長さ/6\operatorname{dp}[0] := Sの長さ/6と初期化しておけばよく、eを見たらdp[3]\operatorname{dp}[3]を1減らしてdp[4]\operatorname{dp}[4]を1増やすという感じで遷移すればよい。ほかの文字についてもだいたい同じで、最後まで走査できたらYesということになる……のだが、問題は文字tで、010 \rightarrow 1232 \rightarrow 3の二通りに追加できる。これはDFSで全探索すればよさそう。

しかし、計算量がどうなるかわからなくてしばらく逡巡していた。自明な評価から順に

  1. titechは最大で16個作れてこの時tは32個だからDFSの末端の数は23241092^{32} \simeq 4 \cdot 10^9以下。
  2. 010 \rightarrow 1232 \rightarrow 3は同じ回数だけ選ぶ必要があるから(3216)6108\binom{32}{16} \simeq 6 \cdot 10^8以下。
  3. 最初は010 \rightarrow 1、最後は232 \rightarrow 3を選ぶ必要があるから(3015)1.5108\binom{30}{15} \simeq 1.5 \cdot 10^8以下。
  4. 010 \rightarrow 1を選んだ回数が常に232 \rightarrow 3を選んだ回数以上でなければならないから、カタラン数でC15107C_{15} \simeq 10^7以下。

逆から貪欲でいいらしい。なんと……

2020年3月16日月曜日

CodeChef Match Challenge 2020 - Inverse of a Function

まずは与えられた数列の美しさを求める方法を考える。数列(ai)(a_i)ii桁目が11の数がd(i)d(i)個含まれているとき、d(i)d(i)個から奇数個選べば残りの数をどう選んでもxorのii桁目が11になる。つまり、ii桁目だけに限ってxorの総和を取ると、d(i)>0d(i) > 0なら2i2^i

2Nd(i)((d(i)1)+(d(i)3)+...)=2Nd(i)2d(i)1=2N1 \begin{aligned} & 2^{N-d(i)} \biggl( \binom{d(i)}{1} + \binom{d(i)}{3} + ... \biggr) \\ = & 2^{N-d(i)} 2^{d(i)-1} \\ = & 2^{N-1} \end{aligned}

個足されることになる。すなわち、ii桁目が立っている数が(ai)(a_i)の中に1つでもあれば、ii桁目の総和は2N12i2^{N-1} 2^iであり、ひとつもなければ00である。したがって、全体の総和はbitwise orを使って

2N1i=1Nai 2^{N-1} \bigvee_{i=1}^{N}a_i

と表すことができる。これが(ai)(a_i)の美しさである。

次にF(N,B):=F(N, B) := 長さNNで美しさBBの数列の数、を求める方法を考える。上の式を見ると、BBが与えられたときにB/2N1B/2^{N-1}の立っているビットを考えればよいことがわかる。つまり、B/2N1B/2^{N-1}ii桁目が11なら、NN個の数のうちの少なくとも11つの数のii桁目が11であればよく、ii桁目に限定したNN個の数の選び方は2N12^N-1通りとなる。いっぽう、B/2N1B/2^{N-1}ii桁目が00なら、NN個の数の対応するii桁目はすべて00でなければならないので、選び方は11通りである。したがって、整数xxの立っているビットの総数をc(x)c(x)で表すとき、

F(N,B)={(2N1)c(B/2N1)(B2N1で割り切れる)0(それ以外) F(N, B) = \begin{cases} (2^N-1)^{c(B/2^{N-1})} & \qquad (Bが2^{N-1}で割り切れる)\\ 0 & \qquad (それ以外) \end{cases}

と書ける。

さて、解くべき問題はF(N,B)Xmod  MF(N, B) \equiv X \mod Mを満たす最小のBBを求めるというものだった。上の式を見ると、(2N1)kXmod  M(2^N-1)^k \equiv X \mod Mを満たす最小のk0k \ge 0が求まれば、kk個の11を並べてN1N-1回左シフトしたものが求める最小のBBということになる。これで離散対数問題に帰着できた。

ただし、上の式からわかるように、N2N \ge 2の時は美しさ11の数列がないので、X=0X=0に対しては常に11が答えということになる。(もちろんM=0M=0なら00)。

CodeChef March Challenge 2020 - Break

ひたすらゲームの性質を観察する。

S = 1

先手の手の列をx1,...,xNx_1, ..., x_N, 後手の手の列をy1,...,yNy_1, ..., y_Nとする。

まず、先手は一手目より小さいカードを出せない。つまり、先手の一手目は必ず最小のカードでなければならない。

後手も先手の一手目以下のカードは出せない。

先手の有効な手の列でxi>xi+1(i2)x_i > x_{i+1} (i \ge 2)となっている部分があったら、入れ替えが可能であることを示す。後手はxi+1x_{i+1}と等しいカードをiiターン目までに出したはずだが、ちょうどiiターン目には出せないので、iiターン目より前に出しているはず。したがって、xi,xi+1x_i, x_{i+1}の入れ替えと同時にyiy_iyi+1y_{i+1}も入れ替えれば有効な列のままである。

以上より、先手の手の列としてはカードを昇順にソートしたものだけ調べればよい。これに対する後手の手の列y1,...,yNy_1, ..., y_Nも昇順としてよいことを示す。yi>yi+1y_i > y_{i+1}となるiiがあるとき、入れ替えてもそれぞれのxi,xi+1x_i, x_{i+1}との大小関係は維持される。あとはyi+1y_{i+1}を先に出すことによってxi+1x_{i+1}が出せなくなる場合があるか検討する必要があるが、その場合はxi+1=yix_{i+1} = y_{i}が成り立つので、xi+1=yi>yi+1>xi+1x_{i+1} = y_{i} > y_{i+1} > x_{i+1}となって矛盾。

したがって、先手、後手のカードをそれぞれ昇順にソートして、有効な手の列になっているかどうか確かめればよい。

S = 2

盤面から消えたカードはbeatしたorされたものだけなので、同じ数字が書かれたカードがNN枚より多くあると引き分けることはできない。

いっぽう、以下のテクニックを使えば先手と後手のカードをだいたい任意に入れ替えることが可能なので、同じカードがNN枚以下の場合はだいたいYesということになる。

  1. 先手提出 → 後手giveup → ... → 先手提出 → 後手giveupを繰り返したあと、
  2. 先手提出 → 後手提出 → 先手giveup で攻守を交代して
  3. 後手提出 → 先手giveup → ... → 後手提出→ 先手giveup

先手と後手は1と3で出したカードを交換したことになる。このあとは後手提出 → 先手提出 → 後手giveup→先手提出 → 後手提出 → 先手giveup→……を繰り返しながら、カードを2枚ずつ捨てていけばよい。

しかし、コーナーケースがいくつかある:

  • N=1,2N=1, 2の場合、上のテクニックを使おうとすると途中で先手のカードが尽きてしまうので、特別に処理する必要がある。
  • 先手の初期手札の最小値\ge後手の初期手札の最大値がなりたっていて、先手または後手のカードがすべて同じ数の場合、上のテクニックがうまくいかない。

初手giveupができないルールと、カードがなくなった瞬間に終了するルールのおかげでだいぶ面倒になっている。

CodeChef March Challenge 2020 - Winning Ways 2

区間[l,r][l, r]に登場する同じ数をまとめてそれぞれ個数を数える。grundy数を考えると、ゲームの勝敗は個数の組み合わせだけで決まっていることがわかる。つまり、現れる個数をs1,...,sks_1, ..., s_k個とした時に、その総xor S=s1s2...skS = s_1 \oplus s_2 \oplus ... \oplus s_kで勝敗が決まっている。先手が初手でsis_iを減らしてsiSs_i \oplus Sにすることができればxorがゼロになって、先手必勝の盤面になる。したがって、先手が勝てる初手の総数は

(s1s1S)+ ...+(skskS) \binom{s_1}{s_1 \oplus S} +  ... + \binom{s_k}{s_k \oplus S}

と計算できる。ただし、石を増やしたりパスしたりすることはできないのでsisiSs_i \le s_i \oplus Sの項は00とする。これを素朴に計算すれば部分点が取れる。

満点解法について。個数が同じもの、つまりsi=sjs_i = s_jとなるようなものはまとめて計算できることに注目する。つまり、個数sis_ic(si)c(s_i)個あると考えるとき、c(si)c(s_i)を係数として

c(si)(s1s1S)+...+c(sk)(skskS)(ijならsisjとする) c(s_i)\binom{s_1}{s_1 \oplus S} + ... + c(s_k)\binom{s_k}{s_k \oplus S} \qquad (i \neq j なら s_i \neq s_j とする)

と計算できる。登場する個数がll種類あるとして、llがもっとも大きくなるのは11回登場する数が1種類、22回登場する数が1種類、...、ll回登場する数が1種類あるときで、このとき

1+2+...+lN 1 + 2 + ... + l \le N

なので、l=O(N)l = {O}(\sqrt{N})と評価できる。これなら全体の計算量はO(QN){O}(Q\sqrt{N})となって間に合うはず。

さて、問題は区間[l,r][l, r]に登場する数をどのように数えるかだが、Mo's algorithmでできるのでそうした。

  • size[x]:=\operatorname{size}[x] := ある区間に登場する数xxの個数
  • coef[y]:=\operatorname{coef}[y] := 個数yyにかかる係数。

というデータを持ちながら伸縮すればO(1){O}(1)で遷移できるので、QQ個のクエリに対しては O(NQ){O}(N\sqrt{Q}) となる。つまり、計算量をまとめると O(QN+NQ){O}(Q\sqrt{N} + N\sqrt{Q})

ただ、size\operatorname{size}coef\operatorname{coef}をハッシュテーブルで持つと定数倍が重すぎて間に合わなかった。size\operatorname{size}に関しては、登場する数を座標圧縮すれば長さNN以下の配列で持てる。しかし、coef\operatorname{coef}O(N){O}(\sqrt{N}) で走査する必要があるので、素朴に配列を使うと方針が破綻する。これは双方向リストっぽい持ち方で解決した。つまり、 長さNNの配列で持った上で、

  • next[y]:=\operatorname{next}[y] := t>yt > ycoef[t]>0\operatorname{coef}[t] > 0となるような最小のtt
  • prev[y]:=\operatorname{prev}[y] := t<xt < xcoef[t]>0\operatorname{coef}[t] > 0となるような最大のtt

という風に前後のインデックスを保持しておく。coef\operatorname{coef}のある項を増やしたり減らしたりするときにはnext\operatorname{next}prev\operatorname{prev}を適切に変更する。こうすると、先頭からnext\operatorname{next}をたどればcoef\operatorname{coef}の正の要素だけを走査できるようになる。これで間に合った。


SBCLについて。これでも11秒くらい掛かって不思議に思っていたのだが、原因はコード変換に失敗していたことだった。sb-c:define-source-transformdefine-compiler-macroと違ってコンパイル時実行ではないので、eval-whenで括らないとコンパイル時には存在しないことになってしまう。直したら余裕で間に合った。AtCoderだとスクリプト実行だからこの問題が起きなくて気づけなかった。

CodeChef March Challenge 2020 - Egg-free DAG

egg-free DAGが作れるためには明らかにchordal graphでないといけない。逆にchordal graphなら、perfect elimination orderingにしたがってDAGを作れば勝手にegg-freeになる。

以下は必要なアルゴリズムについてのメモ。

与えられた頂点の順列がperfect elimination ordering (PEO)か判定する

https://www.cse.iitd.ac.in/~naveen/courses/CSL851/uwaterloo.pdf に疑似コードが載っている。ハッシュテーブルを使えば期待線形時間の実装は難しくない。できればハッシュテーブルなしで線形にしたかったけれど、やり方を思いつかなかった。

Maximum Cardinality Search

chordal graphに対してPEOを求めるアルゴリズム。正確には、グラフに対してなんらかの頂点の順列を出力し、入力がchordal graphであればそれが必ずPEOになっているアルゴリズム。結果を上の判定アルゴリズムで調べれば、chordal graphかどうかも同時にわかることになる。

An introduction to chordal graphs and clique treesの疑似コードより:

V := {0, 1, ..., N-1}
L = {}
result = []
for i from N-1 downto 0:
  find v in V s.t. |adj(v) ⋂ L|が最大
  result[i] = v
  L := L + {v}
  V := V - {v}

これを実装するのがけっこう難しかった。logN\log Nがついていいなら優先度変更可能なヒープを使うのがいちばん簡単?

V := 頂点0, 1, ..., N-1を優先度0として挿入したヒープ
L = {}
result = []
for i from N-1 downto 0:
   v := V.pop()
   result[i] = v
   for w in vの隣接頂点:
     wがLに含まれていればwの優先度を+1する

しかし、優先度の範囲が0N10 \sim N-1なので、頑張れば配列の操作だけで線形にできる……はずなのだが、簡潔な方法が思いつかなかった。けっきょく、双方向リストを二次元的に持ってdancing linksのようなことをして優先度の変更とポップをO(1){O}(1)にした。[1] 自分の実装はここにある。

Editorialはmaximum cardinality searchではなくlex. BFSに言及していたけれど、これはこれでけっこう大変そうに見える。


性質を調べてググって特徴付けを理解してアルゴリズムを実装して……みたいな段取りに3日かけてしまった。


  1. 正統な方法はTarjan, Yannakakis(1984)に書いてあるらしいのだが、買えなかった…… ↩︎

2020年3月6日金曜日

UTPC 2011 G - プログラミングコンテストチャレンジブック

2つの三角形をA,BA, Bとして、それらの3辺をそれぞれ長いほうからA1,A2,A3A_1, A_2, A_3, B1,B2,B3B_1, B_2, B_3とする。6辺の中で一番長い辺をA1A_1としてよい。

入力(ai)(a_i)があらかじめ長さの降順で並んでいるとする。6辺の選び方の順番を全列挙すると次のようになる:

  • A1,A2,A3,B1,B2,B3A_1, A_2, A_3, B_1, B_2, B_3
  • A1,A2,B1,A3,B2,B3A_1, A_2, B_1, A_3, B_2, B_3
  • A1,A2,B1,B2,A3,B3A_1, A_2, B_1, B_2, A_3, B_3
  • A1,A2,B1,B2,B3,A3A_1, A_2, B_1, B_2, B_3, A_3
  • A1,B1,A2,A3,B2,B3A_1, B_1, A_2, A_3, B_2, B_3
  • A1,B1,A2,B2,A3,B3A_1, B_1, A_2, B_2, A_3, B_3
  • A1,B1,A2,B2,B3,A3A_1, B_1, A_2, B_2, B_3, A_3
  • A1,B1,B2,A2,A3,B3A_1, B_1, B_2, A_2, A_3, B_3
  • A1,B1,B2,A2,B3,A3A_1, B_1, B_2, A_2, B_3, A_3
  • A1,B1,B2,B3,A2,A3A_1, B_1, B_2, B_3, A_2, A_3

これらを調べていく。三角形が成立するためには、A1<A2+A3,B1<B2+B3A_1 < A_2 + A_3, B_1 < B_2 + B_3が成り立っていることが必要十分なので、小さいほうの4辺A2,A3,B2,B3A_2, A_3, B_2, B_3は左に詰めても損にならない。したがって、下の列挙で-でつながった辺同士は列(ai)(a_i)上で隣り合っているとしてよい。

  • A1A2A3,B1B2B3A_1 - A_2- A_3, B_1- B_2 - B_3
  • A1A2,B1A3B2B3A_1 - A_2, B_1 - A_3 - B_2 - B_3
  • A1A2,B1B2A3B3A_1 - A_2, B_1 - B_2 - A_3 - B_3
  • A1A2,B1B2B3A3A_1 - A_2, B_1 - B_2 - B_3 - A_3
  • A1,B1A2A3B2B3A_1, B_1 - A_2 - A_3 - B_2 - B_3
  • A1,B1A2B2A3B3A_1, B_1 - A_2 - B_2 - A_3 - B_3
  • A1,B1A2B2B3A3A_1, B_1 - A_2 - B_2 - B_3 - A_3
  • A1,B1B2A2,A3B3A_1, B_1 - B_2 - A_2, - A_3 - B_3
  • A1,B1B2A2B3A3A_1, B_1 - B_2 - A_2 - B_3 - A_3
  • A1,B1B2B3A2A3A_1, B_1 - B_2 - B_3 - A_2 - A_3

これらを3つに分類してそれぞれ最大値を計算する。

  • A1A2A3,B1B2B3A_1 - A_2- A_3, B_1- B_2 - B_3

これはいちばん簡単で、三角不等式を満たす連続する3辺を左から探して、見つかったらさらに3辺を探せばよい。

  • A1,B1A2A3B2B3A_1, B_1 - A_2 - A_3 - B_2 - B_3
  • A1,B1A2B2A3B3A_1, B_1 - A_2 - B_2 - A_3 - B_3
  • A1,B1A2B2B3A3A_1, B_1 - A_2 - B_2 - B_3 - A_3
  • A1,B1B2A2,A3B3A_1, B_1 - B_2 - A_2, - A_3 - B_3
  • A1,B1B2A2B3A3A_1, B_1 - B_2 - A_2 - B_3 - A_3
  • A1,B1B2B3A2A3A_1, B_1 - B_2 - B_3 - A_2 - A_3

連続する5辺のすべてについて、BBが三角不等式を満たすような選び方でA2+A3A_2 + A_3が最大になるものを検討すればよい。その選び方について、ai<A2+A3a_i < A_2 + A_3となるような最大のaia_iを左方から探せばよくて、これは二分探索でできる。

  • A1A2,B1A3B2B3A_1 - A_2, B_1 - A_3 - B_2 - B_3
  • A1A2,B1B2A3B3A_1 - A_2, B_1 - B_2 - A_3 - B_3
  • A1A2,B1B2B3A3A_1 - A_2, B_1 - B_2 - B_3 - A_3

やはり連続する4辺のすべてについて、BBが三角不等式を満たすような選び方でA3A_3が最大になるものを検討すればよい……のだが、その選び方について、左方にあるai,ai+1a_i, a_{i+1}aiai+1<A3a_i - a_{i+1} < A_3を満たすものの中でai+ai+1a_i + a_{i+1}が最大になるものを探す必要がある。これは(遅延伝搬付きの)平衡二分木に、aiai+1a_i - a_{i+1}をキー、ai+ai+1a_i + a_{i+1}を値として挿入していって、キーがA3A_3より小さい区間の中での最大値を求めればよい。O(NlogN){O}(N \log N)


  • A1A2A3,B1B2B3A_1 - A_2- A_3, B_1- B_2 - B_3

以外のパターンはすべて左に詰めることができるので、連続した6辺だけを調べればよいらしい。まったく気づかなかった。

ARC 045 C - エックスオア多橋君

まずは列に関する同等の問題を解く。つまり、区間の総XORがXXになるような区間の総数を求める問題を考える。頂点を0-basedで与えて0,1,...,N10, 1, ..., N-1とし、区間[0,k)[0, k)の総XORをS[k]S[k]で表すことにする。区間[l,r)[l, r)の総XORはS[r]S[l]S[r] \oplus S[l]と書けて、これがXXに等しいという条件を整理すると

S[l]=S[r]X S[l] = S[r] \oplus X

となる。つまり、r=0r = 0から順に、S[r]XS[r] \oplus Xに等しいようなS[i],i[0,r)S[i], i \in [0, r)が何個あるか数えればよくて、これはハッシュテーブルなどにS[i]S[i]の出現回数を記録していけばO(N){O}(N)でできる。

木に関してもオイラーツアーを考えれば同じようにできるが、列の場合の方針をそのまま適用すると、同じ頂点を重複して数えることになってしまう。対処は簡単で、pre-orderに対応するものだけ数えるとか決めておけばよい。