Graphics Programming

[ALGOSPOT] NQUEEN - Zipper의 활용 본문

Season 1/하스켈

[ALGOSPOT] NQUEEN - Zipper의 활용

minseoklee 2015. 4. 13. 07:46

문제: https://algospot.com/judge/problem/read/NQUEEN


N의 범위가 1≤N≤12인 간단한 문제...인데 그냥 백트래킹으로 짜니까 시간 초과 ㅜㅜ


일단 n-queen 알고리즘과 무관한, 문제 입력을 처리하는 코드부터 작성한다.


import Control.Monad


main = do

n <- read `fmap` getLine :: IO Int

forM_ [1..n] $ const $ do

x <- read `fmap` getLine :: IO Int

print $ nqueen x


이제 N을 입력받아 답의 개수를 반환하는 nqueen 함수를 작성하면 된다.


NN개의 칸에 N개의 퀸을 놓는 모든 경우의 수를 따지는 건 너무 어리석은 짓이고.. 각 행에 퀸이 하나씩만 들어갈 수 있으므로 각 행의 모든 열에 놓아보는 방법을 쓴다. n-queen의 가장 기초적인 해법이다.


import Data.List

import Data.Array

empty n = array (1,n) [(i,0) | i<-[1..n]]

nqueen n = fill (empty n) 1 1 0 where

fill board row col cnt = case () of

()  | row > n -> fill (board // [(n,0)]) (n-1) (board!(n-1) + 1) (cnt+1)

| col > n -> if row==1 then cnt else fill (board // [(row-1, 0)]) (row-1) ((board!(row-1)) + 1) cnt

| peace -> fill (board // [(row,col)]) (row+1) 1 cnt

| True -> (fill board row (col+1) cnt)

where

peace = (row==1) || False == (foldl1' (||) [(board!i) `elem` [col,col+i-row,col+row-i] | i <- [1..(row-1)]])

정말 이 혼돈의 끝은 어디일까


사실은 (http://en.wikibooks.org/wiki/Algorithm_Implementation/Miscellaneous/N-Queens)의 C++ 코드와 똑같은 로직이다. 이쪽이 훨씬 알아보기 쉬울 것이다.


하지만 위의 nqueen 함수는 치명적인 문제가 있는데, Array 모듈을 사용한다는 것이다. 하스켈의 배열은 읽기는 O(1)이지만 쓰기는 O(N)이라는 흉악한 성능을 자랑하는데 분기 4개 중 3개에서 쓰기를 수행한다. N = 12면 시간 초과를 피할 수 없다.


하스켈도 mutable array를 모나드 형태로 지원하기 때문에 관련 자료를 찾아볼까 했지만 마침 지퍼(Zipper)가 생각났다. 거창하게 지퍼라 할 것도 없이, (지금까지 놓은 퀸의 리스트 + 현재 퀸)을 보관하면 된다. 이 때 리스트는 역순으로 저장되어, 바로 전에 놓은 퀸이 리스트의 맨 앞에 온다. 백트랙을 할 때 무조건 바로 위의 행으로 돌아가기 때문에 바로 리스트의 헤드를 취하면 된다.


import Data.List

data Zipper = Zipper [Int] Int

emptyZipper = Zipper [] 1

zqueen n = zfill emptyZipper 1 0 where

zfill (Zipper [] col) row cnt = if col > n then cnt else zfill (Zipper [col] 1) (row+1) cnt

zfill (Zipper prev@(prevh:prevt) col) row cnt = case () of

()  | row > n -> zfill (Zipper prevt (prevh+1)) n (cnt+1)

| col > n -> if row==1 then cnt else zfill (Zipper prevt (prevh+1)) (row-1) cnt

| peace -> zfill (Zipper (col:prev) 1) (row+1) cnt

| True -> zfill (Zipper prev (col+1)) row cnt

where

peace = diag prev 1 col

diag [] _ _ = True

diag (c:cs) i col = if c==col || c-col==i || col-c==i then False else diag cs (i+1) col


Data.Array 모듈을 완전히 제거했다. 컴파일한 상태에서 속도 측정하는 방법은 아직 모르겠고 ghci에서 비교해보니 N = 12일 때 zqueen이 nqueen보다 약 4배 빨랐다. 저지 시스템은 실행시간 501ms으로 통과. 똑같은 로직을 c++로 대충 짜면 5배 빠르다(...)

Comments