Google Code Jam 2011 予選問題を解いてみた
ようやくGCJ2011の予選の問題を全てHaskellで解けました。
解いた順番に書きます。
C. ビット数
与えられた数字を2つの2進数の和で表す時に、1の数を最大化する問題。
唯一自力で解けた問題です。与えられた数字を2進数に直し、下の位から見ていき、一意的に解が求められます。
問題自身にはあまり関係ないですが、Debug.Traceモジュールのtrace関数を使って
func arg1 arg2 ... = body
のような関数をprintデバッグしたいときには、
func arg1 arg2 ... = trace (出力内容) (func2 arg1 arg2 ...) func2 arg1 arg2 ... = body
という風にやると簡単に出来ます。
i2B n c | n > t = i2B n (c + 1) | n == t = 1 : int2Bit (n - t) [2^x | x <- [0..]] (c - 1) | otherwise = int2Bit n [2^x | x <- [0..]] (c - 1) where t = 2^c int2Bit n bit c | c < 0 = [] | n >= bit !! c = 1 : int2Bit (n - bit !! c) bit (c - 1) | otherwise = 0 : int2Bit n bit (c - 1) solve ar n carry | n == 0 = 1 - carry | ar !! n == 0 = 2 - carry + solve ar (n - 1) 1 | otherwise = 1 + carry + solve ar (n - 1) carry ans ar = solve ar ((length ar) - 1) 0 doIt c n | n > 0 = do l <- getLine putStrLn $ "Case #" ++ show c ++ ": " ++ (show $ ans (i2B (read l) 0) ) doIt (c + 1) $ n - 1 | otherwise = return() main = do l <- getLine doIt 1 (read l)
A. カードシャッフル
カードシャッフルを数回行った際に上からw番目のカードの始めの位置を求める問題。
Largeではカード枚数が10億枚もあるので全てのカードの位置を覚えるのは無理。どうするのか全く分かりませんでしたが、解説によると最後から逆に考えていけば良いとのこと。
リストを作る際に1 : [2, 3]で[1, 2, 3]のリストを作れますが、[2, 3] : 4で[2, 3, 4]のリストは作れないようです。
あと、getLineを使う関数では戻り値を整数とかには出来ないようです。IO関係を使った関数はIO以外を戻り値には出来ないみたいですね。
import Control.Applicative getArgs str = map (read :: String -> Int) (words str) makeA str array = ((ar !! 0) , (ar !! 1)) : array where ar = map read $ words str makeCut c ar w n | 0 < c = do l <- getLine makeCut (c - 1) (makeA l ar) w n | otherwise = doCut ar w 0 n doCut ar pos c n | c >= length ar = putStrLn $ "Case #" ++ show n ++ ": " ++ show pos | pos <= snd (ar !! c) = doCut ar (fst (ar !! c) + pos - 1) d n | pos < fst (ar !! c) + snd (ar !! c) = doCut ar (pos - snd (ar !! c)) d n | otherwise = doCut ar pos d n where d = c + 1 doIt n count | count <= n = do [m, c, w] <- getArgs <$> getLine makeCut c ([] :: [(Int, Int)]) w count doIt n $ count + 1 | otherwise = return() main = do l <- getLine doIt (read l) 1
B. カードシャッフル
消費期限と満足度があるコーヒーが数種類あり、1日に1杯ずつ飲む時に得られる最高の満足度を求める問題。
ものすごく実装が難しかった問題です。これも全く分かりませんでしたが、解説によると「日付を順方向に考えて消費期限が来たらコーヒーが使えなくなる」ではなく、「日付を逆方向に考えて消費期限が来たらコーヒーが使えるようになる」と考えれば良いようです。なるほどなー。
アルゴリズムとしては、event-drivenっぽく「コーヒーが使えるようになる」時と「コーヒーが使えなくなる」時をイベントとして時間を巻き戻していけば良いです。HaskellにはPriorityQueueがないので、Data.Mapを使って実装。
import Data.Map import qualified Data.Map as Map import Data.Maybe getArgs str = Prelude.map (read :: String -> Integer) (words str) getC ary i = fst (snd (ary !! i)) getS ary i = snd (snd (ary !! i)) calcHpy hpy now fut sat = hpy + (now - fut) * sat i2d n = fromRational $ toRational $ fromIntegral n calcNewKey key event | member key event = calcNewKey (key + 0.001) event | otherwise = key multiInsert key value event = insert (calcNewKey key event) value event addCoffee ary event now hpy sat e coffee j satdel satstart count | sat == 0 = if (now - c < 1) then passTime ary event now hpy s newCoffee (-1) now count else passTime ary newEvent now hpy s newCoffee newKey now count | s > sat = if (now - c < 1) then passTime ary (delete satdel event) now hpy s newCoffee (-1) now count else passTime ary (delete satdel newEvent) now hpy s newCoffee newKey now count | s == sat = if (satdel > 0) then let ndel = i2d ((floor satdel) - c) in if(0 < ndel) then passTime ary (insert ndel (1,sat) (delete satdel event)) now hpy s newCoffee ndel satstart count else passTime ary (delete satdel event) now hpy s newCoffee (-1) satstart count else if (now - c < 1) then passTime ary event now hpy s newCoffee (-1) satstart count else passTime ary newEvent now hpy s newCoffee newKey satstart count | otherwise = passTime ary event now hpy sat newCoffee satdel satstart count where newdel = i2d (now - c) newKey = calcNewKey newdel event newEvent = multiInsert newdel (1, s) event s = getS ary j c = getC ary j newCoffee = (insertWith (+) s c coffee) removeCoffee ary event now hpy sat e coffee satdel satstart count | sat /= snd (snd e) = passTime ary event now hpy sat coffee satdel satstart count | Data.Map.size coffee == 1 = passTime ary event now hpy 0 newCoffee (-1) (-1) count | otherwise = let maxCoffee = findMax newCoffee s = fst maxCoffee c = snd maxCoffee newdel = i2d (now - c) newKey = calcNewKey newdel event in if (newdel < 1) then passTime ary event now hpy s newCoffee newKey now count else passTime ary (multiInsert newdel (1, s) event) now hpy s newCoffee newKey now count where newCoffee = delete sat coffee passTime ary event now hpy sat coffee satdel satstart count | Data.Map.null event = if(sat == 0) then putStrLn $ "Case #" ++ show count ++ ": " ++ show hpy else putStrLn $ "Case #" ++ show count ++ ": " ++ show ( hpy + (min (now) (fromJust (Map.lookup sat coffee))) * sat) | otherwise = let i = fst (snd e) e = findMax event next = floor (fst e) in if(sat /= 0) then if (i == 0) then addCoffee ary (deleteMax event) next (calcHpy hpy now next sat) sat e (insertWith (+) sat (next - now) coffee) (fromIntegral (snd (snd e))) satdel satstart count else removeCoffee ary (deleteMax event) next (calcHpy hpy now next sat) sat e (insertWith (+) sat (next - now) coffee) satdel satstart count else if (i == 0) then addCoffee ary (deleteMax event) next hpy sat e coffee (fromIntegral (snd (snd e))) satdel satstart count else removeCoffee ary (deleteMax event) next hpy sat e coffee satdel satstart count makeCoffee ary n event k count | 0 <= n = do [c, t, s] <- fmap getArgs getLine makeCoffee ( (t, (c, s)) : ary) (n - 1) (multiInsert (i2d t) (0, n) event) k count | otherwise = passTime ary event k 0 0 Data.Map.empty (-1) (-1) count doIt m c | c <= m = do [n, k] <- fmap getArgs getLine makeCoffee ([] :: [(Integer, (Integer, Integer))]) (n - 1) Data.Map.empty k c doIt m (c + 1) | otherwise = return() main = do l <- getLine doIt ((read :: String -> Int) l) 1
このソースコードの実装はめちゃくちゃ苦労しました・・・。
まず、Mapでkeyに対応するvalueを返す関数lookupの戻り値がvalueの型でないんですね。何故かMaybeモナドに包まれています。
何で?と思って調べてみると、Maybeモナドというのは、簡単に言うと戻り値がない場合も対応できるモナドだそうです。Mapの中に求めるkeyがない場合があるので、lookupはMaybeモナドに包まれているのでしょうか。
Maybe a型をa型にするには、Data.MaybeモジュールのfromJust関数を使います。
次にHaskellはC++やJavaに比べてまだライブラリが少なく、MultiMap(keyの重複を許すmap)がありません(一応外部提供のMultiMapライブラリはあるみたいです)。仕方ないので自分でMapをMultiMapに改造することにしました。
具体的にはあるkeyでmapに新たに入れる時に、そのkeyが既にmapに含まれていたらkeyに1未満の小さい数εを足してmapに入れます。keyの値を見る時はfloor keyとしてkeyの小数点以下を切り捨てにすることでMultiMapと同じ機能に出来ます。
IntegerをDoubleに変換するにはどうすればよいのか?
これはソースコードのi2d関数のように整数にfromIntegral、toRational、fromRationalの順番に関数を
適用すればよいです(多分もっといい方法がありそうですけど・・・)。
Mapに足すεですが、イベントの数 * ε < 1であればよいです。イベントの数は高々コーヒーの種類 * 2なので、ε < 1 / 200 であればよいはずですが、念のために0.0001にしてみました。しかし、これだとなぜかkeyが10^12のときにMapが上手く動かず止まってしまいました。10^12の前では10^(-4)は無視されてしまうんでしょうか??ε = 0.001にしたら上手く動きました。
しかし、Haskellではこういう複雑なプログラムは型の条件が厳しいので大変です・・・。