AtCoder の新環境テストの問題を解いてみる

Posted on 月 17 2月 2020 in 学習

AtCoder が現在新環境のテストをしている: https://atcoder.jp/contests/language-test-202001

今までの環境では GHC 7.10.3 だったのが,新環境では 8.6.5 になるので,この機会に参加してみるかということで,テストで公開されてる問題を解いてみた.そのコードと解説.なお,当方 AtCoder の Haskell 経験全然無いので,なんか指摘あったらしてくれ.

テンプレ作成

よく知られてる問題として,Haskell で AtCoder の問題解く場合,Prelude を単純に使うと全部 String でデータを持ってきてしまいめっちゃメモリ食って GC のお世話になると言うのがある.他にも AtCoder の問題だと気軽にソートしたい時とかあるけど,リストのソートめっちゃ遅かったり,とにかく Prelude はめっちゃリスト推してくるんだけど,現実的にリストだと辛い場面が多い.なので,ByteStringVector 系の API を整備したくなってくるので,整備した.

整備したのは, https://github.com/mizunashi-mana/haskell-atcoder-template に置いてある.もし,ちゃんと ARC / ABC に参加してたら,不足してるのどんどん足してくかもしれない.

具体的に何整備したかと言うと,まずよく使う言語拡張のオプション追加した:

BangPatterns
bang pattern ! が使えるようになる拡張.これつけるとパターンを一回 seq 挟んでから照合するようになる.これ付けとくと,最適化の解析がちょっと速くなって,情報も増えるので通常より最適か効きやすくなって速くなる場合が多い.ただ,後述する Strict で基本は付くのであまり使わなくて良い.ただ,ネストしたパターンについては,Strict じゃダメな場合があるので,明示的に bang pattern 書く必要がある
BlockArguments
f $ \x -> x とかを f \x -> x って書けるやつ.いらない $ 省けるのでタイプ数が 1 減る
FlexibleContexts
型制約の記法をちょっと柔軟にするやつ.普通 f :: Enum Int => Int とか書けないところを書けるようにしてくれる.元々の制限は型推論で出てきた型にも適用されるので,Vector の API とかでたまにこの拡張が必要になる
LambdaCase
\x -> case x of ...\case ... って書けるやつ.モナディックなやつ書くときに do { r <- m; case r of ... }m >>= \case ... みたいに書ける
MultiWayIf

if cond1 then e1 else if cond2 then e2 else e3

1
2
3
4
if
  | cond1     -> e1
  | cond2     -> e2
  | otherwise -> e3

って書けるやつ.ネストをしないで済みやすい

OverloadedLists
[x :: a, y, z](IsList l, Item l ~ a) => l みたいな型を持つようになるやつ. Vector 気軽に作りたいとき便利
OverloadedStrings
"str"IsString p => p みたいな型を持つようになるやつ. ByteString 気軽に作りたいとき便利
ScopedTypeVariables
トップレベルの型注釈で forall a. a -> a みたいな書き方ができるようになって,型 a を式中で参照できるようになるやつ.後述の TypeApplications と合わせて使うと便利
Strict
全てのパターンの外側に bang pattern ! が付くようになるやつ.bang pattern いちいち付けても同じ恩恵受けられるけど,めんどいし忘れる場合も多いのでこの拡張使うと良い
TypeApplications
x :: forall a b. a -> Int -> b みたいなのに対して,x @Char @() :: Char -> Int -> () みたいに型適用できるやつ. read :: forall a. Read a => String -> a 系の出力が多相的なやつは曖昧な型でエラーになりやすいが,read @Int みたいにしておくとわざわざ注釈書かなくても型を決められる

それから入出力系を整備した.まず, Read に変わるやつで, ReadBS っていうの定義してる.これは単純に ByteString から読み込むやつ.で,それベースに

