ABC374 振り返り - Obsidian Publish# ABC374 振り返り
- [キーエンスプログラミングコンテスト2024(AtCoder Beginner Contest 374) - AtCoder](https://atcoder.jp/contests/abc374)
- ABCD 4完 (1105) ··· [コンテスト成績証 - AtCoder](https://atcoder.jp/users/oceajigger/history/share/abc374)
![[スクリーンショット 2024-10-06 11.22.19.png]]
ABC374 でした。
前回 [[ABC373 振り返り | ABC373]] に似ていて E, F の難易度が高めでした。
- A (灰) ··· 文字列の末尾3文字をとって `san` かどうか調べる
- B (灰) ··· 先頭から比較して文字が異なるインデックスを調べる
- C (灰) ··· 部分集合を全部列挙して試す。一般にはビット全探索。Haskell なら `subsequences`
- D (茶) ··· 順列全探索 + ビット全探索で全経路試す
- E (水, upsolve) ··· 答えで二分探索。二分探索に渡す $f$ 関数の構成が難しかった
でした。
E は「答えで二分探索」なのがわかっていても解けず。
テストケースは通るものの WA がなくせず、時間切れでした。惜しかったと思いたいところですが、考察してみると、自力で答えに到達するのは難しかったのがよくわかりました。
## [A - Takahashi san 2](https://atcoder.jp/contests/abc374/tasks/abc374_a)
文字列 $S$ の末尾が `san` になってるか確認する。
Haskell なら `takeEnd 3` で後ろから 3文字取るとよいです。
```haskell
main :: IO ()
main = do
s <- getLine
let s' = takeEnd 3 s
printYn $ s' == "san"
```
## [B - Unvarnished Report](https://atcoder.jp/contests/abc374/tasks/abc374_b)
文字列 $S$ と $T$ を比較して、異なる場合は異なる最初の位置を特定する。
まず $S = T$ の場合を場合分けで処理。これで単純化する。
あとは `zipWith (=)` で $S$ と $T$ の異なる最初の位置を探す。
それでも同じ場合は長さが違うので、それを考慮する。
過去にも似たような問題があった気がします。
と、いいつつ $S$ の長さしかみておらず、$T$ の長さを考慮するのを失念して 1ペナもらいました。
```haskell
main :: IO ()
main = do
s <- getLine
t <- getLine
let n = length s
m = length t
if s == t
then print 0
else do
print $ case elemIndex False $ zipWith (==) s t of
Just i -> i + 1
Nothing -> min n m + 1
```
## [C - Separated Lunch](https://atcoder.jp/contests/abc374/tasks/abc374_c)
$N \leq 20$ 程度の整数を $2$ つのグループに分割する、というのが主眼です。
$20$ しかないので部分集合を全部列挙できます。全部列挙しても $2^{20}$ で $10^6$ 程度のサイズです。
Haskell の場合、リストの部分集合を全部列挙するのに `subsequences` を使うと良いです。
まず、$K_i$ の合計を求めておく。
部分集合を求めて、その総和がグループ $A$ 側の人数。$K_i$ の合計からそれを引いたものが $B$ 側の人数とみなせます。
その二つのうち大きい方が「同時に昼休みをとる最大人数」になります。
これを全部分集合に対して試して、最小を取る。
```haskell
main :: IO ()
main = do
_ <- getInt
ks <- getInts
let s = sum' ks
ans = minimum [max (sum ks') (s - sum ks') | ks' <- subsequences ks]
print ans
```
## [D - Laser Marking](https://atcoder.jp/contests/abc374/tasks/abc374_d)
平面上に、指定された $N$ 本の直線を引くにあたりレーザー照射の移動にかかる時間を最短にしたい。
レーザーを照射しているときは速度 $S$ 、そうでないときは速度 $T$ で移動できる。
$N \leq 6$ と小さいので、C 問題に同じく全列挙でよいです。
今回は $N$ 個の直線をどういう順番で引くか、が関心事なので `permutations` を使います。計算量は $O(N!)$ です。
全パターン試すには直線を描く順番に加えて、直線の開始位置を $v_1 = (A_i, B_i)$ と $v_2 = (C_i, D_i)$ どちらを先にみなすかの判断が必要。
直線ごとに $(v1, v2)$ と解釈するか、反転させて $(v2, v1)$ と解釈するかを全パターン試したい。
ビットマスクのように $N$ 個に対して `[True, False, True, ...]` みたいな反転するか否かの系列を全パターン生成するとよいです。
これまたビット全探索、Haskell なら `sequence $ replicate n [True, False]` で書けます。更にいうと `replicateM` をリストモナドに適用する `replicateM n [True, False]` でも OK
```haskell
ghci> replicateM 3 [True, False]
[[True,True,True],[True,True,False],[True,False,True],[True,False,False],[False,True,True],[False,True,False],[False,False,True],[False,False,False]]
```
便利です。
こちらの計算量は $2^6 = 64$ 程度です。
`permutations [0 .. n - 1]` と `replicateM 6 [False, True]` の計算量を合算しても $6! \times 2^6 = 720 \times 64 = 46080$ 通りと小さいです。
なお `permutations` と `replicateM` の直積はリスト内包表記で 合成すると簡単です。
あとはその全列挙したパターンに対し、距離関数を定義して全部計算し最小値を得ます。
`Double` で全部計算しても精度としては問題ないです。
```haskell
main :: IO ()
main = do
(n, s, t) <- auto @(Int, Double, Double)
xs <- replicateM n $ do
[a, b, c, d] <- map (read @Double) . words <
gt; getLine
return ((a, b), (c, d))
let xa = listArray @Array (0, n - 1) xs
ans =
minimum
[ resolve s t xa path choices
| path <- permutations [0 .. n - 1],
choices <- sequence $ replicate n [True, False]
]
print ans
resolve s t xa path choices =
let path' = ((0, 0), (0, 0)) : zipWith (\i c -> bool (xa ! i) (swap $ xa ! i) c) path choices
dists = zipWith fn path' (tail path')
where
fn (_, u2) (v1, v2) =
let d1 = distance u2 v1
d2 = distance v1 v2
in d1 / s + d2 / t
in sum' dists
distance :: (Double, Double) -> (Double, Double) -> Double
distance (x1, y1) (x2, y2) = sqrt ((x1 - x2) ^ 2 + (y1 - y2) ^ 2)
```
## [E - Sensor Optimization Dilemma 2](https://atcoder.jp/contests/abc374/tasks/abc374_e)
結論、答えで二分探索でした。
$N \leq 100$ と小さいので DP かと思いましたが、DP をどう構成しても計算量を落としきれない。
一方「最小値の最大値」とか、ある製造能力 $w$ を達成したいときに必要な予算 $f(w)$ は $O(N)$ 程度で計算できそうな雰囲気があるので、おそらく $f(w) \leq X$ を二分探索する問題と踏みました。
この方針は合ってました。
問題はその $f$ 関数の定義です。
- 製造能力を $i \ldots N$ 工程まですべて、最低でも処理能力 $w$ にするためには、 $S_i$ を幾つ購入し、$T_i$ を幾つ購入しその結果コストは幾らになるか?
を計算するわけです。
まず機械 $S_i$ だけを購入するパターンを考えてみます。
機械 $S_i$ は $1$ 台あたり $A_i$ 個分処理できるわけで、それだけで $w$ に到達させるには $\lceil {w}/{A_i} \rceil$ 個、$S_i$ を購入すると良いです。これで必要な $S_i$ の個数が求まる。
機械 $T_i$ に対しても同様に考えられる。つまり $\lceil w/B_i \rceil$ 個
問題は、二つの機械を混ぜて買うときです。ここの最適解の割り出しが難しい。
ここは解説にある通り、$S_i$ の台数の上界は $B_i$ 、$T_i$ の台数の上界は $A_i$ として全探索すれば良いようです。
$A_i, B_i \leq 100$ なのでもっと雑に、それぞれ $0 \ldots 100$ 台までを考慮すれば良い。
$S_i$ の台数 $k$ が求まれば、$w$ 到達に必要な残りは $w - k \times A_i$ になるので、先に同様それを $B_i$ で `ceildiv` することで $T_i$ の台数およびコストが求まります。
$T_i$ の台数 $k$ を探索するときにも同じ。
うーん、まさか一方の台数の上界が $100$ で良いとは、まったく思いつかず (というか未だあまりよく飲み込めていない 😓)
この考察に至らず三部探索とか色々やっていましたが、だめでした。
```haskell
main :: IO ()
main = do
[n, x] <- getInts
items <- replicateM n $ do
[a, p, b, q] <- getInts
return (a, p, b, q)
let (ok, _) = bisect2 (0, 10 ^ 9 + 1) (\wi -> f wi <= x)
where
f wi = sum' [cost item wi | item <- items]
cost (a, p, b, q) wi =
let ss = (wi `ceildiv` a) * p
ts = (wi `ceildiv` b) * q
both1 =
minimum
[ k * p + (remain `ceildiv` b) * q
| k <- [0 .. 100],
let remain = wi - k * a,
remain >= 0
]
both2 =
minimum
[ k * q + (remain `ceildiv` a) * p
| k <- [0 .. 100],
let remain = wi - k * b,
remain >= 0
]
in minimum [ss, ts, both1, both2]
print ok
```
## 感想など
E は本戦中に解けそうで解けない感じだったので、惜しいかと思っていましたがよくよく考察してみると、自力では上界を $100$ にして全探索するという理屈にまったく辿りつけなかったですし、そこがこの問題の本懐だと思います。というわけで、惜しくない。
答えで二分探索なのは慣れていれば雰囲気で分かるわりに Difficulty が高いので何でだろうと思いましたが、合点がいきました。
終わってみれば B の 1ペナがもったいなかったです。
これがなければ水色パフォーマンスで、もうすこしレートが上がっていました。
が、+2 で微増、減らなかっただけでもよしとしましょう。
次回 ABC375 も予定どおり参加予定です。