084 C - Shopping Street¶
- created: 2022-12-16 fri
- ご意見・ご要望はissue・プルリク用のGitHubまで
- 競技プログラミングのためのF#入門
- 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の実装より簡潔になった上に意味も把握しやすくなりました. ようやくすっきり理解できてとてもいい気分です.