readLineInputs :: forall a. ReadBS a => IO [a]
一行読み込んで,空白区切りで分けて,それぞれ読み込むやつ
readLineInputsVec :: forall a. ReadBS a => UVec.Unbox a => IO (Vector a)
readLineInputs とやることは同じだけど,返り値がリストじゃなくて unboxed vector
discardLine :: IO ()
一行捨てる
printN :: Show a => a -> IO ()
改行なし print

とかを定義した.後, Vector / MVector をそれぞれ unboxed vector / unboxed mutable vector のエイリアスに設定してたり,Debug 空間を Debug.Trace のエイリアスにしたり,諸々小細工したりしてる.詳細は https://github.com/mizunashi-mana/haskell-atcoder-template/blob/master/src/Header.hs を見てくれ.

で,こいつを CPP#include して使ってる.

Language Test の解答例

で,このテンプレを使った解答例を挙げていく.入出力の概略ぐらいしか問題文は書かないので,AtCoder の問題ページ も参照してくれ.

Welcome To AtCoder

入力
a
b c
s

abc は整数値,s は文字列

出力
a + b + cs を空白区切りで一行に
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include "../src/Header.hs"

main :: IO ()
main = do
  [x] <- readLineInputs @Int
  [y, z] <- readLineInputs @Int
  s <- BS.getLine

  printN $ x + y + z
  putSpace
  BS.putStrLn s

これはいいと思う.type application が無いと,入力 xyzNum a => a ぐらいまでしか決まらなくて,type defaulting が起きる.-Wall 下だと警告が出るので Int を指定してる.

Product

入力
a b

ab は整数値

出力
a * b が奇数なら Odd と,偶数なら Even と出力
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include "../src/Header.hs"

main :: IO ()
main = do
  [x, y] <- readLineInputs

  putStrLn $ solve x y

solve :: Int -> Int -> String
solve x y
  | (x * y) `mod` 2 == 0 = "Even"
  | otherwise            = "Odd"

これも良いと思う.特に言うことはなさそう.

Placing Marbles

入力
abc

abc01

出力
1 の個数
1
2
3
4
5
6
#include "../src/Header.hs"

main :: IO ()
main = do
  s <- Text.getLine
  print $ ocount (== '1') s

ocount :: MonoFoldable mono => (Element mono -> Bool) -> mono -> Intofoldl' で該当する要素を数える関数.微妙になかったので作った.

Shift only

入力
N
a1 ... aN

N は整数で,a1 から aN も整数

出力
最大何回 a1 から aN を 2 で割れるか
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "../src/Header.hs"

main :: IO ()
main = do
  discardLine
  xs <- readLineInputs

  print $ minimum [ checkShifts x | x <- xs ]

-- |
--
-- >>> checkShifts 0
-- 0
-- >>> checkShifts 2
-- 1
-- >>> checkShifts 20
-- 2
--
checkShifts :: Int -> Int
checkShifts = go 0
  where
    go m 0 = m
    go m n
      | n .&. 1 == 1 = m
      | otherwise    = go (m + 1) $ n `shiftR` 1

テンプレでは Data.Bit が読み込まれてて使える.それ使って,それぞれ実際割り切れなくなるまで割ってみて,その中で一番早く割り切れなくなったやつを持ってくる.まあ,アルゴリズム的な最適化の余地は幾つかあるけど,いいでしょ.

Coins

入力
a
b
c
x

abcx は整数

出力
500 が a 個,100 が b 個,50 が c 個ある状況で,ちょうど x になるような組み合わせの数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "../src/Header.hs"

main :: IO ()
main = do
  [a] <- readLineInputs
  [b] <- readLineInputs
  [c] <- readLineInputs
  [x] <- readLineInputs

  print $ solve a b c x

-- |
--
-- >>> solve 1 1 1 0
-- 1
--
solve :: Int -> Int -> Int -> Int -> Int
solve a b c x = length @[] do
  a' <- [0..a]
  b' <- [0..b]
  let x' = x - a' * 500 - b' * 100
  withFilter $ x' >= 0 && x' <= c * 50
  pure ()

