Hard¶
068 D - String Equivalence¶
- created: 2022-11-30
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ, Panasonic 2020 D - String Equivalence
- 公式解説, PDF
解説1¶
- 下記方針に基づいた最終実装例
- 実装を見ると特に明確なように, 長さ
Nの標準形は長さN-1の文字列に一文字足して作られます. - 出題例をもとに特に小さい
Nで実験してもわかります. - 追加できる文字は
aを先頭に, 各文字列の中の最大の文字の一つ次までです.
ここまでの情報をもとにすれば素直に実装できます. まず補助関数を二つ作ります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
今回の問題の範囲では困らないためそのままにしているものの, succ 'z'の挙動が気になるなら適切に書き換えてください.
さて, 長さ2の標準形の一つabに対して長さ3の標準形を作りましょう. 関数mによってaからcまで追加できます. 文字の追加の仕方はいろいろあって文字列の連結で素直に後ろにつなげてもいいでしょう. ここでは文字列(.NETとしては文字の配列)を文字のリストにし, リストの連結もconsを使って前に追加します.
1 2 3 | |
長さ2の標準形はaaとabだから両方に作用させましょう.
1 2 3 | |
これで長さ3の標準形が得られました. しかしこれはchar list list listです. 実際に欲しいのはstring listで, char listをstring = char[]に変える必要もあります.
リストのネストを一つ減らすのはList.concatで, List.collect = List.map >> List.concatを使ってつなげて書けます. char list -> stringは.NETレベルのメソッドSystem.String.Concatを使います. リストはほしい文字列の逆になっているからList.revの処理も必要です.
これを再帰でまとめると次の解答が得られます.
1 2 3 4 5 6 7 8 9 10 | |
解説2: 末尾再帰¶
少し書き方を変えれば末尾再帰にできます. 結論だけ書くと次の通りです.
1 2 3 4 5 6 7 8 9 | |
069 C - Align¶
- created: 2022-11-30
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ, Tenka1 Programmer Contest C - Align
- 公式解説, PDF
解説¶
はじめに¶
公式解説の他にもいくつか解答を見ていると, 偶奇で場合分けをしているコードもあれば, 一本で計算しきっているコードもありました. ただ私にはあまり直観的でなくすっと理解しにくかったため, ここではcojnaさんのコード例を参考にした実装を紹介します.
簡単な方針¶
大きな数値から小さな数値を引いた方がよいのは明らかです. ソートして大きい数のリストhsと小さい数のリストlsにわけ, 大きい数と小さい数を順に並べ続ければいいでしょう. 特に分けたリストを再帰的に先頭から取れば十分です.
問題は全要素数が奇数の場合の処理で, hsとlsから一つずつ取り続けて余った項をどこに置くかが問題です.
具体例で実装¶
入力例1で上の方針を追いかけます.
1 | |
まずは「ソートして大きい数のリストhsと小さい数のリストlsにわけ」の部分を実装します.
1 2 3 4 5 6 | |
大きい方は大きい順に取るためList.revで順序を反転します.
1 2 3 4 5 | |
あとはxsとzsの先頭から再帰的に処理します. 再帰関数をfrecとして次のような呼び出しを前提にしましょう.
1 2 3 4 | |
はじめから初項を分離しなくてもいいとは思いますが, cojnaコードに合わせています. h-lの部分が結果を積むaccumlator変数です. hはhigh, lはlowで値の大小の分割を表しています. 最後の(ls,hs)はタプルにせず分けても構いません. 次の実装を前提にタプルにしています.
さて, 再帰関数の本体を考えましょう.
1 2 3 4 | |
functionの部分は次の省略表記です.
1 2 3 4 5 | |
これは次のように書いても構いません.
1 2 3 4 5 | |
以下前者のコードを前提に考えます. まずパターンマッチのmatchの第一行から考えましょう.
1 2 | |
まさにリストを先頭と残りに分割して処理しています. 再帰呼び出しのacc + h-low + high-lの項は, 一つ前の先頭の項に対して大きい数と小さい数の差を取るそのままの処理です. low, high, xs, ysの項も分割した項をそのまま積めば問題ありません.
残り二行を確認しましょう.
1 2 3 4 | |
[],[h]は項数が奇数の場合の余りの処理で, 最後の_,_が項数が偶数の場合の処理です. 後者は積み切った値を素直に返せばよく何も考える必要はありません. したがってあとは一つ余った項の処理だけです.
結論から言えばmax (h-low) (high-h)です. はじめにsplitAt (N/2)でわけました. この分け方で最後の項がlsとhsのどちらに入るか変わります. どうしても揺れが起こるためmaxでその揺れを吸収しています.
入力例1と新たに作った以下のもう一つの入力例をもとに確認しましょう.
入力例1での最後の余りの処理は次のようになります.
1 2 3 4 5 6 7 8 9 | |
したがってこちらはhigh-hを取るべきです. 具体的に全体としてどのような並び方を選んだのかを考えるのも大事です. 実際には次のようになっています.
h-low:8 1 6 2 3high-h:1 8 2 6 3
つまり初項を大きい方から取るか, 小さい方から取るかが最後の取り方で決まります.
さて, 新たな入力例はlet Aa = [|1L;4L;5L|]とします. この余りの処理は次のようになります.
1 2 3 4 5 6 7 8 9 | |
入力例1と違ってh-lowを取るべきです. 具体的に全体としてどのような並び方を選んだかと言えば次の通りです.
h-low:5 1 4high-h:1 5 4
もちろん他の可能性がないかも考えるべきではありますが, 前の項との差を取るアルゴリズムの組み方からしてありうるのはこの二通りしかありません. あとはこれを一般的にきちんと書き切れば適切なコードができます.
070 C - Snuke Festival¶
- created: 2022-12-01
- ご意見・ご要望はissue・プルリク用のGitHubまで
- 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 | |
071 C - Linear Approximation¶
- created: 2022-12-02
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ, ABC 077 C - Snuke Festival
- 公式解説, PDF
解説¶
公式解説通り入力Aaをシフトした配列をBaとします. 解説の便宜のためBaをソートした配列をCaとしましょう.
公式解説通りに実装すれば問題ありません. 他の人のコードを見ていると中央値の扱いにぶれがあるようです. 単にBa.[N/2]だけとしているコードもあれば, 偶奇でわけたコードもあります.
ちなみに私はさらに違う処理にしました. 選ぶべきbが整数でなければならないため, Nが奇数のときはCa.[N/2], Nが偶数のときはCa.[N/2]とCa.{N/2-1}を中央値の候補としています. もちろんNが偶数のときは二つの値を計算して小さい方を取ります.
どう書くとすっきりするかは人によるものの, 私は中央値の候補を配列にして処理しました. 具体的には次のように中央値の候補を作っています.
1 2 | |
最終的な悲しさの最小値は中央値の配列Maから次のように計算しています.
1 | |
よほど複雑な処理をかませる場合は適切な対処が必要ですが, Array.map f |> Array.sumはArray.sumByで書くとすっきりします.
私がはまり倒したため念のため書いておきます. 次のように書くとMaの値を取ってしまい正しい悲しさの最小値が得られません.
1 | |
おまけ¶
次の二つのコードの結果が面白いです.
- https://atcoder.jp/contests/abc102/submissions/2783376
- 言語: F# (Mono 4.0)
- 実行時間: 207 ms
- メモリ: 35328 KB
1 2 3 4 | |
- https://atcoder.jp/contests/abc102/submissions/2783237
- 言語: F# (Mono 4.0)
- 実行時間: 207 ms
- メモリ: 31932 KB
1 2 3 4 | |
違うのは最後の和を取る部分でsumByなのかfoldかです. 実行時間は変わらないものの消費メモリが一割違います. 少なくともMono 4.0でメモリ効率を考えるならfoldの方がよいようです.
そしてさらに私の提出コードも見てみましょう.
- https://atcoder.jp/contests/abc102/submissions/36920080
- 言語: F# (.NET Core 3.1.201)
- 実行時間: 154 ms
- メモリ: 49092 KB
1 2 3 4 5 6 7 8 | |
偶数項のとき和を二回取っているため明らかにこちらの方が遅いはずですが, Mono 4.0よりも高速です. Mono 4.0のリリースが2015年, .NET core 3.1.201のリリースが2021年で後者の方が新しいため, 処理系の高速化のおかげでしょう.
072 C - 4/N¶
- created: 2022-12-02 fri
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ, TENKA1 2017 C - 4/N
数学から決まる議論¶
特に小さいNに対して大学受験でもよくある問題です. 数学の話でもあって数学系学習と並行した議論を目指す私のスタンスからは大事なため, 公式解説の議論を簡単にくり返します.
対称性によってどれから議論してもよいためまずはhから考えましょう. 必要条件からの絞り込みでhを小さい方から増やして具体的に確認し続ければよく, まさにアルゴリズミックに次のように議論を進めます.
h=1として4/N - 1を計算する.- これは
1/n+1/kに等しいはずだから再びn=1として左辺に移行する.4/N-1-1と1/kが等しくなる整数kが取れるか確認する.
- 取れなければ
n=2で確認する.4/N-1/2-1と1/kが等しくなる整数kが取れるか確認する.
- 取れなければ
n=3で確認する.4/N-1/2-1と1/kが等しくなる整数kが取れるか確認する.
- ...
- 順次
n=hまで確認する.
- これは
h=2として4/N-1/2を計算する.- ...
と続けます. 原理的にこれしか議論しようはないものの, 時間内に解けるかが懸念点です. そして問題文でh,n,w≤3500が保証されているため, この範囲内でけりが着くはずと思って全探索します. もちろん3つ全てでループするとTLEするため, 2つ決まれば3つ目は自動的に決まる性質を使って二重ループで片付けます.
解答1: 素直なループ¶
1つ求めればよいため, 1つ決まったら計算を停止させます. 命令型的なfor文とbreakを使えればよいのですが, 残念ながらF#にはbreakがないようで, 使うならwhileとフラグ管理です.
ただし命令型として素直なforとbreakがないだけです. Haskellの遅延リスト・遅延評価と同じく, 遅延評価してくれるseqを使いましょう. 次のような適切な書き方をすれば明示的なbreakが不要です.
1 2 3 4 5 | |
for h in 1L..N doがカウントアップのforで, 二重ループだから二つ出てきます. 条件を満たした値を取る部分がif b then yield valueです. この場合はyieldキーワードはなくてもよいようですが手癖で書いています. 詳しくは公式のシーケンスを見てください. seqとコンピュテーション式による見慣れない構文には慣れるしかありません.
seq {...}でいわば遅延リストを作り, その先頭の値をSeq.headで取れば必要な計算だけで処理が終わります. Schemeのように有理数を処理してくれる言語もありますが, F#標準で有理数はないため整数の範囲でおさまるように標準的な対処を取ります. 適当なところで打ち切った数による積で構成する三つ目の整数がintに入る保証がないため, int64で計算する点に注意すれば, あとは特に問題ないでしょう. 結論としては次のようなコードで通ります.
1 2 3 4 5 6 7 8 9 10 | |
出力処理は何となく既存の出力処理を流用するために配列にしました. タプルのまま次のようにも書けます.
1 | |
解説2: 再帰¶
「競技プログラミングのためのF#入門」で無限ループを含めてループは再帰で書けると書きました. 実際ここでも再帰で書けます. 提出結果を見ると再帰の方がメモリを食っています. 2022-12時点では効率を求めるよりも通るコードを書く, さらには基本的なアルゴリズムの教育用コンテンツを作る方に主眼があるため, 別解として再帰によるコードも紹介します.
forのコードを素直に書き換えればよく, 結論としては次のように書けます.
1 2 3 4 5 6 7 8 9 10 | |
ポイントはもちろんifの条件分岐です. 最初に条件をみたして値を返すか(停止するか)判定し, 停止しなければhかkを適切にカウントアップします. ここでは上記のseq+ループアルゴリズムに合わせました.
コードが読みにくくなるためここでは勧めませんが, 例えば次のようにまとめて書けます.
1 2 3 4 5 | |
さらに次のようにも書けます.
1 2 | |
let ... inはOCamlのコードでよく出てきます.
073 C - Strange Bank¶
- created: 2022-12-03 sat
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ, ABC099
解答1: 公式解説, シンプルなループ処理¶
公式解説の処理をF#で単純に書き直すならこのコードのようになるでしょう.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
最初のwhile i <=n doはforで簡単に書き換えられるとして, 可変変数を不変変数に置き換えつつ, 内部のwhile t > 0 doをどう関数型らしく書き換えるかがまず問題です. F#解説で書いたように再帰で書き換えます.
tは各whileの直前で設定し直していて最終的にccを返せばよいため, tは単純に関数の入力パラメータにすればよいでしょう. 6で割るwhileループを表す再帰関数は次のように書けます.
1 | |
正の数を正の数で割り続けたあまりは0になるため, then節が短くなるように条件式を書き換えています. 9で割り続ける処理はmod kのkが変わるだけで同じ処理です. それもまとめた再帰関数は次のように書けます.
1 | |
次にメインのwhile i<=n doを考えます. これは最終的に積んだ値resを返すループだからfoldを使えばよいでしょう. 結論としては次のように書けます.
1 2 3 4 5 6 | |
最後のif式ではelseをつけ忘れないようにしましょう. もちろんつけないと型エラーで動きません.
最後に, そもそも意味がわからずはまり倒したため, let cc6 = frec 6 0 i; let cc9 = frec 9 cc6 (N-i)で何をしているかを説明する自分用のメモ. まず次のように書き換えて考えるといいでしょう.
1 2 | |
こうすると次のように意味がはっきりします.
cc6:iを6のベキだけで処理した回数cc9: 残りのN-iを9のベキだけで処理した回数
あとは全探索の部分です. ある数iを6のベキで処理し切ったら残りのN-iは9のベキで処理するしかありません. これを全探索で全てのiに対して確認しています.
解説2: ユーザ解説, メモ化再帰¶
ユーザ解説はDPで解いています. 私の前にF#でDPで解いている人がいます. 実行時間も短いためこれも考えてみましょう. 上記コードはちょっと長いため単純化したコードを紹介します. 2022-12時点で全てではないものの, Educational DP Contest / DP まとめコンテストにF#で取り組んだ結果があるため, こちらも参考にしてください. (2022-12時点では特にHaskell・OCamlコードを焼き直しただけで, 私は自力でDPを解く力量がありません.)
まずF#版のarray6とarray9にあたる配列生成を考えます. もちろんいくつか書き方はあり, 比較的簡潔なのは次のコードでしょうか.
1 2 3 | |
再帰関数のpはベキを何回まで取ればいいか計算する関数です.
次は本丸のメモ化再帰です. 定型的な書き方があるため, コピペで使えるようにReference.fsxにメモしています.
まずはコードを示してそれにコメントしましょう.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
メモ化の部分がlet memorec fです. let memo = System.Collections.Generic.Dictionary<_,_>()はF#のMapではなく.NETの辞書を読んでいます. memo.Addで破壊的に書き換わってくれた方が便利だからです. let rec frec jはメモがあればその値を返し, ない場合は値を詰めて返します.
処理の本体がcountです. 第一引数のfrecはmemorec中でfrecとして呼び出す関数で, 対応を明確にするために名前を揃えています.
まずc1から考えましょう. 6のベキで大きい方から削るため, (array6 |> Array.findBack (fun x -> x<=n))で削れる中で最大の数を取ります. c1は削って残った数で, frec c1でメモ化再帰または動的計画法でc1に辿り着くまでの最小操作回数が取れます. 実際にcount関数のelse節でprintfn "%A" (c1,c2,frec c1, frec c2)で出力して確認してみてください.
同じ計算を9に対しても適用して, 小さい方を取れば最小回数が得られます. 最後に1を足すのはmin (frec c1) (frec c2)は最終ステップ一手前の値だからです.
あとはcountにmemorecでガワをかぶせて計算すれば求める結果が得られます. メモ化のための辞書memoをクロージャーmemorecで隠蔽している分コードが読みにくいかもしれません. オリジナルは最終的に呼び出す関数が素直な再帰になっているため, こちらの方がわかりやすいかもしれません.
074 D - Harlequin¶
- created: 2022-12-03 sat
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ, CADDI 2018
- 公式解説, PDF
解説¶
特記事項はなく, 解説通り次のように書けばいいでしょう.
1 2 3 4 5 | |
関数の合成としてsolveの引数を省略して書いています. 関数の合成は>>です. 数学での関数合成は$f \circ g$で$g$を作用させてから$f$を作用させます. F#でこれを書きたいならf << gです. パイプラインの発想と同じく左から右に流して書くにはg >> fです.
上記のsolveは引数をつけると次のように書けます.
1 | |
「全て偶数」の代わりに「奇数が存在する」と書きたければ次のようにも書けます.
1 | |
075 D - Face Produces Unhappiness¶
- created: 2022-12-12 mon
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
- 公式解説
解説¶
考え方としてはユーザー解説の方がわかりやすいでしょう. 引用します.
で、これらを考えていくと、回転させるのは、ある同一方向を向いている塊を回転させれば良さそうということになる。 やっと400点レベルにまで落ちてきた。 方向が一致している区間を縮約するとLRLRLRやRLRLRLになっているはず。 例えばLRLRLRの最初のRを回転させるとLLLRLRでLRLRとなり、方向が一致していない箇所が2つ減る。 なので、LかRのどちらかを貪欲に回転させて、方向不一致の区間を減らしていったときの幸福人数が答え。 幸福人数はN-(LかRのグループ数)となる。各グループで1つは幸福でないため。
あとはこれをどうコードに落とし込むかにかかっています.
最大値を計算する部分に集中します.
- 元からの幸福度を計算する. これは前後の向きが一致しているか, 各位置の前後のペアの文字を調べて確認すればよい.
- 部分列をうまく回転させると回転させた両端の
2だけ幸福度が上がる. 特に最大2*Kだけ幸福度が上がる.
この和を取れば幸福度最大の状況の幸福度が計算できるはずです. ただしLLLLLRRRRRの例のような真の最大幸福度が実現される場合, 問題の制約によって端の人の幸福度の処理が必要で, この処理を忘れてはいけません.
実装1¶
F#の文字列は文字列のモジュールもある一方で文字のシーケンス(Seq)です. 文字列のモジュールにほしい処理がなくても, シーケンスの関数を拝借して処理できます. 実際Seq.pairwiseで前後ペアのシーケンスが取れるため, これで元の幸福度計算用の処理を進めればいいでしょう. 例えば次のような結果が得られます.
1 2 3 4 | |
前後ペアを取った結果のシーケンスから幸福度を計算するには, シーケンスのたたみ込みで和を取ればよく, 典型的にはSeq.foldで計算できます. ここではSeq.sumByを紹介します.
1 | |
これが処理の本体で, あとは次の通りです.
1 2 3 4 5 6 7 | |
実装2¶
HaskellにはあってもF#にはない便利関数は数限りなくある一方, 珍しくHaskellだと前後ペアを作る便利関数がないようです. こういう場合はzipやzipWithを使うのが典型的な処理です. そしてHaskellコードの移植時に慣れていないとはまる部分でもあります. ちなみにHaskellのzipWithはF#だとSeq.map2です.
さてどうやって前後ペアを作るかというと, zipを使って二つのシーケンスをタプルのシーケンスにまとめます.
1 2 3 4 | |
上の例のようにシーケンスのzipは二つのリストの長さが一致しなくても短い方に自動的に合わせてくれます. しかしリストや配列では長さが違うとエラーになるため注意してください.
これを使うと前後ペアは次のように作れます.
1 2 3 4 5 | |
あとは実装1と同じように処理すれば求める結果が得られます.
076 C - たくさんの数式¶
- created: 2022-12-12 mon
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
- 公式解説
解説¶
文字列がもっと長いと気になってはくるものの, (末尾再帰ではない)再帰でさっと書いてしまえばいいでしょう. HaskellではfoldMなどで豪快な処理が書けるものの, (今の私のF#の腕前では)シンプルな移植ができないのも理由の一つです.
計算用の再帰関数は次のように実装できます.
1 2 3 | |
入力の文字列を一桁数値のリストに変換する処理は次のように書けます.
1 | |
ここで1を単にint64 cだけにすると49Lが返ってきてはまり倒します. 上のようにint64 '0'を引いて整数として1Lが返るようにするか, c |> string |> int64のように文字列にしてからint64を通しましょう. 例を見るとわかるようにInt32の範囲を飛び越えるため, オーバーフロー対策でInt64を使うのは必須です.
その他¶
逐次計算していくのではなく, いったん文字列から必要な数を切り出す(ベキ集合を作る)タイプの実装でHaskellコードを見ると, replicateMを使ってブーリアンのベキ集合を使ってどの文字を取ってくるか判定しています. Reference.fsxをreplicateMかベキ集合で検索すれば対応する関数のリスト版実装があります. これを参考にHaskellコードの移植を考えてもいいでしょう.
077 A - 01 Matrix¶
- created: 2022-12-12 mon
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
- 公式解説
解説¶
アルゴリズムを考えるのが大変なだけで, 公式解説通りに素直に実装すればよいでしょう.
F#の文字列連結は単純な+でよく, 連続した文字からなる文字列はString.init (W-A) (fun _ -> "1")で作れます. List.iterやfor文で順次stdout.WriteLineしても構いません. あえて文字列のリスト(や配列)を作りたければ, 例えば次のような形でB行とH-B行分の文字列を生成すればいいでしょう.
1 2 3 | |
F#でのリストの連結はList.appendまたは@演算子です.
TODO¶
- 配列やシーケンスにしたときどれだけ速度が変わるか?
078 C - K-th Substring¶
- created: 2022-12-13 tue
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
- 公式解説
解説¶
公式解説の方針そのままの実装を考えます. 条件をみたす部分文字列をどう作るかがポイントで, 今回の条件ではとにかく手当たり次第に作ると諦めるのが肝心です.
文字列の各i番目からi+K-1番目までの部分文字列を作り倒すには, 例えば次のように書けばよいでしょう.
1 2 3 4 5 | |
この返り値はもちろん文字列の配列の配列で, 型はstring[][]です. これをフラットにする, つまりstring[]にするにはArray.concatを作用させればよいです. しかし標準でArray.mapの結果をArray.concatしてくれる関数Array.collectがあるため, 素直にこれを使えばいいでしょう.
1 2 3 4 5 | |
上の出力を見るとわかるように"a"が二つ出てきます. あとはこの重複をArray.distinctで潰し, Array.sortしてK番目を取れば終わりです.
079 D - Handstand¶
- created: 2022-12-13 tue
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
- 公式解説
解説¶
これも基本方針は公式解説です. 自力実装ではまり倒したため, 今回はPythonコードを参考にしました.
まずは前後で文字が切り替わる番地を取得しましょう. シンプルなのは次のようなfold処理です.
1 2 3 | |
公式解説の後半ではいくつかややこしい条件分岐処理があります. これをシンプルに処理するために番地にNを追加します.
1 2 3 4 5 | |
これで公式解説で言うi_kを要素とする配列が作れました. あとはX_kの配列を作れば終わります.
配列を作ってからArray.maxを作っても構いません. ただ今回は添字に関する処理をしながら逐次最大値を計算していくだけで求める値が得られるため, はじめからfoldを使って処理します. 配列IaはNを追加していていわば余計な項を含んでいます. このため添字に関してやや面倒な処理が必要です. 具体的には次のように書きます.
1 2 3 4 5 | |
foldはIa自身ではなくIaの添字の配列で回します. Nの追加があるため[|0..l0-1|]とループはIaの最後まで回らないようにします. ポイントはjの構成です. 解説のS_{i_k} = 0 or 1での添字の変化はint S.[Ia.[i]] - int '0'で対応します. さらに「k>rならi_k=N+1」の処理がまさにmin hoge l0の部分です. これで添字を作ればIa.[j]で必要な値が取れます.
TODO¶
- ユーザー解説の尺取り法実装
080 C - Remainder Reminder¶
- created: 2022-12-14 wed
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
解説¶
公式解説通りに素直に実装します. 強いて言うならmapしてからsumではなく, 一気にsumByしてしまうと少し速くなります. 例えば次のように書くといいでしょう.
1 2 | |
081 C - Boxes and Candies¶
- created: 2022-12-14 wed
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
解説¶
公式解説通りに素直に実装します. 競プロと言えどプログラミングである以上, 簡潔さと明確さを兼ね備えてほしいため, 条件分岐をどうすっきりまとめるかが焦点です. 特に今回はややこしい条件分岐はmaxでまとめられます. 私自身, 執筆時点でまだまだ不慣れな部分です.
さっと正解を書けたとしても, 他の人, 特にショートコードを書く人達のコードをいくつか眺めると勉強になります.
結論としては次のように書けばよいでしょう.
1 2 3 4 | |
総和がほしいタイプのループ処理だからfoldですっきり書けます.
082 C - Different Strokes¶
- created: 2022-12-14 wed
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
解説¶
公式解説通りに素直に実装します.
今回入出力は次のように処理する前提で解説します.
1 2 3 | |
降順ソートはArray.sortDescendingとArray.sortByDescendingがあり, 今回は後者を使えばよいでしょう.
次は和を取る部分です. 配列はF#流の0はじまりとします. 公式解説では別途b_iの和を取っておくような形になっていました. しかし配列の添字が偶数ならa_iを, 奇数なら-b_iを足すようにすればb_iの和を別途用意する必要はありません.
問題は配列の添字をどう用意するかです. Array.indexedで元の配列Xaを添字づけてから処理する方法もあれば, 添字の配列でArray.foldやArray.sumByを回す方法もあります. ここでは次のようにArray.foldで添字を積む方法を取ります.
1 2 3 4 5 | |
Array.foldで持ち回る変数として和のaccだけではなく添字のiも積みます. 添字にあたるiを削るため最後にfstで和だけを取っています.
Array.foldに食わせるラムダでlet c = hoge inを使っています. let inをうまく使うと一行でも見やすく書けて便利です. もちろん二行に分けて書いても構いません.
083 A - Darker and Darker¶
- created: 2022-12-15 thu
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
参考¶
解説¶
破壊的な実装¶
公式解説通りに実装します. 今回のポイントは破壊的・非関数型的な実装です. F#には独自のキュー(関数型のキュー)がなく, .NETの破壊的なキューしかありません. 関数型のキューを自前実装するのも面倒です. 余程の強烈なこだわりでもない限り素直に.NET実装を使えばいいでしょう.
念のため書いておくと, いわゆる関数型言語でも効率が必要な場合は破壊的な実装を使います. 関数の内部など影響範囲が確実に限定されていれば問題はなく, それを突き詰めて関数とアクションの分離にまで持ち上げたのがHaskellです. AtCoderでHaskellの実装を見るとシンプルな実装はしづらいようです.
さらについでに書いておくと, 少なくともAtCoder上のOCaml勢は破壊的な実装を厭いません. 困ったらOCaml勢の実装も参考にしましょう. 今回も解説用のコードはOCamlを参考にしました.
入出力¶
入出力とその変数は次のようにします.
1 2 3 4 5 | |
変数の初期化¶
まずはキューを初期化しつつ, フラグ管理・操作回数管理用の配列を作ります. F#では配列の配列ではなく二次元の配列があり, 今回は二次元の配列が便利そうなのでこちらを使います.
1 2 3 | |
Daが本体のループでゴリゴリ書き換える変数です. そもそもとしてF#の配列は破壊的なデータ型で, mutableをつけなくてもDa.[j,i] <- 0で破壊的に変更できます. Daは.を-1, #を0で初期化しています. 最終的に何回目の処置で黒に変わったかを表す二次元配列で, -1はまだ書き換えできていない状態を表します.
さらに#の場所もキューに積みます. ここでAaのi,jはそれぞれy座標とx座標で混乱するため, 上記のDaとqは添字を入れ変えてx,yと書けるようにしています.
本体のループ処理¶
let mutable ans = 0で最終的に返す変数を宣言します. あとは適宜qに積みつつqが尽きるまでループします. qに値があるかどうかはプロパティq.Countで判定できます.
1 2 3 4 | |
いろいろな処理の部分を考えます. 何はともあれqから値をポップします.
1 | |
.NETではq.Dequeue()です. ポップした値を中心にして周囲4マスの値を確認します. 周囲4マス分の移動を表す配列Maを用意します.
1 | |
さらに配列の範囲外参照と訪問状態をチェックする関数を準備します.
1 | |
ここで白(.)かどうかの判定は初期値ではなく書き換えたかどうかを判定したいため, 入力のAaではなくDaの値で判定します. 一緒に配列の範囲内か確認するべく値Da.[x,y]ではなく配列自体を渡しています.
この二つを使って次のように周辺4マスの状態を確認しつつ値を更新します.
1 2 3 4 | |
Maの各値をArray.iterで参照します. 周囲4マスのどれかを表すx,yを準備してx,yの値が白(.)かどうかを判定します. 点x,yがまだ白ならDa.[x0,y0]+1でDa.[x,y]を更新します. 最後に新たに黒(#)にしたフラグ立てとしてq.Enqueue(x,y)でキューにx,yを積みます.
途中ansをバリバリ書き換えているためこれで最終的な解答になるか微妙な不安がありますが, 都度キューに積んだ値を見てループしているため, これで常に最新の変更回数が取れています.
084 C - Shopping Street¶
- created: 2022-12-16 fri
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
- ユーザー解説
三つの実装¶
まずはユーザー解説にあったビットマスクを使った実装を紹介します. しかし執筆時点でビットマスクに慣れていないためにいま一つ腑に落ちず, Haskell実装を参考にビットマスクではな真偽値の形で全パターンを作る実装を確認しました. この実装は簡潔な割に見通しもよいコードで何をどうすればいいかようやく見えました. そこでさらにHaskell実装を参考にパターン網羅部分をビットマスクに書き換える処理を実装しました.
ビットマスクに慣れていなければ解説2の実装を読むといいでしょう. 最後に解説3でビットマスク化したときの実装を載せています.
解説1: ビットマスクを使う方法¶
解説する実装はどちらかと言えば関数型らしい実装にしています. しかしfor文による処理の方がわかりやすいかもしれません. 本質的に全く同じ処理をしているため, 読みやすい方で実装を確認してください.
入力¶
F_{ijk}のj,kは指示の上で曜日と午前午後でわかれています. しかしこれは時間指定用パラメーターが10個あると思って処理した方が便利です. 特にループ用に[|0..9|]の意図を明確にする変数としてjkNum = 10-1を用意します.
さらに次のように入力を取得して変数に束縛します.
1 2 3 4 5 6 | |
全体構成¶
全パターンを総なめして最大値を取ります. 以下のコードでは0はじまりで配列を作っているため, 総パターン数にあたるtotalは2^10-1とします. 最大値を取るため, たたみ込むループとみなせ, 大枠は次のfoldです.
1 2 3 4 | |
ループはビットマスクで回します.
各パターンのチェック¶
foldの内部を考えます. 各開店パターンごとに店が両方営業しているか調べ, 両方開いていれば利益を計算します. 特に開店している時間帯の個数をcとして積みます. c=0は一つも開店しない除外パターンだから最大値は更新しません. それ以外の場合は利益を計算して最大値候補を比較して更新します.
店iとお姉ちゃんが両方開店している時間帯の個数に対する配列をCaとすれば, fold内部は次のように書けます.
1 2 3 4 5 6 7 | |
最後のelse let p = ...は問題文での利益計算そのままです. あとは店ごとに両方開店している時間帯の個数を数えれば終わりです.
開店時間帯の個数計算¶
F_{ijk}に関する処理です. 店舗数はNで開店時間帯の総和を求めたいため, Caは0埋めしたArray.zeroCreate Nで初期化し, 時間指定の10個のループを回します.
1 2 3 4 5 6 | |
いま見たいのはfun acc n -> hogeの部分であるため, 以下そこだけ抜き出しましょう.
1 2 3 | |
foldで積み回す変数は(c, Ca)として, ループの変数はjkにします.
1 2 3 | |
次にビットマスクに関する変数nを処理します. 基本はn >>> jkで, 開店状況を取るためにさらに&&& 1をかませます. 該当時間帯で開店していればlet bit = n >>> jk &&& 1が立つため, bit = 1なら開店状況を更新します. もちろん開店している店の個数に関するcも同時に更新する必要があります. まとめると次のように書けます.
1 2 3 4 | |
最後のタプルを作る部分が一行にまとめると読みづらいなら適当に書き換えるといいでしょう.
1 2 3 4 5 6 7 8 | |
補足: ビット演算¶
私自身, この記事の執筆時点で全くビット演算に慣れていません. どういう計算になっているか確認したければ次のコードを実行して出力を見るといいでしょう.
1 2 3 4 | |
解説2: ビットマスクの代わりに全開店状態の真偽値を生成して確認する¶
ビットマスクに慣れていないため, 比較用に真偽値で開店状態を全て生成する実装も試してみました. Haskellを参考にしています.
1024通りの真偽値を生成¶
F#でのリスト版replicateMを実装して, Oaとして開店状態の真偽値を表す配列を生成しました.
1 2 3 4 | |
Faを真偽値に変換¶
さらにHaskell実装と同じくFaを0,1から真偽値に変換します.
1 | |
処理の大枠のfoldの構成¶
解説1と同じくfoldでたたみ込めばよいため, 大枠は次のように書けます.
1 2 | |
foldの内部¶
opnは[|false; false; false; true; true; false; false; false; true; false|];のような1024通りの開店状況のうちの一つです. まず一つはお店が開いていなければならないため, 少なくとも一つはtrueが必要です. 特に次のような分岐処理が入ります.
1 2 3 | |
利益計算¶
あとは利益計算処理を書けば終わりです. この準備のもとでF_{ijk}から開店状況(開店店舗数)を取得し, 利益を計算する関数calを作りましょう.
上記のGaと入力のPaの各行ごとにopnと比較して計算すればよく, 特にこれらの各行がきちんと対応しているため, Array.map2で同時に各行を取得しつつ処理した結果の総和を取れば問題文のP_{1,c_1}+P_{2,c_2}+...+P_{N,c_N}が計算できます. 特に利益計算は(Ga, Pa) ||> Array.map2 (cal opn) |> Array.sumとして, cal関数を作れば十分です.
cal関数の構成¶
最後にcal関数の構成を考えます. まずGa(Fa)の各行gとopnで渡した開店状況を比較して開店個数を取ります. 真偽値に変換しているためArray.map2で次のように書けます.
1 | |
これでフラグが立っている店舗数を計算すればよく, Array.sumByを使えば次のように計算できます.
1 | |
これで開店店舗数nがわかったため, あとはPaの各行pに対して開店店舗数にあたるp.[n]を取れば終わりです. 特にArray.getを使えばパイプラインで次のように流せます.
1 | |
最後に処理をまとめると次のように書けます.
1 2 3 4 5 6 7 8 9 10 11 | |
参考¶
replicateMやfilterMなどいくつかのアクションに対して, F#のリスト版の実装をReference.fsxに収録しています.
解説3: 解説2の実装のビットマスク化¶
まず実装は次の通りです.
1 2 3 4 5 6 7 | |
calが少し書き換わります. map2 >> sumByではなくfold2で一気に和を計算しています. さらに真偽値ではなく0,1のままで処理を進めているため, map2 (&)の部分でo=b && o=1のような書き方が必要です.
あとのポイントはビットマスクからのopnの手動生成です. 私はこれでようやくビットマスクで何をやっているか理解できました. 私の感覚では解説1の実装より簡潔になった上に意味も把握しやすくなりました. ようやくすっきり理解できてとてもいい気分です.
085 C - Pyramid¶
- created: 2022-12-17 sat
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
解説¶
公式解説通りに素直に実装します.
入出力¶
1 2 3 | |
補助関数¶
l1距離を測る関数l1を定義します.
1 | |
はじめ l1 x1 y1 x2 y2で定義したものの, 間違って右辺の出てくる順番でl1 x1 x2 y1 y2と書いてバグったため, (私にとって)間違えにくいタプルで書き直しました.
参照点の選出¶
Array.findで探せます.
1 | |
F#のnot equalはa <> bです. ちなみにHaskellではa /= bです.
問題の条件によって必ず条件をみたす点が存在するためArray.tryFindなどを使う必要はありません.
全探索¶
まず中心のデータを一気に生成します.
1 2 | |
あとは再びArray.findで条件をみたす要素を探します. これも必ず, それも一意的に存在するとわかっているためArray.tryFindなどで保険をかける必要はありません.
全ての入力が条件をみたすか確認する必要があるため, Array.findの中でAaに対するチェックのループが走ります.
1 2 3 4 | |
最後に返り値の数値のタプルからsprintfで文字列を生成しましょう.
1 2 3 4 5 6 7 8 | |
086 D - Coloring Dominoes¶
- created: 2022-12-17 sat
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
入出力¶
共通の入出力です.
1 2 3 4 | |
解説1¶
再帰で素朴に実装します.
補助関数¶
まずは補助関数を準備します.
1 2 3 | |
MODはまさに余りを取るための数です. .NETでは数値を_で区切って読みやすく書けます. Int32でも問題ないとは思うものの, 念のためLをつけてInt64にします.
let (.*)と括弧をつけると演算子が定義できます. 計算結果を%で処理し忘れないように全てこの演算子で計算します.
あと公式解説でいうX(縦並び)判定用の関数としてisVerticalを用意しました.
再帰関数¶
まず先頭から確認をはじめます. 縦が一致していたら3をかけて1進め, そうでなければ横の並びと判定して6をかけて2進めます. 最後まで来たら積んできた値を返します. したがって次の処理ではじめればいいでしょう.
1 2 3 4 5 6 7 | |
あとは地道に各iに対して前後を比べて処理します. 公式解説の判定をミスなく実装するだけです.
1 2 3 4 5 6 7 8 9 10 | |
読みづらくも書きにくくもなく, 程々の長さの実装で問題はないでしょう.
解説2¶
公式解説を参考にHaskell実装を参考に実装します.
方針¶
大事なのは縦並びか横並びかで, 文字が続くかどうかを判定すればよく, いちいち二つの文字列を比べなくても一つの文字列だけ見れば判定できます. 一つの文字列を見て縦並び・横並びを判定したリストを作っておいて, 前後のペアを順に確認すれば目的の処理が完遂できます.
本質的には変わりませんが, こちらは再帰ではなくfoldで実装します.
補助関数¶
解説1と同じ補助関数を準備します.
1 2 | |
文字列の処理¶
F#のList.groupと違い, HaskellのData.List.groupは文字列を先頭から見て同じ文字が続く限りグループ化する関数です. 具体的には次のような挙動を取ります.
1 2 3 4 5 6 7 8 9 10 | |
ここで定義した再帰関数のgroupがHaskellのData.List.groupのF#実装です. これを使って文字のリストに分割し, 内部の各リストの長さを取れば1のときは縦並び, 2のときは横並びです.
1 | |
サンプルの実行結果は次の通りです.
1 2 3 4 5 6 | |
大枠¶
先程定義したpatternsを処理します. 初項によって初期値は3か6か変わります. 縦か横かは既に判定済みなため, 前後のペアをList.pairwiseで素直に作って順次確認すれば十分です. これをまとめると次のようにfoldが書けます.
1 2 3 | |
関数fは解説1と本質的に同じで次のように書けます.
1 2 3 4 5 | |
全体をまとめましょう.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
087 D - Friend Suggestions¶
- created: 2022-12-18 sun
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
-
- 要点: アルゴリズムの検討・実装
入出力¶
1 2 3 4 | |
解説¶
練習も兼ねて(破壊的な)Union-Findを簡易実装して対応します. 提出された解答を眺めていたら, 少なくともRustではpetgraph crateがあってAtCoderでも使えるようです.
破壊的な簡易Union-Find¶
クラスとしての実装はUnionFind.fsxに置いてあります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
入力の変換¶
直接的な友達・ブロックの隣接行列を作りながらUnion-Find木を作ります. 隣接行列は.NETのResizeArray()のAddでゴリゴリと破壊的に作ります.
1 2 3 4 5 6 7 8 | |
Faが友達(friends), Baがブロックの配列です. 入力のXaの処理の中でunite a bをかませてUnion-Find木を作っています.
最終的な計算¶
公式解説通り次の量を計算します.
1 2 3 | |
各頂点iに対して連結成分のサイズはsize i, 友達関係にある頂点jの数はFa.[i].Countで計算できます. ブロック関係にある頂点jの数はUnion-Findのfindを使って次の処理で計算できます.
1 | |
あとは各頂点ごとの計算を次のようにArray.mapで処理します.
1 2 3 4 | |
まとめ¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | |
088 D - XOR World¶
- created: 2022-12-18 sun
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
- 要点: アルゴリズムの検討・実装
入出力¶
1 2 | |
解説¶
素直な計算ではTLEする¶
最大で10^12個の項の計算が入るため, 次のようなごく素直な計算はTLEです.
1 | |
ちなみに手元で計算したベンチマークは次の通りです.
1 2 3 4 5 6 7 8 9 | |
結果は次の通りです.
1 2 3 4 5 6 7 8 9 10 11 | |
10^10の時点で既に20秒もかかっています.
実装¶
公式解説通りの実装は結果から言えば次の通りに書けます.
1 2 3 4 5 6 | |
ちなみに(x+4L)%4LはA = 0への対策です. F#では(-1)%4 = -1で配列外参照が起きます. ここでは場合分けではなくmodの部分に手を入れました.
問題はgの実装です. 条件文をいろいろ書いて頑張ってもいいのですが, 以下の実験・具体例での確認を前提に上記のようにすっきり書いた方がよいでしょう.
一気通貫に確認¶
結論から言うと(私には)見にくくこれでは何とも言えないように思います.
ただ数が少ない場合は単純実装で簡単に確認できるため, まずは単純実装で様子を見ます.
1 2 3 | |
結果は次の通りです.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
周期性の存在を前提に見れば何とは0と1がポツポツ現われる程度の事情は見えます. プログラムで手を抜いてはいけないようなので具体的に確認します.
具体的に確認¶
公式解説を前提に次のように具体的に項を2つずつまとめて書いてみます.
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
周期4を前提に調べましょう.
- 0: はじめは問答無用で
0- もしくは
0に対して新たに0をXORするから0
- もしくは
- 1:
0に1をXORで足すから1 - 2:
1にXORで2を足すから3 - 3:
1のペアが2つできてXORは0- もしくは
3に3をXORするから0
- もしくは
次の周期です.
- 4:
0に対して新たに4をXORするから4 - 5:
1のペアが3つできてXORは14で初期化されたと思うと1のペアが1つで1
- 6:
1に対して新たに6をXORするから7 - 7:
1のペアが4つできてXORは0- もしくは
7に7をXORするから0
- もしくは
次の周期です.
- 8:
0に対して新たに8をXORするから8 - 9:
1のペアが4つできてXORは18で初期化されたと思うと1のペアが1つで1
- 10:
1に対して新たに10をXORするから11 - 11:
1のペアが4つできてXORは0- もしくは
11に11をXORするから0
- もしくは
もちろん一気通貫の場合と結果は同じですが, mod 4で何故どんな値が出るかはっきりしました. これをまとめたのが最初の実装です.
ついでに: 数学での実験¶
念のため書いておくと数学でもこの手の実験・具体例の確認はとても大事です. 具体例を確認した結果をそのまま数学的帰納法で証明に持ち込む単純な場合もあります. もっと言えば面白い具体例, 特に反例ができればそれで論文が書ける場合さえあります. 有名な予想に対して反例を提出して解決して有名になった人もあり, その論文・講演がいまでも語り草になるほどです.
Mr. Counterexampleとして世界的に名を馳せた日本人数学者として永田雅宜がいます. 私の専門だった作用素環でも荒木の場の量子論・量子統計力学からのIII型フォン・ノイマン環の構成や, パワーズによる量子統計力学を媒介にした連続無限個の$\mathrm{III}_{\lambda}$環の構成は特に有名です.
089 E - Colorful Hats 2¶
- created: 2022-12-19 mon
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
- 要点: アルゴリズムの検討
入出力¶
1 2 3 | |
解説¶
公式解説通りに実装します.
MOD計算¶
計算漏れしないように演算子を定義してそれを使いましょう.
1 2 | |
MODはInt32でも問題ないと思いますが, たまにオーバーフローしてはまり倒すため, 怪しそうな場合はとにかくInt64に倒します.
大枠¶
各iごとに計算した結果をかけて積んでいけばよく, 単純にArray.foldでループを回します. 変数名は公式解説に合わせます.
積む変数は最終的な計算用の値であるTiとxi,yi,ziです. 前の人までの帽子の値が必要なためxi,yi,ziもStateに積む必要があります. 初期値はかけ算の初期値だからTi = 1, 帽子の数は(xi,yi,zi) = (0,0,0)でよく, 特に次のように書けます.
1 2 3 | |
最終的に必要なのはtの値だからそれをfstで切り取ります. タプルを切り取るのはfstとsndまでしかないため, この最後の切り取りが単純になるようにStateを構成しています.
folderの構成¶
まずtを更新します. filterとlengthを組み合わせるのが素直な実装です. ここではsumByで次のように処理します.
1 | |
いまはInt64でtを積むため, filter >> lengthで処理する場合は最後にint64をかませる必要があります.
次は(x,y,z)の更新です. これは単純な場合分けで十分です.
1 | |
あとはこれらの値をタプルにまとめて次のステップに回します.
1 | |
MODつきのかけ算にするよう注意しましょう.
まとめ¶
ここまでの処理をまとめると次のように書けます.
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
090 D - Even Relation¶
- created: 2022-12-20 tue
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
- 要点: アルゴリズムの検討・実装
入出力¶
1 2 3 | |
解説¶
DFSで素直に木を走査します.
隣接リスト生成¶
二点間の距離の処理によって全体の処理が変わるため, そこに注意すればあとは素直に作るだけです. ここではw&&&1とビットの論理和で処理します.
1 2 3 4 | |
他の問題ではResizeArrayで処理したときもありますが, ここではListで処理しています. 今回せっかくなので試してみたら, 少なくとも今回のケースではListの方が速いようでした.
DFS¶
再帰とfoldで隣接リストを走査します. 今回はArray.zeroCreate Nで初期化した配列の値を再帰の中でゴリゴリ書き換える形で実装します.
根と初期値を適当に決める必要があります. ここではdfs関数をpiを根のインデックス, ciを子ノードのインデックス, vを0,1の値として構成します. 特に次の形で計算をはじめます.
1 2 | |
dfsを実装しましょう. 本体のfold処理は次のように書きます.
1 2 3 4 5 | |
まずArray.get Aa ciでdfsの引数で指定した隣接リストの子ノードを取ります. Array.filterで親のノードを除外した上でfoldの中でXaをゴリゴリ書き換えます. foldのgciはdfsに「子ノードの子ノード」として食わせる前提で孫(grandchild)の想定で名付けた変数です. 再帰的にdfs ci gciで呼び出して値を更新します. ここでv^^^wと排他的論理和で距離を処理しています. 親子で偶奇が違う場合だけフラグが立ちます.
最後にコメントアウトで残した書き換え処理を考えましょう. ciで渡した子ノードの値を書き換えます. ここではv^^^1として排他的論理和で値を書き換えます. 結果的にdfsは次のように書けます.
1 2 3 4 5 6 | |
まとめ¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
091 D - Coloring Edges on Tree¶
- created: 2022-12-21 wed
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
- 要点: アルゴリズムの検討・実装
入出力¶
1 2 3 | |
解説¶
公式解説ではBFSで解説していました. ここでDFSで木を走査します.
隣接リスト生成¶
問題の指示によってi本目の辺はa_iとb_iを含むとしているため, 隣接リストを作るときに辺の番号も同時に割り振ります. ループのときの配列の添字と混同しないようにここでedgeのeを採用して記述します.
1 2 3 4 | |
ここでは配列の中身をリストにしました. 問題90ではResizeArrayよりリストの方が速かったため, 何となくリストにしています. 何が速いかはきちんと検証するべきです.
DFS¶
再帰とfoldで隣接リストを走査します. 今回はArray.zeroCreate Nで初期化した配列の値を再帰の中でゴリゴリ書き換える形で実装します. IntMap(いわゆる辞書)を使っているHaskell実装もあります.
dfs関数には親ノード・子ノード・色と書き換える配列を渡せばよいでしょう. 大枠として次のように書けばよいはずです.
1 2 | |
ではdfsの中身を考えましょう. まず指定した子ノードがつながっている頂点を取るためGa.[ci]を取ります. この中から親ノードを排除したいためfilterをかませます.
1 | |
これに対して最終的にノード番号とのタプルになるように色を順に割り振ります. dfsの引数に入っているcolor以外の色を割り当てるように色を振るため少し工夫します. 前段のfilterと合わせると要素数の処理が面倒なため, 遅延処理のSeqを使って力づくで辻褄を合わせます. 結論から言うと次のように書きます.
1 | |
Seq.zipはseqを二つ与えてそれらをタプルにしたseqを返します.
1 2 3 | |
二番目のリストの最後の15が結果で消えている点に注意してください. List.zipやArray.zipでは「長さが等しいリスト・配列を与えなさい」と怒られます. Seq.zipなら余計な長さの部分を切り落としてくれるため, Seq.initInfiniteで無限リストを生成し, 無限リスト中の不要な要素をfilterで切りつつ, 必要なところだけ切り出す都合のいい処理が書けます. あとはこれと入力の配列をfoldで処理します.
1 2 3 | |
fold中ではdfsの引数を子ノード・孫ノードのciとgciにずらして, Cq中で指定した色と更新した配列を与えます.
最後にdfsの結果から出力用のデータを生成します. 出力では色の数も返す必要があるからです. 出力のXaから最大値を返せばよく, 例えば次のように書けばよいでしょう.
1 2 3 4 5 | |
SeqもArrayもリストのcons(::)のような先頭への追加がなく, 配列同士の結合のappendしかありません. 配列にせず, 最終的に返すべき改行区切りの文字列を生成してもいいでしょう.
092 B - Unplanned Queries¶
- created: 2022-12-21 wed
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
- 要点: アルゴリズムの検討
入出力¶
1 2 3 | |
解説¶
公式解説通りに素直に実装します. アルゴリズムを考えるのが全てで実装は簡単です. 各頂点の出現数を数えて偶奇判定するだけのシンプルなプログラムでよいため, 次のように書けば終わりです.
1 2 3 4 5 6 7 8 9 | |
093 D - Transit Tree Path¶
- created: 2022-12-22 thu
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
- 要点: アルゴリズムの検討
解説1, DFS¶
公式解説通りにDFSで実装します.
入出力¶
1 2 3 4 5 | |
距離cだけint64にしています.
隣接リストの生成¶
ここまでに何度か出てきたのと同じように素直に実装すれば問題ありません.
1 2 3 | |
DFSの実装¶
Array.zeroCreate N |> dfs (K-1) (-1) 0Lを前提に実装すると次のようにすっきり書けます.
1 2 3 | |
F#の配列は破壊的なデータ型で, Array.setはunitを返すだけで書き換えた配列を返してくれるわけではないため, Array.set Da v dを別立てにしています. foldの中身は隣接リスト内の値が親ノードと一致していたら積んだ値を返し, それ以外は入力のdに対して隣接リストが持つ距離cを素直に足して積むだけです.
二点間の距離の計算¶
あとはKからの最短距離を素直に計算します.
1 | |
まとめ¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
解説2: ダイクストラ法¶
他のF#解答を参考にダイクストラ法での実装も紹介します. もとのコードは二つポイントがあります.
- 破壊的な実装である.
PriorityQueueの代わりにSetを使っている.
もとの入力をリストで処理している一方, 私は配列で実装しています. 処理系も違うため単純な速度比較はできません. どなたか入力を合わせて私のコードと速度比較してみてください. そのうち自分で実装してみようとは思っています.
入出力¶
1 2 3 4 5 | |
大枠¶
DFSと同じで次のように書けます.
1 2 3 4 5 6 7 8 9 10 11 12 | |
つまりDFSがdijkstraに変わっただけです.
ダイクストラ法¶
私はまだ一般的なダイクストラ法をきちんと理解できていません. 詳しくはアルゴリズムの本を参照してください.
今回の実装に関しては次の通りです.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
もとのコードはloopが完全に破壊的です. このコードもDa.[K] <- 0Lやloopの中でのDa.[bi] <- sが厳密には破壊的なコードです. わざわざ非破壊的にするほどでもないため, 大目に見て実装しています.
ポイントは優先度つきキューの代わりにSetを使っている点です. キュー代わりのSetに積んだ値から最小値を取り出し, キューが尽きるまでループをくり返しています. もちろんSetでは速度は出ません. また配列のArray.setと違ってSet.addは更新したSetを返してくれる非破壊的な関数です.
ちなみに.NET6で優先度つきキューが実装されたものの, AtCoder上のF#は.NET Core 3.1.201で使えません.
TODO 確認: 解説2とオリジナルの破壊的な実装の速度比較¶
094 D - Blue and Red Balls¶
- created: 2022-12-23 fri
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
- 要点: 組み合わせの処理
解説¶
数学的な組み合わせ処理とmodつきnCrの実装だけです. 後者は自分用のライブラリ・関数を用意しておくべきです.
入出力¶
1 2 | |
順列・組み合わせ系の関数¶
modつきの計算の場合, いくつかの処理が簡略化できます. 逆数を取る走査もかけ算で表せるためそれを前提にした実装です. 単純に順列はp, 組み合わせはcにしています.
1 2 3 4 5 | |
これ以外の順列・組み合わせ系の関数実装サンプルがArithmetics.fsxにあります. 必要に応じて参照してください.
各iに対する計算¶
1 | |
まとめ¶
1 2 3 4 5 6 7 8 9 | |
095 D - Xor Sum 4¶
- created: 2022-12-23 fri
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
- 要点: 組み合わせの処理
解説¶
入出力¶
2^{60}はint64の範囲におさまります.
1 2 3 | |
mod計算用の演算子定義¶
1 2 | |
たまにはまる場合があるため, aとbにも都度%MODをかませています.
実装¶
計算するのは排他的論理和で各ビットごとに計算した結果を積めば十分です. 特に10進表記のaの各iビットは(a>>>i)%2Lで取れます. ビットごとの和が必要なため, ビットでの各i桁ごとにAa |> Array.sumBy (fun a -> (a>>>i)%2L)を計算します.
1 2 | |
解説にあるように各ビットごとの問題のXORの総和は0の個数 * 1の個数です. 最終的には10進数としての和を取る必要があるため, 各iごとに2^iをかける必要があります. この和はfold2で次のように計算できます.
1 2 3 | |
本体はiとyの計算です. 解説にある0の個数 * 1の個数をy .* (N-y)です. 上で書いたように, これに2^i = pown 2L iをかけています. あとはそこまでの和accに積めば総和が計算できます.
096 D - Make Them Even¶
- created: 2022-12-24 sat
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
- 要点: アルゴリズムの検討
共通の入出力¶
1 2 3 | |
解説1: 破壊的・命令型の処理¶
採用するアルゴリズム¶
たまには完全命令型の処理も紹介します. アルゴリズムは公式解説とは少し変えます. まずは各行を左から順に処理して右端に集め, 残った右端は上から処理する形に変えます. 文章でわかりにくい場合は以下の実装を見ればすぐにわかるでしょう.
ちなみに入力をいちいち書き換える形で実装しているため, REPLで実行するたび入力の価を読み込み直すのが面倒でした.
結果を保存する変数Xa¶
特に深い理由はありませんが, ここではResizeArray<string>としてはじめから最終的に返す文字列の形で積みます. ついでに文字列生成関数も作ります.
1 2 | |
各行を左から処理する¶
入力の値が奇数な場合, そこを-1して書き換えつつ, 右の価に+1して書き換えます.
1 2 3 4 5 6 | |
F#のジェネレーター(?)は0..Hで[|0..H|]とHまで作ってくれます. PythonやRustと挙動が違うため注意してください. はじめに書いたように一筆書き形式ではなく各行は右端で処理を止めるため, 列に関して0..W-2としている点に注意してください.
他の言語では+=で簡単に+1できる部分がいちいち全て書かなくてはいけません. ただこれは2022年時点で変更可能な変数が起こしてきた事故を受け, いろいろな言語は不変な変数を導入しています. 特にF#と同じく何も書かなければ標準で不変な仕様にしている言語さえ増えています. やってほしくない処理を面倒にしてそもそも敬遠させる言語設計です. これを極端にしたのがHaskellのモナド機構です.
右端の処理¶
単純に右端を上から処理します.
1 2 3 4 5 | |
特に言うべきことはないでしょう. 強いていうならIa.[i].[W-1] <- Ia.[1].[W-1]-1の更新は不要です.
出力用処理¶
これも特に言うことはありません.
1 | |
解説2: 公式解説に沿った実装¶
一筆書き経路の構成¶
ここでは先頭の奇数行を左から読み, 偶数行を右から読む形にします. 配列処理上は一行目が配列の零行目になるため偶奇が反転します. もちろん配列の零行目を右から読み始めても構いません.
1 2 3 4 5 | |
反転させている部分は添字を持っていないと面倒なため, 値だけではなく添字も持たせています. ついでに添字は入力Iaの添字ではなく, 問題指示の1-originの添字に変換しています. 一筆書き仕様に変えているため, 最後にArray.concatを使って二重配列から単なる配列に変換しました.
fold処理の大枠¶
一筆書きのJaを使ってfoldで処理します. 値の入れ替えは文字列化してリストで記録します.
問題は値の入れ替えともとの配列の値を見た書き換え処理です. もともと偶数であったとしても隣を書き換えた結果, 奇数として処理する必要が出てきます. 入力の配列の価を書き換えずに処理するには前の項の偶奇をfoldに積みます.
これらをまとめるとfoldで取り回す値は([],true,(0,0))とすればよいでしょう. はじめの値が変更した場所を積むリスト, 次の真偽値は前の値の偶奇判定結果, 最後の値は入力の配列の添字です.
これをもとに大枠は次のように書けます.
1 2 | |
最終的には最小処理回数も返す必要があり, それはリストの長さであるため, 文字列化して先頭に積みます. 破壊的な処理ではResizeArrayにAddで積みましたが, 今回はリストにconsで積んだため最後にList.revが必要です.
foldの中身¶
前の値が偶数か奇数か, 新たな値が偶数か奇数かで四通りの判断が必要です. 言葉よりも実装を見る方が速く正確でしょう.
1 2 3 4 5 6 7 | |
前の値がtrue, つまり偶数だったなら変更はなくaccに値を積みません. ただしfoldで新たに来た値にその真偽を積み, 奇数だった場合は次の処理で変更を積みます.
前の値がfalseだったときを考えます. もとの値vが偶数だと変更処理が入って奇数になるため, bにfalseを積む必要があります.
097 C - Base -2 Number¶
- created: 2022-12-24 sat
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
- 要点: アルゴリズムの検討
入出力¶
1 2 | |
参考¶
正のnに対するn進展開とn進展開を十進展開に直す関数をライブラリに記録しています. 具体的にはArithmetics.fsxのdecimalToNaryとnaryToDecimal関数です. 必要に応じて参照してください.
解説1: 再帰関数¶
大枠¶
うまく実装すれば処理できると思いますが, ここでは単純にN=0かどうかで場合わけします. 本体の処理は再帰関数で対応するため, 大枠は次のように実装します.
1 2 3 | |
再帰関数は数値のリストを作って, 最後にString.concatで連結します.
再帰関数¶
まず引数にわたってくるnを2で割ってどんどん小さくします. n=0になったら積んできたリストを返せばよいため, if n=0 then accは規定路線です. あとは本体の再帰プロセスを考えます.
-2進の部分で少し工夫が必要です. 結論から書くと次のように書けます.
1 2 3 | |
F#の%は負の数に対して負の値を返すため, absをかませて正の値にした上でaccに積みます. 次にfrecに食わせる値は(k-n)/2にしています. もちろんここは(n-k)/(-2)でいいのですが, 符号分を手計算で処理しています.
解説2: unfoldによる処理¶
同じ処理をunfoldで書きます. こちらは結論だけにします.
1 2 3 4 5 6 7 | |
unfoldは公式リファレンスまたはReference.fsxを参照してください.
098 C - Palindromic Matrix¶
- created: 2022-12-24 sat
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
- 要点: アルゴリズムの検討
はじめに¶
解説を書いていたら通ってはいけないコードが通っているようです. 例えばこの提出コードは次の入力が通らないもののACになっています.
1 | |
いろいろ考えていたら混乱してきたため, 2022-12-24時点で解説は書き切らず明確なところまでで終わりにします.
入出力¶
1 2 3 | |
公式解説でのH,Wがともに奇数の場合の図¶
1 2 3 | |
公式解説の補足¶
- サイズ 1, 2, 4 のグループ: 解説ページの行列で
a,bは四箇所,c,d,eは二箇所,fは一箇所あります. この行列(のブロック)として何箇所に現れるかをサイズと呼んでいます. H,Wが奇数と奇数ではない場合, サイズ1の要素があると回文にならないため条件文に適切に反映させる必要があります.
用語¶
a,b,cなどのアルファベットを文字種と呼ぶ. アルファベットをいくつ含むかを「文字種の数」または「文字種の個数」と呼ぶ.- 特に
"aaabbc"という入力に対して次のように定まる.- 文字種は
a,b,cの3個ある.
- 文字種は
- 特に
- 入力の中で各アルファベットが何文字あるかを表す数を「文字の数」または「文字の個数」と呼ぶ.
- 特に
"aaabbc"という入力に対して次のように定まる.- 文字
aは3個. - 文字
bは2個. - 文字
cは1個.
- 文字
- 特に
基本的な考察¶
H,Wがどちらも偶数の場合¶
縦・横ともに回文を作るために必ず縦・横の鏡写しの分の四つ必要です. つまり全ての文字種の文字の個数は必ず4の倍数になっていなければなりません.
(H,W)がともに奇数の場合¶
公式解説で説明があった場合です. 先も図を引用した公式解説でいうfにあたる箇所, つまり回文(鏡映)の中心があり, ここは文字の個数が1だけあれば十分な場合があります. 他にも(H,W) = (1,3)でのabaやaaaのように文字の個数が2の文字があっても許される場合があり 文字の個数が3の文字があっても許される場合があります.
少なくともHかWのどちらが偶数の場合¶
ともに奇数の場合と違って鏡映の中心の文字が設定できないため, 文字の個数が奇数個の文字種があると破綻する場合があります. 例えば(H,W) = (1,4)の場合の"aaab"や, (H,W) = (1,6)の場合の"aaabbb"は不適です.
解説¶
前処理¶
入力の要素は自由に並べ替えられるため, 入力行列での文字の位置やどんな文字があるかは関係なく, 文字種の数と各文字の個数がいくつあるかを勘定すれば十分です.
1 | |
まずString.concat ""で入力の文字列を全て連結して一つの文字列にしています. Seq.groupBy idで文字種とその数をグループ化して取得します. 最後にある文字種がいくつあるかだけを取るべくSeq.map (snd >> Seq.length)をかけています.
(true,true)¶
小さいブロックから考えましょう.
まず(H,W) = (2,2)とします. このときありうるのは次の形だけです.
1 2 | |
つまり全ての文字が一致して文字数は4です.
次に(H,W) = (4,4)とします. このときどこか一つに文字を置くと, その文字はちょうど鏡写しで必ず四つ存在します. 具体的には次のような形状です.
1 2 3 4 | |
つまり現れる文字は常に4の倍数です. 変数m4はいったんmod 4でフィルターしていて, その結果から和による積み上げでs1とs2を作っています. これらはどちらも0でなければなりません.
(true,false), (false,true)¶
これは縦・横が入れ替わっただけで本質的には同じです. 後者で考えましょう.
例えば(H,W) = (1,2)のような具体例を考えればわかるように, 公式解説の奇数・奇数ペアのfにあたる中心はありません. したがってただ一つだけある文字種があってはならないため, s1 = 0の条件が必要です.
次は二つだけある文字種がどれだけあってよいかを考えます. これも小さい方から具体的に考えましょう.
(H,W) = (1,2)で考えると次の形しかありません.
1 | |
次に(H,W) = (1,4)を考えると次の二通りが考えられます.
1 | |
1 | |
全て同じ文字種か, 文字種が二種類あって違う場合です.
ここで(H,W,Ia) = (1,4,[|"aaab"|])という不適格な場合を考えましょう.
ここで(H,W,Ia) = (1,6,[|"aaabbb"|])を考えます.
(false,false)¶
公式解説にあるブロックを引用します.
1 2 3 | |
099 B - Simplified mahjong¶
- created: 2022-12-25 sun
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
- 要点: アルゴリズムの検討
入出力¶
1 2 3 | |
解説¶
方針¶
公式解説とは違い, 次の方針で前から順に計算します.
- 各
iごとに自分自身でペアを作れるだけ作る. - 各
iごとにあまりは一枚出るか出ないかで, これを次に持ち越す. - 前の
iであまりがあった場合は, 積み残しとのペアを考えつつ自分自身でペアを作れるだけ作る.
次のfoldで素直に処理できます.
1 2 3 | |
accがペアの数を積む変数でmがあまりの有無を表します. 最後に必要なのはペアの数を表すタプルの第一変数だからfstで切り出します.
fold内の条件分岐¶
まずあまりがなかった場合はごく単純に次のように書けます.
1 | |
次の二つはあまりがないときの分岐です. まず入力のA_iが0の場合はペアを作りようがないためそのまま次に回します.
1 | |
今度は入力のA_iが非零の値を持つため, 前からの積み残しの分を考えて計算します.
1 | |
積み残しと一つペアを作るため, 次に積み回すための商とあまりのq,rはa-1から計算します. さらにa-1で計算した以上ペアがもう一つできているためペアのカウントはacc+q+1で+1が必要です.
まとめ¶
1 2 3 4 5 6 7 8 9 10 11 | |
100 C - Vacant Seat¶
- created: 2022-12-25 sun
- ご意見・ご要望はissue・プルリク用のGitHubまで
- GitHub上の対応ディレクトリ
- 公式ページ
- 要点: アルゴリズムの検討,
for breakの処理
解説¶
公式解説通りに素直に実装します. F#ではforのbreakがありません. それは再帰関数で処理できます. AOJのHaskellやOCamlのコードでも時々観測できます.
結論として次のように書けます.
1 2 3 4 5 6 7 8 9 10 11 12 | |