AtCoder Beginner Contest 400に参加したので振り返り。
ABの2完。どんどん解けなくなってるぞ。
A - ABC400 Party
\(400\) を \(A\) で割るだけ・・・なのだけど、読み間違えて \(A\) が素数か否か、という問題を解こうとしてしまってた。 焦ってはいけないね。
main :: IO ()
= do
main <- readLn @Int
n
print $ if 400 `mod` n == 0 then 400 `div` n else -1
B - Sum of Geometric Series
式 \[ X = \sum_{i=0}^M N^i \]
の通りに \(i=0\) から順繰りに足していくだけ。
main :: IO ()
= do
main <- list1 ucInt
[n,m]
putStrLn $ solve n m
solve :: Int -> Int -> String
= go 0 0
solve n m where go i acc
| acc > 10^9 = "inf"
| i > m = show acc
| otherwise = go (i+1) (acc + n^i)
これも dropWhile
や scanl
を使って書けそう。
C - 2^a b^2
本番中は解法思いつかず、15分ほど考えてD問題に行った。
\[\begin{eqnarray} &&X = 2^a \times b^2 \\ &\Leftrightarrow & b^2 = \frac{X}{2^a} \\ &\Leftrightarrow & b = \sqrt{\frac{X}{2^a}} \end{eqnarray} \]なので、 \(2^a \le N\) なる \(a\) それぞれについて、 \(b\) の最大値 \(b_{max}(a)=\lfloor\sqrt{N/{2^a}}\rfloor\) を求める。 各 \(a\) について、 \(1 \le b \le b_{max}(a)\) なる \((a,b)\) の組み合わせは良い整数を構成するが、ここで \(b\) が偶数( \(=2b'\) )のとき、
\[\begin{equation} X = 2^a \times b^2 = 2^a \times (2b')^2 = 2^{a+2} \times {b'}^2 \end{equation} \]となるので、奇数である \(b\) の個数のみ数えれば良い。
実装は以下の通り。
main :: IO ()
= do
main <- readLn @Integer
n
let as = takeWhile (\x -> 2^x <= n) [1..]
= map (isqrt . \a -> n `div` 2^a) as
bs = map (\b -> (b + 1) `div` 2) bs
bs'
print $ sum bs'
isqrt :: Integral a => a -> a
= bsearch (\i -> i^2 <= x) 0 (x+1) isqrt x
平方根の整数部分を求める関数 isqrt
は、二分探索で実装している。この方法の場合、 Int
ではオーバーフローしてしまうので、 Integer
を使用している。
もともと、 isqrt
の実装は以下のようにしていた。
isqrt :: Integral a => a -> a
= if c1^2 <= x && x < c2^2 then c1 else c2
isqrt x where c0 = sqrt (fromIntegral x)
= floor c0
c1 = ceiling c0 c2
この実装だと、 isqrt (10^18-1)
の結果が 1000000000
となってしまう(正しくは 999999999
)。
これは、 fromIntegral
で Int
を Double
に変換したときに、 Double
の精度が足らず情報が落ちてしまったものと思われる
(実際、 fromIntegral @Integer @Double $ 10^18 - 1
は 1.0e18
と評価される)。
他の人の提出1を見てみると、Newton法で実装しているものもあった。 今後はこちらを使うのが良いかな2。
D - Takahashi the Wall Breaker
問題文や例を眺めつつ、これは01-BFS3で解くんだろうなーと当たりはついた。が、01-BFSを実装するのは初めてで結構苦戦した。 結局サンプルが通らず、時間内に提出できなかった。
01-BFSの自体の実装はこんな感じ( Sq
は Data.Sequence
)。
-- | 01-BFS
bfs01 :: forall a m i. (MArray a Int m, Ix i, Show i) =>
-> [i]) -> -- ^ 現在点からのコストが0の探索候補点を取得
(i -> [i]) -> -- ^ 現在点からのコストが1の探索候補点を取得
(i -> -- ^ 探索範囲のbound
(i, i) -> -- ^ 開始点
[i] Int)
m (a i = do
bfs01 next0 next1 b start -- 開始点からのコストを格納するarrayを作成(maxBoundは訪れていないことを表す)
<- newArray b maxBound
cost -- 開始点にコスト0を設定
$ \s -> writeArray cost s 0
forM_ start -- 探索実施
go cost (Sq.fromList start)-- 開始点からのコストを返却
return cost
where
go :: a i Int -> Sq.Seq i -> m ()
= case Sq.viewl queue of
go dist queue -- 両端キューが空であれば終了
Sq.EmptyL -> return ()
-- 両端キューの左から次の探索点を取り出す
Sq.:< rest -> do
v -- 開始点から探索点までのコストを取得
<- readArray dist v
d -- コストが0の探索候補点を取得
<- filterM (fmap (>d) . readArray dist) . next0 $ v
cand0 -- コスト1の探索候補点を取得
<- filterM (fmap (>d+1) . readArray dist) . next1 $ v
cand1 -- コストを更新
$ \cand -> writeArray dist cand d
forM_ cand0 $ \cand -> writeArray dist cand (d+1)
forM_ cand1 -- コスト0の探索候補点を両端キューの左、コスト1の探索候補点を両端キューの右に追加し、次の探索へ
$ Sq.fromList cand0 Sq.>< rest Sq.>< Sq.fromList cand1 go dist
BFSとはいうものの、最短距離ではなくコストを更新しているので、Dijkstra法に近いことをしているのかな。
今回、「前蹴り」で壁の区画を道の区画に変更できる。このコストを1とすれば、探索候補点を取得する関数は以下。
next0 :: Ix i => i -> [i]
= filter (\v -> grid ! v /= '#') . arounds bound nei1
next0
next1 :: Ix i => i -> [i]
= filter (\v -> grid ! v == '#') . arounds bound nei2
next1
-- | 範囲外の点は除外して、ある点に隣接する点を列挙する。
arounds :: Ix i => (i, i) -> (i -> [i]) -> i -> [i]
= filter (bound`inRange`) . neighbours
arounds bound neighbours
-- | (i, j) から上下左右4つの位置を列挙する
nei1 :: (Ix i, Num i) => (i, i) -> [(i, i)]
= [bimap (i+) (j+) (p,q) | p <- [-1,0,1], q <- [-1,0,1], p==0||q==0, (p,q) /= (0,0)]
nei1 (i, j)
-- | (i, j) から上下左右毎に2個先までの位置を列挙する
nei2 :: (Ix i, Num i, Enum i) => (i, i) -> [(i, i)]
= [bimap (i+) (j+) (p,q) | p <- [-2..2], q <- [-2..2], p==0||q==0, (p,q) /= (0,0) ] nei2 (i, j)
当初、コスト1の探索候補点を取得する処理を、「壁の向こうの道を探す」と考えて、距離が2か3のマンハッタン距離にある点で、道であるようなものを求めるよう実装していた。
= filter (\v -> grid ! v /= '#') . arounds bound nei2
next1 = [bimap (i+) (j+) (p,q) | p <- [-3..3], q <- [-3..3], abs p + abs q `elem` [2,3]] nei2 (i, j)
これだと、Sampleは通るが、Allは通らない。 壁が3つ以上連続している場合に、それ以上探索できなくなってしまうため。
実際、通る場合と通らない場合で、探索後の cost
配列を出力すると以下のようになっている。
サンプル問題4の入力
#################### ##...##....###...### #.....#.....#.....## #..#..#..#..#..#..## #..#..#....##..##### #.....#.....#..##### #.....#..#..#..#..## #..#..#.....#.....## #..#..#....###...### #################### #################### ##..#..##...###...## ##..#..#.....#.....# ##..#..#..#..#..#..# ##..#..#..#..#..#..# ##.....#..#..#..#..# ###....#..#..#..#..# #####..#.....#.....# #####..##...###...## ####################
正しい回答の
cost
配列21111122222233333344 11000111111223222334 10000011111122222233 10010011121122232233 10010011111222233344 10000011111122233344 10000011121122232233 10010011111122222233 10010011111223222334 21121122222233333344 21121122222334333445 22112112222233433344 22112112222223333334 22112112223223334334 22112112223223334334 22111112223223334334 32211112223223334334 33222112222223333334 43322112222233433344 44333223333334444445
誤った回答の
cost
配列(探索していない区画は.
と表示).................... ..000..1111...222... .00000.11111.22222.. .00.00.11.11.22.22.. .00.00.1111..22..... .00000.11111.22..... .00000.11.11.22.22.. .00.00.11111.22222.. .00.00.1111...222... .................... .................... ..11.11..222...333.. ..11.11.22222.33333. ..11.11.22.22.33.33. ..11.11.22.22.33.33. ..11111.22.22.33.33. ...1111.22.22.33.33. .....11.22222.33333. .....11..222...333.. ....................
感想
今日はコンテスト中にちょこちょこ中断(30分くらいかな)が入って、なかなか集中して取り組めなかった・・・ とはいえ、(解答は見たものの)よく考えればできたなーという問題だったので、やはり実力不足。 (D問題も、解いてみればド典型といった問題で、探索候補点をもう少し考察できていれば時間内に解けてたという感触)
また Double
の精度不足でWAとなったものについて、結構気軽に fromIntegral
や ceiling
、 floor
等々で整数と浮動小数点数の変換をしてたのは反省。
できるだけ整数のまま計算を進めるようにしたい。
探索問題については、DFSやBFSといった実装は用意しているが、いざ使おうとするとまごついてしまう。 さらっと実装できるくらいにしておきたい。。。
https://en.wikipedia.org/wiki/Integer_square_root#Using_only_integer_division↩︎
https://betrue12.hateblo.jp/entry/2018/12/08/000020 こちらのブログの01-BFS(及び幅優先探索、Dijkstra法)の説明はわかりやすかった。↩︎