withFilter は Scala からの輸入で,リスト内包表記の条件式相当のやつ.基本的には 500 / 100 の範囲で全探索してる.ちゃんとやろうと思えばなんかできそう.OverloadedLists を使ってる弊害で,リストリテラルの表記で,曖昧な型エラーが起きるので,Foldable のインスタンスを type application で固定してる.

Some Sums

入力
n a b

nab は整数

出力
1 以上 n 以下で,10 進法での各桁の和が a 以上 b 以下であるものの総和
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include "../src/Header.hs"

main :: IO ()
main = do
  [n, a, b] <- getLineInputs @Int

  print $ sum [ x | x <- [1..n], let k = sumDigits x, a <= k, k <= b ]

-- |
--
-- >>> sumDigits 11
-- 2
--
sumDigits :: Int -> Int
sumDigits = go 0
  where
    go m 0 = m
    go m n = go (m + n `mod` 10) $ n `div` 10

リスト内包表記も便利.特に length とか sum とかに食わせる場合は融合変換が効くので,気にしないで書いて良さそう.これはまあ,そのまま安直に問題文の指示通りのことをしてる.

Card Game for Two

入力
N
a1 ... aN

Na1 から aN は整数

出力
a1 から aN を大きい順に2人がそれぞれ出していった時の2人の得点差
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "../src/Header.hs"

main :: IO ()
main = do
  discardLine
  xs <- readLineInputsMVec @Int

  r <- solve xs
  print r

-- |
--
-- >>> Vec.unsafeThaw [3, 1] >>= solve
-- 2
-- >>> Vec.unsafeThaw [20, 18, 2, 18] >>= solve
-- 18
--
solve :: MVector Int -> IO Int
solve ys = do
    MVec.sortBy rcompare ys
    ifoldlMVec' go 0 ys
  where
    go m i x = m + if i .&. 1 == 0 then x else negate x

多分この問題は入力めっちゃ少ないのでそうでもないんだけど,リストのソートめっちゃばんばんメモリ使って GC のお世話になって遅くなるイメージなのでソートは基本的に mutable vector でイントロソートする方針にしてる.そのために mutable vector 向けの API もちょっと書いた.ま,そんな感じです.

Kagami Mochi

入力
N
d1
...
dN

Nd1 から dN は整数

出力
d1 から dN を真に小さい順に並べた時の最大の長さ
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include "../src/Header.hs"

main :: IO ()
main = do
  [n] <- readLineInputs @Int
  xs :: MVector Int <- MVec.replicateM n do
    [x] <- readLineInputs
    pure x

  MVec.sort xs
  xs' <- Vec.unsafeFreeze xs
  print $ olength $ Vec.uniq xs'

これもソートしてユニークするだけ.なんか,mutable vector は割と不遇でいろんな API が提供されてない (それは並行並列な世界を考えると当然なんだけど,競プロ的には辛い.もっとゆるふわな API が欲しい).なんで,immutable vector との間を行ったり来たりする必要がある.後,入力部分は毎度おなじみ,曖昧な型を避けるため型指定していけって感じ.

Otoshidama

入力
n y

ny は整数

出力
n 個 10000 / 5000 / 1000 を使って y を作る組み合わせ.なお,作れない時は -1 -1 -1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include "../src/Header.hs"

main :: IO ()
main = do
  [n, y] <- readLineInputs @Int

  case solve n y of
    Nothing           -> putStrLn "-1 -1 -1"
    Just (i1, i2, i3) -> prints [i1, i2, i3]

-- |
--
-- prop> maybe n (\(x, y, z) -> x + y + z) (solve n $ m * 1000) == n
-- >>> solve 9 45000
-- Just ...
-- >>> solve 20 196
-- Nothing
--
solve :: Int -> Int -> Maybe (Int, Int, Int)
solve n y = headMay @[_] do
  i1 <- [0..n]
  let n2 = n - i1
  i2 <- [0..n2]
  let i3 = n2 - i2
  let y' = y - i1 * 10000 - i2 * 5000
  withFilter $ y' == i3 * 1000
  pure (i1, i2, i3)

