gt; getLine return ss let m = length t let dp = accumDP @UArray f min (maxBound :: Int) (0, m) [(0, 0)] sss where f (i, v) ss | v == maxBound = [] | otherwise = [ (i + fromJust j, v + 1) | s <- ss, let j = g i t s, isJust j ] print $ if dp ! m == maxBound then -1 else dp ! m ``` ## [E - Insert or Erase](https://atcoder.jp/contests/abc344/tasks/abc344_e) 双方向リストを作る問題です。 `IntMap` をベースに実装しました。こういう大量の更新に対して `IntMap` を使うのは、経験上ちょっと速度的な心配がありましたが... 悪い予感が的中し TLE しました。 冒頭でも書いたとおり、値の更新操作をアトミックに行わず読み/書きに分けたところ定数倍が嵩んだようです。 この TLE がなかなか取れず、30分近く格闘しましたが AC できず終わりました。 終了後 `IM.adjust` を使えるところでは使うようにしたところ、1.5sec となり AC でした。 1.5 sec は結構ぎりぎりで、挿入や削除の関数の中で少し凝ったことをするともう駄目、という感じでした。 富豪的プログラミング 🙅 ゼッタイ ```haskell main :: IO () main = do _ <- getInt as <- getInts q <- getInt qs <- replicateM q getInts let xs' = foldFor' (fromListLL as) qs $ \xs query -> do case query of [1, x, y] -> insertLL x y xs [2, x] -> deleteLL x xs _ -> undefined printList $ toListLL xs' {-- LinkedList --} type IntLinkedList = IM.IntMap (Int, Int) toListLL :: IntLinkedList -> [Int] toListLL xs = drop1 . takeWhile (/= maxBound) $ iterate (\k -> snd (xs IM.! k)) minBound deleteLL :: Int -> IntLinkedList -> IntLinkedList deleteLL x xs = do let (left, right) = xs IM.! x IM.adjust (\(_, r) -> (left, r)) right $ IM.adjust (\(l, _) -> (l, right)) left xs {-# INLINE deleteLL #-} insertLL :: Int -> Int -> IntLinkedList -> IntLinkedList insertLL x y xs = do let (_, right) = xs IM.! x IM.adjust (\(_, r) -> (y, r)) right $ IM.adjust (\(l, _) -> (l, y)) x $ IM.insert y (x, right) xs {-# INLINE insertLL #-} fromListLL :: [Int] -> IntLinkedList fromListLL as = do let (x0, prev') = foldl' (\(x, prev) (cur, next) -> (IM.insert cur (prev, next) x, cur)) (IM.empty, minBound :: Int) $ zip as (tail as) IM.insert minBound (minBound :: Int, head as) $ IM.insert (last as) (prev', maxBound :: Int) x0 ``` ### 座標圧縮 + MVector で実装 `IntMap` ではどうも 1.5 sec ぐらいが限界に感じたので、ベースを Unboxed MVector にした実装を作ってみました。 予め、最大の要素数分領域を確保する必要があるので使い勝手は少し落ちますが、その分速いです。こちらなら 570ms 程度。 最大サイズを事前に見積もる、インデックスを現実的な大きさに収める必要があるのでクエリ先読み + 座標圧縮をします。 こちらの方が、最大サイズの考慮が必要だったり作用を伴いモナディックになるなど実装としてはやや難易度が上がるので、短い時間で実装するならやはり `IntMap` の方がよいですね。 ```haskell -- ABC343 E -- https://atcoder.jp/contests/abc344/tasks/abc344_e main :: IO () main = do _ <- getInt as <- getInts q <- getInt qs <- replicateM q getInts -- 値の挿入にはインデックスが必要なので先に値を座標圧縮して索引を振っておく let qs' = concat [ case query of [1, x, y] -> [x, y] [2, x] -> [x] _ -> undefined | query <- qs ] (rank, ix) = zaatsuRank 0 (minBound : as ++ qs') n = rangeSize (bounds rank) xs <- newLL n (-1) for_ (zip (minBound : as) as) $ \(x, y) -> do insertAfterLL xs (ix x) (ix y, y) for_ qs $ \case [1, x, y] -> insertAfterLL xs (ix x) (ix y, y) [2, x] -> deleteLL xs (ix x) _ -> undefined result <- getElemsLL xs printList result {-- VUM LinkedList --} type LinkedList s e = VUM.MVector (PrimState s) (Int, Int, e) -- 最大要素数 n の双方向リスト -- 先頭の番兵のインデックスは 0 / インデックスは値に対して座標圧縮で振る newLL :: (PrimMonad m, VUM.Unbox e) => Int -> e -> m (LinkedList m e) newLL n def = do xs <- VUM.new (n + 2) VUM.write xs 0 (minBound, VUM.length xs - 1, def) VUM.write xs (VUM.length xs - 1) (0, maxBound, def) return xs insertAfterLL :: (PrimMonad m, VUM.Unbox e) => LinkedList m e -> Int -> (Int, e) -> m () insertAfterLL xs i (j, val) = do (_, right, _) <- VUM.read xs i VUM.modify xs (second3 (const j)) i VUM.write xs j (i, right, val) VUM.modify xs (first3 (const j)) right {-# INLINE insertAfterLL #-} deleteLL :: (PrimMonad m, VUM.Unbox e) => LinkedList m e -> Int -> m () deleteLL xs i = do (left, right, _) <- VUM.read xs i VUM.modify xs (second3 (const right)) left VUM.modify xs (first3 (const left)) right {-# INLINE deleteLL #-} getElemsLL :: (PrimMonad m, VUM.Unbox e) => LinkedList m e -> m [e] getElemsLL xs = tail . reverse <gt; loop [] h where (h, t) = (0, VUM.length xs - 1) loop acc k | k == t = return acc | otherwise = do (_, next, val) <- VUM.read xs k loop (val : acc) next ``` ## (追記) HashMap に変更すると辞書でも比較的余裕が出る [[Haskell の IntMap vs HashMap]] に詳しく記載しました。 ## 感想など 前回同様、水色パフォーマンスにもう少しで手が届くかとおもったところで、逃しました 😂 が、やはり序盤の問題の実装の安定感が出てきていてそこは成長を感じます。 ここからはそのあと一歩をどう詰められるかの勝負になっていくと思います。 引き続きがんばります。 ---- # おまけ Haskell 精進記録 あとで追記するかも