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 3
high-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 4
high-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は1
4
で初期化されたと思うと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は1
8
で初期化されたと思うと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 |
|