Coins と同じく安直にやってる.所詮,200022000^2 やしいけるやろ.今回も曖昧な型回避のため type application してる.type application は特例で partial signature 使えて警告も出ないようになってる.便利.

白昼夢

入力
s

s は文字列

出力
s(dream|dreamer|erase|eraser)* にマッチするか判定し,マッチするなら YES,しないなら NO
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "../src/Header.hs"

main :: IO ()
main = do
  s <- BS.getLine

  putStrLn case solve s of
    True  -> "YES"
    False -> "NO"

-- |
--
-- >>> solve "erasedream"
-- True
--
solve :: ByteString -> Bool
solve s = case parseOnlyEof p $ BS.reverse s of
    Left{}  -> False
    Right{} -> True
  where
    p = Parse.skipMany $ altconcat [ Parse.try $ Parse.string $ BS.reverse w | w <- ws ]

    ws = [ "dream", "dreamer", "erase", "eraser" ]

接頭が一致してる単語があるので,単純にパーサ書くと早食いしてしまってうまく解けないやつ.でも逆側だと曖昧さがなくなるので逆から書いたがこれは想定解なんだろうか? ところでこの問題,正規表現があれば一発で解決するんだけど,どうやらライブラリリストから抜けてるっぽい.これは投げとくべきだったなと反省してる.今からでも間に合うんやろか? とりあえず質問だけは投げておいた.

Traveling

入力
N
t1 x1 y1
...
tN xN yN

Nt1 から tNx1 から xNy1 から yN は整数

出力
時刻 t1 から tN でその地点にいれるか判定し,可能なら Yes,できないなら No
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "../src/Header.hs"

main :: IO ()
main = do
  [n] <- readLineInputs
  solve n

