Haskellでプログラミングコンテスト チャレンジブック part2:「Lake Counting」

Haskellで蟻本記事第二弾。


Haskellでプロコンの問題に取り組もうと思ったときにまず難しいと思ったのは、基本的にリストでデータの集合を扱うHaskellでは二次元データへのO(1)でのアクセスをやろうとすると面倒なこと。
次に、そういったデータの集合を更新しながら進むアルゴリズムがやるづらいだろうということ。破壊的代入ができないので。


と、いうことでこの問題"Lake Counting"。
POJの問題で問題Noは2386。


「庭」の大きさを表す整数N,M(N:縦、M:横)が一行で与えられ、続けて以下のような「庭」のデータが与えられる。「w」は「水たまり」を表し、周囲8マスで隣接した水たまりはつながっているとみなす。


...w..............wwww........
..www............w...w........
..www.............w..w........
...w.............w...w........
................w....w........
.....wwwwww......w...w........
....ww............wwww........
...w..........................
..w..wwwwwwwwwww.....ww.......
.w...wwwwwwwwwww....wwww.....w
w....wwwwwwwwwww.....ww.....ww
w....wwwwwwwwwww..........wwww
w........................wwwww
w.....w.w.w.w..........wwwwwww
w....................wwwwwwwww


水たまりがいくつあるかを出力せよ。(上の例では10)

データサイズ
N,M≦100

解法は、データを頭から見ていって「w」に行き着いたら深さ優先探索でそこに隣接する「w」をすべて「庭」から取り除いていき、先頭のループで何回DFSを呼び出したかをカウントするだけ。

手続き型言語なら二次元データ&配列の破壊的更新でDFSを書くことになりますが、Haskellでも同じようなことができるか実験。

Haskellによるコードは以下。

import Array

getLines :: Int -> IO [String]
getLines n = if n > 0 then
                 do l <- getLine
                    ls <- getLines (n-1)
                    return (l:ls)
             else
                 return []

getIntLine :: IO Int
getIntLine = do n <- getLine
                return (read n::Int)

putAnsLn :: (Show a) => a -> IO ()
putAnsLn ans = putStrLn (show ans)

matrix :: Int -> Int -> [[a]] -> Array (Int,Int) a
matrix n m lis = array ((0,0),(n-1,m-1)) (matrix_rec 0 lis [])
    where
      matrix_rec _ [] l = l
      matrix_rec i (x:xs) l = matrix_rec (i+1) xs ((zip [(i,j)|j<-[0..m]] x)++l)

{- DFS -}
dfs :: Int -> Int -> Int -> Int -> Array (Int,Int) Char -> Array (Int,Int) Char
dfs x y n m garden_init = dfs_rec (garden_init//[((x,y),'.')]) [(nx,ny)|(nx,ny) <- [(x-1,y-1),(x,y-1),(x+1,y-1),(x-1,y),(x+1,y),(x-1,y+1),(x,y+1),(x+1,y+1)] , nx>=0 , nx<n , ny>=0 , ny<m]
    where
      dfs_rec garden [] = garden
      dfs_rec garden ((nx,ny):cs) = if garden!(nx,ny) == 'w' then
                                        let new_garden = dfs nx ny n m garden
                                        in
                                          dfs_rec new_garden cs
                                    else
                                        dfs_rec garden cs

solve :: Int -> Int -> [String] -> Int
solve n m gardenlist = let garden = matrix n m gardenlist {- 入力を二次元配列に直す -}
                       in
                         solve_rec 0 0 garden 0
                             where
                               solve_rec i j gard ans = if i<n then
                                                            if j<m then
                                                                if gard!(i,j) == 'w' then
                                                                    {- 水たまりが見つかったらDFSでその水たまりを庭から取り除く -}
                                                                    solve_rec i (j+1) (dfs i j n m gard) (ans+1)
                                                                else
                                                                    solve_rec i (j+1) gard ans
                                                            else
                                                                solve_rec (i+1) 0 gard ans
                                                        else
                                                            ans

main = do n <- getIntLine
          m <- getIntLine
          garden <- getLines n
          putAnsLn (solve n m garden)

演算子//」は「ary//[(i,new_data)]」で「配列aryi番目の要素にnew_dataを代入した配列を返す」演算子で、配列の漸進更新ができる。これによって破壊的代入をするようなコードも書ける。(実際の挙動はどうなっているのかわからないが)
追記:どうやら、破壊的代入ではなく配列全体を作り直すような実装になっており、配列の漸進的更新のコストはO(N)となっているようです。あとで改良したコードを載せますが、上のコードの計算量はかなり大きくなってしまっています。)

実際書いてみて思ったのは、やはり二次元配列を作るときですら多少面倒な手順を踏まなくてはならずデータを使用可能な形に落とし込みづらかったのと、2重ループが書きづらかったということ。

getLines 〜 matrixはライブラリ化しておいたほうがいいですね。特にIO周りで手こずるわけには行きませんので。




今夜Codeforcesがありますね。参加しようかなぁ。

それではノシ