ABC267 A～E をHaskellで

# A - Saturday

[問題 ABC267A](https://atcoder.jp/contests/abc267/tasks/abc267_a)

シグネチャを決める。

```haskell
abc267a :: String   -- S
        -> Int      -- 答え
```

パターンマッチで直接答えを得る。

## 結果

```haskell
abc267a "Monday"    = 5
abc267a "Tuesday"   = 4
abc267a "Wednesday" = 3
abc267a "Thursday"  = 2
abc267a "Friday"    = 1
```

# B - Split?

[問題 ABC267B](https://atcoder.jp/contests/abc267/tasks/abc267_b)

シグネチャを決める。

```haskell
abc267b :: String   -- S
        -> Bool     -- 答え
```

ともかく、$S$ の先頭が `'1'` ならスプリットでない。

```haskell
abc267b ('1':_) = False
```

そうでないとき、列に立っているピンがあるかないか、を並べたものを見たとき、『「ある」「ない」$\times k$「ある」』という形がどこかにあればスプリットである。

列に立っているピンがあるかないかは、それぞれの列に属するピンの状態を取り出して調べる。倒れていることを `True` とすると、全て倒れているかは論理積で判定できる。

```haskell
bs = map ('0' ==) s
cs = map (and . map (bs !!)) [[6],[3],[1,7],[0,4],[2,8],[5],[9]]
```

さて、`cs` の中の『ある～ない～ある』のパターンの検出は、正規表現でも使えれば横着ができるのだけど、`Data.List`の関数の定番の組み合わせで何とかする。

## 結果

```haskell
import Data.List

abc267b :: String -> Bool
abc267b ('1':_) = False
abc267b s =
  case cs1 of
    False:True:False:_ -> True
    _                  -> False
  where
    bs = map ('0' ==) s
    cs = map (and . map (bs !!)) [[6],[3],[1,7],[0,4],[2,8],[5],[9]]
    cs1 = dropWhile id $ map head $ group cs
```

# C - Index × A(Continuous ver.)

[問題 ABC267C](https://atcoder.jp/contests/abc267/tasks/abc267_c)

シグネチャを決める。

```haskell
abc267c :: Int     -- N
        -> Int     -- M
        -> [Int]   -- Ai
        -> Int     -- 答え
```

位置 $i$ からの、問題の積和が求められている状態を図にするとこんな感じ。
下の長方形は普通に $A_i$ の数列で、赤い部分 $A_i$ から $A_{i+M-1}$ までの長さ一定の区間和は尺取り法で計算できる。灰色の部分が求めたい積和。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/196351/27accfcc-f768-05c1-016f-a8dab8345a74.png)
灰色の値が求められているとして、一つ右にずらした値を作るには、赤を引くことで、各 $A_i$ の係数をちょうど1ずつ減らすことができ、特に $A_i$ は消え去る。さらに右から $A_{i+M}$ の $M$ 倍を足しこめばできあがり。
なので、区間和と平行して灰色の値を順次更新して作ることができる。

## 結果

```haskell
abc267c n m as = maximum $ loop as bas0 acc0 (tail m as)
  where
    acc0 = sum $ zipWith (*) [1..m] as -- 積和の初期値
    bas0 = sum $ take m as             -- 区間和の初期値
-- 尺取り法で積和を更新して出力
    loop (l:ls) bas acc (r:rs) = acc : loop ls bas1 acc1 rs
      where
        bas1 = bas - l + r
        acc1 = acc - bas + m * r
    loop _ _ acc [] = [acc]
```

# D - Index × A(Not Continuous ver.)

[問題 ABC267D](https://atcoder.jp/contests/abc267/tasks/abc267_d)

シグネチャを決める。問題Cと同じである。

```haskell
abc267d :: Int     -- N
        -> Int     -- M
        -> [Int]   -- Ai
        -> Int     -- 答え
```

問題Cから話がさらにややこしくなっているが、逆に要素数が $2 \times 10^5$ から $2000$ と劇的に減っている、というところを見るのは「競技」に特化されすぎた思考でよくない。

最終結果を大きくしたいのだから、途中結果も大きいものの方が好ましいはず。しかし、それで捨てていい候補の範囲がどこまでか？をよく考える必要がある。この問題の場合、$A$ を前から順に見て、ある位置 $j$ までの範囲で $B$ の要素を $i$ 個まで選んでいるとき、同じ $i$ どうしでの最大値を残しておけばよい。
そもそも、$B$ を新たに選ぶとき、それが何個目かを知っていないと積和が計算できない、という事情もある。

ということで、$A$ の $1$ から $j$ の範囲で $B$ を $i$ 個 $(1 \leq i \leq \min(j,M))$ 選んだときのそれぞれの最大スコアを追跡するDPを行えばよい。

## 結果

リスト `accs` は先頭から、$i=1,2,\dots$ に対する最大値を保持する。

```haskell
abc267d n m as = last $ foldl step [] as
  where
    step accs a = zipWith1 max accs
                  [acc + a * i | (acc, i) <- zip (0 : accs) [1..m]]

zipWith1 :: (a->a->a) -> [a] -> [a] -> [a]
zipWith1 f (a:as) (b:bs) = f a b : zipWith1 f as bs
zipWith1 _ as [] = as
zipWith1 _ [] bs = bs
```

標準の `zipWith` は、要素数の少ない方で結果を切り上げるが、ここでは `accs` よりも今回の `a` を使った結果の方が長くなるので、余った要素はそのまま続ける亜種が必要になる。

# E - Erasing Vertices 2

[問題 ABC267E](https://atcoder.jp/contests/abc267/tasks/abc267_e)

シグネチャを決める。

```haskell
abc267e :: Int -> Int    -- N,M
        -> [Int]         -- Ai
        -> [(Int,Int)]   -- Ui,Vi
        -> Int           -- 答え
```

$A_i$は正の値なので、頂点を減らすことで、その頂点と接続された頂点を削除するコストは確実に減る。コストを増やしてしまうような操作はない。
つまり、作戦として、現状で最もコストの低い頂点から削除していくことで、全体のコストを下げることができるはず。

辺を調べることで、操作を行った後のコストの変動を反映させることはできる。しかし、次に削除するべきノードを特定するのに、更新されたコスト表から最小値を探すのに線形探索して $O(N)$ かけると、全体で $O(N^2)$ かかって TLE してしまう。ここをうまくやる必要がある。

ヒープ的なものに、コストを優先度、頂点番号を値とした要素を入れることで、最小値を安いコストで特定したい。しかし `Data.Heap` は最小値を $O(1)$ で見つけるが、内容の削除がうまくできない。`Data.Set`, `Data.Map` は最小値を得るのに $O(\log N)$ かかるが、削除や更新も同じ時間でできるので、これを使う。
複数の頂点が同じコストを持つ可能性もあるので、キーはコストだけでなく、頂点番号も対にすることで唯一性を確保する。（多重集合がないから…）

つまり、次のような情報を追跡する：
- コストと頂点番号の対をもつ集合 `costis`：現在コスト最小の頂点を高速に選択するためのもの。頂点の削除コストが更新されるとき、この中の対応する項目も更新する必要がある。
- 頂点に対する現在のコストの表 `costs`：上の集合はこの表の逆引き表といえる。頂点を削除したときに両者を並行して更新する。頂点が削除済みかどうかのフラグも兼ねる。

コスト $c$ の頂点 $i$ を最小コストのものとして選択したとき、`costs` から要素 $i$ を除き、頂点 $i$ からの辺を持つすべての頂点 $j$ について、そのコストを $A_i$ だけ減らすことを `costs` に反映させる。
さらに、このとき変化した項目に関して、逆引き表 `costis` を更新する。

## 結果

```haskell
import Data.Array
import qualified Data.IntMap as IM
import qualified Data.Set as S

abc267e :: Int -> Int -> [Int] -> [(Int,Int)] -> Int
abc267e n m as uvs = maximum $ loop costis0 costs0
  where
    aa = listArray (1,n) as                   -- Aiの配列
    edges = accumArray (flip (:)) [] (1,n)    -- 辺により、頂点iから接続される頂点のリスト
      [p | (u,v) <- uvs, p <- [(u,v),(v,u)]]
    ics = [(i, sum $ map (aa !) vs) | (i,vs) <- assocs edges]
    costs0 = IM.fromAscList ics               -- 初期コスト
    costis0 = S.fromList [(c,i) | (i,c) <- ics] -- 初期コストの逆引き表

    loop costis costs
      | S.null costis = []
      | otherwise     = c : loop costis3 costs2
      where
        ((c, i), costis1) = S.deleteFindMin costis    -- 最小値取り出し
        ai = aa ! i
        js = edges ! i
        costs1 = IM.delete i costs                    -- 頂点iは消す
        costs2 = foldl' f costs1 js -- 接続する頂点のコストをAiだけ減らす
        f im j = IM.update (Just . subtract ai) j im
        dels = [(c,j) | j <- js, Just c <- [IM.lookup j costs]]  -- jsについて
        costis2 = foldl' (flip S.delete) costis1 dels            -- 更新前の値を消す
        adds = [(c,j) | j <- js, Just c <- [IM.lookup j costs2]]
        costis3 = foldl' (flip S.insert) costis2 adds            -- 更新後の値を登録
```
`costs2`を作るときの関数 `f` は、削除済みの頂点のコストを負の値で復活させないようにしている。
<!-- 頂点の死活管理を `costs` に重ねるより、まだ生きている頂点の集合を別で管理して、`js` はそれに限るようにした方がシンプルにできたかもしれない。-->

`costs` をmutable vectorで実装すればより速度を稼げるが、[これで充分 AC できた](https://atcoder.jp/contests/abc267/submissions/34570232)。

<!-- 今回はここまで時間内にできたので達成感がある。-->

## 追記

「[ABC267をPythonで](https://qiita.com/MoroeTachibana-oh/items/684279b223af7d5546ed)」に
> コストは優先度付きキュー内では更新されませんので、取り出したコストの情報が古ければ使わないようにしましょう。

と、ダイクストラ法にキューを持ち込む場面と同様、古い登録情報を無視するだけで、わざわざ削除する必要はないという話があったのでやってみる。なるほど。
ついでに `Data.Array.ST` も使ってmutableな計算にして速度を稼いでみる。`Data.Set` をヒープの代用にするのもやめて `Data.Heap` を使う。

```haskell
import Control.Monad.ST
import qualified Data.Heap as H
import Data.Array.Unboxed
import Data.Array.ST
import Data.Maybe

abc267e :: Int -> Int -> [Int] -> [(Int,Int)] -> Int
abc267e n m as uvs = runST action
  where
    aa :: UArray Int Int
    aa = listArray (1,n) as                   -- Aiの配列
    edges :: Array Int [Int]
    edges = accumArray (flip (:)) [] (1,n)    -- 辺により、頂点iから接続される頂点のリスト
      [p | (u,v) <- uvs, p <- [(u,v),(v,u)]]
    cs = [sum $ map (aa !) vs | vs <- elems edges]
    costis0 = H.fromList [H.Entry c i | (i,c) <- zip [1..] cs] -- 初期コストの逆引き表

    action :: ST s Int
    action = do
      costs <- newListArray (1, n) cs :: ST s (STUArray s Int Int)
      loop costis0 costs 0

    loop :: H.Heap (H.Entry Int Int) -> STUArray s Int Int -> Int -> ST s Int
    loop costis costs acc
      | H.null costis = return acc
      | otherwise =
        do
          let Just (H.Entry c i, costis1) = H.uncons costis    -- 最小値取り出し
          ci <- readArray costs i
          if ci <= 0 then loop costis1 costs acc else do -- 処理済みなのでスルー
            let ai = aa ! i
            let js = edges ! i
            writeArray costs i 0      -- 頂点iは消す
            jcs <- forM js (\j -> do  -- 接続する頂点のコストをAiだけ減らす
              cj <- readArray costs j
              if cj <= 0 then return Nothing else do -- 生きているもの限定
                let cj1 = cj - ai
                writeArray costs j cj1
                return $ Just (j, cj1)
              )
            let costis2 = foldl (\h (j,c) -> H.insert (H.Entry c j) h) costis1 $
                          catMaybes jcs -- 更新後の値をキューに登録
            loop costis2 costs (max acc c)
```

`Data.Array.Unboxed` や `ST` 系のコンテナは、型を明記しないと怒られるので面倒。
でも immutable版は2.2秒超だったのが1.5秒弱になった。