gt; readIORef acc ``` ## [D - Line Crossing](https://atcoder.jp/contests/abc402/tasks/abc402_d) $N$ 本の直線のうち、交差しているものの組み合わせ数を考えます。 平行なものの組み合わせがわかれば、余事象から交差しているものの組み合わせ数も得られます。 点 $A$ と点 $B$ を通る弦 $AB$ の傾きは、$AB$ の中点から円の中心に向かって引かれた直線に必ず垂直になります。 従って各 $AB$ の傾きは、中点 $AB$ の位置によって特徴付けられます。 この問題では各点の座標は分かっていませんが点は等間隔に並んでいるので $\frac{A_i + B_i}{2}$ で求めることができそう、さらに円環になっていることからその $\bmod N$ を取れば良いことがわかります。加えて比較にあたっては $2$ で割っていることは無視できるので、結論 $f(A_i, B_i) = A_i + A_b \mod N$ と写像することで、各直線 $(A_i, B_i)$ の特徴量が得られます。 あとは同じ特徴量の値になる値の出現頻度の $_nC_2$ を取ることで、2つ選んだときに平行になる組み合わせ数 $x$ が分かります。 そして $M$ 本から2つ選んだときの組み合わせ数 $_MC_2$ からこの $x$ を引くことで、余事象である交差する組み合わせ数が得られます。 ```haskell main :: IO () main = do [n, m] <- getInts vs <- replicateM m getTuple let bucket = toBucket (0, n - 1) [k | (a, b) <- vs, let k = (a + b) `mod` n] x = sum . elems $ amap nc2 bucket print $ nc2 m - x ``` なお考察の思考プロセスとしては - 直線の交差を考える問題は、傾きがポイントになることが多い。直線を傾きもしくは傾きと一意に対応する値に写像できないか考える - 特にこの問題は $1 \leq N \leq 10^6$ あるので、同じ傾きになるものをまとめ上げられると突破口が見えそう というところが起点になっています。つまり、たくさんあるデータを特徴量に写像することでまとめ上げるという典型考察です。[ABC248 E - K-colinear Line](https://atcoder.jp/contests/abc248/tasks/abc248_e) なども同様の考え方で解けます。  Haskell は基本的に map / fold でデータを操作するよう体系が確立されているので、同様に思考も写像と集約で考えるように鍛えられていきます。 こういう問題をみると、まずは何かに写像するという 🧠 になります。 ## [E - Payment Required](https://atcoder.jp/contests/abc402/tasks/abc402_e) 期待値 + bit DP の問題です。 DP の状態を考えること自体はさほど難しくないのですが、この問題はおそらく「配る DP」ではうまく解けない、というのが厄介なところ。「もらう DP」で実装する必要がありました。 まず、期待値を求める、制約が中途半端に小さい、$1 \leq N \leq 8$ あたりの制約から bit DP であることに当てにいっても問題ないでしょう。DP で管理すべき状態になりそうなのは「正解した問題の集合」と「その時点での残高」あたりでしょうか。 期待値 DP の考え方として、まず期待値が $0$ になる状態を探すのを起点にすると良いです。 期待値が $0$ になるのは「全ての問題を解いた」か「所持金的にもう解く問題が残っていない」状態です。 その状態からは解ける問題が残っていないので、その期待値を $0$ にし、真逆の状態...つまり「一つも問題を解いていなくて所持金 $X$ 持っている」状態の期待値を求める方向で考えます。 つまり $dp(S_{empty}, X)$ が求めたい状態。 さて、具体例として $5$ 問問題があったとして、そのうち $\{1, 3\}$ の問題に正解した状態を考えてみます。 ここから、次に $4$ 問目を狙うとします。 - 狙った $4$ 問目を解けなかったケース ··· $a =\{1, 3\} を解いていて、残高が \space C_4 \space 減った状態の期待値$ - 狙った $4$ 問目を解けたケース ··· $b = \{1, 3, 4\} を解いていて、残高が \space C_4 \space 減った状態の期待値$ となります。 図にすると以下の状態遷移です。 ![[IMG_3661.jpeg|600]] というわけで配るDP で上記の遷移を実装すれば答えは求められる、と思いたいところですがそうは問屋が卸さず。 **この問題では解けなかった問題には再チャレンジすることができます。** ある状態から、どの問題を選択するかのルートの最適化は、同じ状態に到達してきたルートの max を取ることで可能なのは従来の DP 通りです。 しかしこの問題ではある状態から「解ける場合」「解けない場合」に二方向に遷移し、 **「解けない場合」は、同じ状態を何度も訪問し (残高だけは減ります) その期待値を累積しなければなりません。こちらの結合は max ではなく (+) での結合になります。** つまり - 配る DP(前向きに遷移を「流し込んでいく」手法)では、ある状態に来る異なる遷移ルート同士を必ず一種類の結合演算(`max` か `+` か)でまとめる - しかしこの問題では同じルート内での「成功 × 確率」と「失敗 × 確率」は足し合わせ(`+`)が必要 - 一方で、どの問題を選ぶかという最適ルートの決定には比較(`max`)が必要 と、ルート内の「和」とルート間の「最大値」を混在させる必要があります。この二つの結合演算を配る DP で扱うことは難しいです。 現在の状態に至る複数のルートどう集約 (結合) するかをマニュアル制御する実装は「もらうDP」なら可能です。 というわけで先の図の遷移とは逆向きに考えます。 ```haskell main :: IO () main = do [n, x] <- getInts xs <- replicateM n $ do (s, c, p) <- getTuple3 return (fromIntegral s, c, fromIntegral p / 100) let dp = array @Array ((emptyBS, 0), (fullBS n, x)) [((s, v), f (s, v)) | s <- [emptyBS .. fullBS n], v <- [0 .. x]] where f (bs, v) = maximumDef 0 [ p * (s + a) + (1 - p) * b | (i, (s, c, p)) <- indexed 1 xs, notMemberBS i bs, -- まだ解いてない問題で、 c <= v, -- 残高 v で OK な問題なら挑戦できる let a = dp ! (insertBS i bs, v - c) -- 問題 i が解けた b = dp ! (bs, v - c) -- 問題 i が解けなかった ] print $ dp ! (emptyBS, x) ``` 一つ前の状態からの遷移を累積和で取得する問題など、配る DP では解けない構造の DP がときどきあります。 ある状態に至るルートを一つ一つ独立に処理するのではなく、それらルートをすべて取得してから集約する必要があるタイプの問題がそれにあたると思います。この問題ではルートの結合に `(+)` と `max` の二つを使い分ける必要があることから、もらう DP での実装がおそらく必須になる、と考察しました。 とはいえ、期待値が $0$ になるのが $dp(S_{full}, 0)$ などでそこから $dp(S_{empty}, X)$ に向かっていく計算をするという考え方なら、計算の方向も逆向きとなり、こちらの方針からも「もらう DP」で実装するのが自然ではあります。