070 C - Snuke Festival¶
- created: 2022-12-01
- ご意見・ご要望はissue・プルリク用のGitHubまで
- 競技プログラミングのためのF#入門
- GitHub上の対応ディレクトリ
- 公式ページ, ABC 077 C - Snuke Festival
- 公式解説, PDF
自分用の記録¶
公式解説では二分探索を提案しています. それぞれ次のような解答例があります.
F#版はstart-1やstop+1を外して書けないかと思って轟沈し, Haskell版も番兵なしで書けないかと思って轟沈し, 自力実装もTLEではまり倒したため, CやPythonなどのコードを見つついろいろ試し, 最終的にはRustのコードを参考にしました. ここでもこれに基づいた解説をつけます.
解説¶
参考にした命令型のRustのコードの焼き直しを説明したあと, 関数型に書き換えたバージョンを説明します.
まずは公式解説と同じく配列で入力を取ってソートします. あとの都合があるためBaもソートします.
1 2 3 | |
以下ソートした配列Xa, Ya, Zaで考えます. 何も考えずRustを直移植した結果は次の通りです.
1 2 3 4 5 6 7 8 9 10 11 | |
アルゴリズム上のポイントはBaのソートとi, kの引き継ぎです. AaとCaをソートしているため配列の引数からそのまま個数が計算できます. さらにBaのソートのおかげで, Baに関する添字をjとすれば, j+1に関する結果を計算するときjで計算したiとkから再開でき, 余計な計算が省略できます. Zaの関する計算は問題の素直な不等式ではなくZa.[k]<=bにした上でN-kを計算しています. (解説を書きながら気づいたのですが, Array.sortDescendingを使えば問題の条件のまま計算できます.)
F#コードのポイントはans <- ans + (int64 (i+1) * int64 (N-k))にもあります. ここでint64 (i+1) * (N-k)としてしまうとWAが出ます. 実際に出てはまり倒しました. 理由はintで(i+1) * (N-k)が計算されるとオーバーフローを起こす場合があり, このかけ算自体もint64で計算しないといけないからです. 配列の添字はintでint64にはできないため, 配列の添字を使った計算には注意が必要です.
命令型での焼き直しができました. 一般的には命令型の方が速くメモリ効率もよいため, これで終わらせても問題ありません. しかし関数型競技プログラミングを標榜している以上やはり関数型に書き換えます.
まずはfor b in Ya doから書き換えます. 最終的にはlet mutable ans相当の値を返したいため, foldを使って処理すればいいでしょう. 具体的には次のように書けます..
1 2 3 4 5 6 7 | |
F#では最後に計算した式の値を返してくれるため, これを関数の最後に書けば問題ありません. ポイントはifに対してelse節を書いて値を返す部分です. そもそもelse節がないとエラーを吐いてくれるため, そこで気付けるでしょう.
あとはmutableのiとkをうまく処理する必要があります. 積んだ結果を引き回す必要があるため, accをタプルに変えて積む必要があります.
ループは一般に再帰で書き換えられます. これもelse節で値を返す必要があり, 特に次のように書けます.
1 2 | |
関数名は一文字でaとcです. 短いプログラムだからこれで十分意図は伝わるでしょう. 気にいらなければもう少し長く説明的な名前でも構いません. この関数を使うとfoldは次のように書き換えられます.
1 2 3 4 5 | |
accが(acc,i,k)に書き換わりました. 計算結果もタプルで返します. 先程と違い, foldの最終的な返り値もタプルになってしまうため, 最後に必要な第一要素だけ取り出す関数を噛ませてあります. 最終的な全体実装は次の通りです.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
コードをもっと短くしたければ次のように読み込み時点でソートできます.
1 2 3 | |