solve :: Int -> IO ()
solve m = go m 0 0 0
  where
    go :: Int -> Int -> Int -> Int -> IO ()
    go n t x y
      | n == 0    = putStrLn "Yes"
      | otherwise = do
        [t', x', y'] <- readLineInputs
        let d = abs (x - x') + abs (y - y')
        let td = t' - t
        case td >= d && (td - d) `mod` 2 == 0 of
          True  -> go (n - 1) t' x' y'
          False -> do
            replicateM_ (n - 1) discardLine
            putStrLn "No"

純粋性をかなぐり捨てて書いた.まあ,これはいいでしょ.次行ってみよう.

Interactive Sorting

入出力
n q

(n, q) = (26, 1000), (26, 100), (5, 7) が最初に提示される

q 回大文字アルファベット c1c2 について

? c1 c2

を出力でき,その度に

r

r = <, > が返ってくる.最後に問合せ結果から分かる n 文字のアルファベットをソートした文字列 s について

! s

を出力する.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#include "../src/Header.hs"

main :: IO ()
main = do
  [n, _] <- readLineInputs
  solve n $ Vec.iterateN n succ 'A'

solve :: Int -> Vector Char -> IO ()
solve n v = do
    xs1 <- Vec.unsafeThaw v
    case n of
      5  -> go1 xs1
      26 -> go2 xs1
      _  -> error $ "unexpected input:" ++ show n
    xs2 <- Vec.unsafeFreeze xs1

    putStr "! "
    oforM_ xs2 putChar
    putEndLine
  where
    go1 xs = do
      askSwap xs 0 1
      askSwap xs 2 3
      ask xs 0 2 >>= \case
        True  -> pure ()
        False -> do
          MVec.swap xs 0 2
          MVec.swap xs 1 3
      --   2 - 3
      --  /
      -- 0 - 1

      ask xs 2 4 >>= \case
        --     4
        --    /
        --   2 - 3
        --  /
        -- 0 - 1
        True  -> askSwap xs 3 4
        -- 4
        --  \
        --   2 - 3
        --  /
        -- 0 - 1
        False -> do
          askSwap xs 0 4
          MVec.swap xs 4 2
          MVec.swap xs 4 3
      -- 0 - 2 - 3 - 4
      --  \
      --   1


      ask xs 1 3 >>= \case
        -- 0 - 2 - 3 - 4
        --  \     /
        --   1 --
        True  -> askSwap xs 1 2
        -- 0 - 2 - 3 - 4
        --          \
        --           1
        False -> do
          askSwap xs 1 4
          MVec.swap xs 3 1
          MVec.swap xs 2 1

    go2 xs = sortM askChar xs

    askSwap xs i1 i2 = ask xs i1 i2 >>= \case
      True  -> pure ()
      False -> MVec.swap xs i1 i2

    ask xs i1 i2 = do
      c1 <- MVec.read xs i1
      c2 <- MVec.read xs i2
      askChar c1 c2

    askChar c1 c2 = do
      putStrLn ['?', ' ', c1, ' ', c2]
      putFlush
      [c] <- getLine
      pure case c of
        '<' -> True
        '>' -> False
        _   -> error $ "unexpected input: " ++ [c]

-- |
--
-- >>> xs <- mvecFromList ['B','A','E','F','D','C']
-- >>> sortM (\x y -> pure $ x < y) xs
-- >>> mvecToList xs
-- "ABCDEF"
--
sortM :: (Char -> Char -> IO Bool) -> MVector Char -> IO ()
sortM cmp v = do
    v2 <- MVec.clone v
    go 0 (MVec.length v) v2 v
  where
    go i l xs ys = if
      | l == 1    -> pure ()
      | otherwise -> do
        let i1 = i
            l1 = l `div` 2
        go i1 l1 ys xs
        let i2 = i + l1
            l2 = l - l1
        go i2 l2 ys xs

        merge i1 i1 l1 i2 l2 xs ys

    merge i i1 l1 i2 l2 xs ys = if
      | l1 == 0 -> do
        let xs2 = MVec.slice i2 l2 xs
        let ys2 = MVec.slice i l2 ys
        MVec.copy ys2 xs2
      | l2 == 0 -> do
        let xs1 = MVec.slice i1 l1 xs
        let ys1 = MVec.slice i l1 ys
        MVec.copy ys1 xs1
      | otherwise -> do
        c1 <- MVec.read xs i1
        c2 <- MVec.read xs i2
        cmp c1 c2 >>= \case
          True -> do
            MVec.write ys i c1
            merge (i + 1) (i1 + 1) (l1 - 1) i2 l2 xs ys
          False -> do
            MVec.write ys i c2
            merge (i + 1) i1 l1 (i2 + 1) (l2 - 1) xs ys

率直に言ってめんどくさい.最初に n に関する場合分けをして,n = 5 の時は最適なソートを,それ以外の時はマージソートを行う.

n = 5 の時は,decision tree の葉の数が今回は 5! = 120 必要で,2分木の葉の数の限界値は 2h2^h なので少なくとも h7h \geq 7 じゃないといけない.ここから最大 77 回比較がこのソートの最適解だと分かる.逆に言えば decision tree において,選択の幅を狭めるような早めに葉に到達してしまう選択をなるべくしない比較を考えないといけない.で,まあ結果はコードの通り.なるべく対称性を保つような比較を行っていき,徐々に要素の順序を確定させていく.

マージソートの方はそのまんまって感じ.2つの mutable vector 使ってそれぞれの再帰ステップで交互に役割を交代させながらソートしてく.ソートの際 IO が必要で vector-algorithms が使えなかった.これは,テンプレにあったほうがいいんか?

モンスターテイマー

問題文を読み解くのがめんどくさかったので,解いてない.

まとめ

というわけで AtCoder 用のテンプレ作ったので,良かったら利用してください.僕もやっていきたい (やっていくとは言ってない).DP 関連のサポートがちょっと薄いので,その辺おいおいやりながら整備していきたいねって感じ.

GHC 8.6.5 入ったら,Strict 拡張あるので,全部 unboxed literal と unboxed type 使って書く必要もなさそう.だいぶコーディング体験改善されそうで嬉しいっすね.こちらからは以上です.