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
で隠蔽している分コードが読みにくいかもしれません. オリジナルは最終的に呼び出す関数が素直な再帰になっているため, こちらの方がわかりやすいかもしれません.