하스켈로 알고리즘 문제 풀기
팁 모음
목 차
1. 입출력
2. 재귀
3. 리스트 모나드
4. 지연 평가와 가비지 컬렉션
5. 문자열 ≠ String
2
4
0 0
0 1
1 0
1 1
3
3 0
2 7
4 8
• 테스트 케이스 개수
• 케이스 1
• 데이터 개수
• 데이터 1 (좌표)
• 데이터 2
• …
• 데이터 N
• 케이스 2
• 데이터 개수
• 데이터 1
• 데이터 2
• …
전형적인 입력 형식
1. 입출력
import Control.Monad
main = do
numCases <- read `fmap` getLine :: IO Int
-- numCases <- readLn :: IO Int
replicateM_ numCases solve
solve = do
…
• replicateM_ 필요
• 정수 읽기 (테스트 케이스 개수 T)
• 또는 이렇게
• 문제 풀이를 T번 실행
• 실제 문제 푸는 코드
1. 입출력
대부분의 코딩은 이렇게 시작
입력 파싱
• 수 하나
• n <- read `fmap` getLine :: IO Int
• x <- read `fmap` getLine :: IO Double
• 띄어쓰기로 구분된 수 목록
• ns <- (map read . words) `fmap` getLine :: IO [Int]
• 줄바꿈으로 구분된 좌표 목록
• n <- read `fmap` getLine :: IO Int
• ps <- replicateM n $ do
[x,y] <- (map read . words) `fmap` getLine :: IO [Int]
return (x,y)
1. 입출력
• 입력: 10
• n = 10
• x = 10.0
• 입력: 1 2 3 4
• ns = [1, 2, 3, 4]
• 입력: 3
0 1
3 3
2 5
• ps = [(0,1),(3,3),(2,5)]
calc xs = f xs acc where
f [] acc = acc
f (x:xs) acc = f xs (acc + g x) where
g x = …
feasible candies = f (length candies) candies 0 where
f _ [] _ = True
f i (x:xs) acc
| (x+acc) `mod` i /= 0 = False
| otherwise = f (i-1) xs (acc + div (acc + x) i)
• 누적 변수(accumulator)가 유용하다
• 코딩이 쉬워진다
• 컴파일러가 최적화하기 좋다
• 실제 예시
2. 재귀
재귀 함수 코딩하기
임의 개수의 결과를 반환하는 비결정적 계산
• 뭔 소리야
• 컴퓨터 과학 시간이 아니다
• 그냥 문제 풀 때는 경우의 수 생성기로 생각
• 격자 생성
• 1 <= x <= 10, 1 <= y <= 10 격자점들
• grid = [ (x,y) | y <- [1..10], x <- [1..10]]
• 모델링하기 좋은 계산
• 초기 리스트에 반복적으로 map 적용, concat으로 연결
• 상태 공간 트리 탐색
3. 리스트 모나드
랭포드 쌍의 생성
• 랭포드 쌍의 정의
• {1,1,2,2,…,n,n}의 모든 두 x에 대해 x와 x 사이에 다른 수가 x개 오는 배치
• {1,1,2,2,3,3} -> 2 3 1 2 1 3
• {1,1,2,2,3,3,4,4} -> 4 1 3 1 2 4 3 2
• 구현 방법
• 빈 배치로 시작
• 가능한 모든 위치에 두 n을 놓는다
• 남은 가능한 모든 위치에 두 n-1을 놓는다
• …
• 남은 가능한 모든 위치에 두 1을 놓는다
• 상태 공간 트리
• 탐색 순서에 따라 백트래킹, 레벨 우선 순회, 가지치기하면 분기 한정(branch and bound) 등…
3. 리스트 모나드
리스트 모나드를 이용한 레벨 우선 순회
langford2 n = f [n,n-1..1] [] where
f :: [Int] -> [(Int,Int)] -> [[(Int,Int)]]
f [] alignment = return alignment
f (x:xs) alignment = do
pos <- possiblePositions x n alignment
let align' = (pos,x) : (pos+x+1,x) : alignment
soln <- f xs align'
return soln
possiblePositions x n alignment = [i | i <- [1..2*n-x-1], notElems (i,i+x+1)] where
notElems (a,b) = all ((i,_) -> i /= a && i /= b) alignment
3. 리스트 모나드
import Data.List
main = do
xs <- words `fmap` getLine :: IO [String]
minimumBy comp (permutations xs)
comp :: String -> String -> Ord
comp x y = …
• minimumBy, permutations 함수
• 문자열 10개의 리스트
• 순열들의 지연 리스트
• minimumBy로 최소 원소 검색
• 그럼 공간 복잡도는 상수겠네?
4. 지연 평가와 가비지 컬렉션
10!개의 순열
공간 복잡도 상수 아닌데요
• 왜요
• 놀랍게도 GHC 탓
• minimumBy는 평가가 끝날 때까지 리스트 전체를 메모리에 붙잡아둔다
• 왜 그러냐고? 저도 모르겠어요
• 즉 이미 비교가 끝난 원소를 가비지 컬렉션하지 못한다
• 길이 10!이고 완전 평가된 리스트 = 메모리 한계 초과
• 최소 원소 구하는 함수를 직접 정의. 가비지 컬렉션 잘됨
least (x:xs) = f x xs where
f x [] = x
f x (y:ys) = case comp x y of
LT -> f x ys
_ -> f y ys
4. 지연 평가와 가비지 컬렉션
문자열 데이터 타입
• 문자열 = String = [Char] 아닌가
• 아닌데요
• 입력이 몇 개 없거나 처리할 양이 적으면 무관
• 문자열 대규모 입출력시 성능이 심각하게 구리다
• String을 썼는데 메모리 or 시간 초과하면
• Text 또는 ByteString 검토
• 일반적인 문자열 처리는 Text
• Ascii 문자만 쓰면 ByteString이 제일 빠르다
5. 문자열 ≠ String
아주 긴 문자열 입력
• String으로 읽은 다음 변환
solve = do
ns <- getLine
let candies = (map read . words) ns :: [Int]
• 그런데 숫자는 최대 10만개, 각 숫자는 최대 10글자
• 띄어쓰기로 구분하면 최대 110만 글자 정도
• 110만 글자 = 1100000 Bytes = 1.1 MB 뭐가 문제?
5. 문자열 ≠ String
String의 메모리상 표현
• 단방향 링크드 리스트
• 문자열의 한 문자당 5 워드 = 40 바이트 (1워드 = 8바이트 가정)
• 110만 글자 = 1.1 * 40 MB = 44 MB
• 잉?
5. 문자열 ≠ String
String 대신 ByteString
• ByteString은 메모리상에서 C의 char[] 배열과 유사하게 표현된다
• String 읽는 코드를 다음과 같이 대체
import Data.Maybe
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS8
solve = do
ns <- BS.getLine
let candies = (map (fst . fromJust . BS8.readInt) . BS8.words) ns
• 실행 속도가 3배 빨라진다
5. 문자열 ≠ String
끝
1. 입출력

하스켈로 알고리즘 문제 풀기

  • 1.
  • 2.
    목 차 1. 입출력 2.재귀 3. 리스트 모나드 4. 지연 평가와 가비지 컬렉션 5. 문자열 ≠ String
  • 3.
    2 4 0 0 0 1 10 1 1 3 3 0 2 7 4 8 • 테스트 케이스 개수 • 케이스 1 • 데이터 개수 • 데이터 1 (좌표) • 데이터 2 • … • 데이터 N • 케이스 2 • 데이터 개수 • 데이터 1 • 데이터 2 • … 전형적인 입력 형식 1. 입출력
  • 4.
    import Control.Monad main =do numCases <- read `fmap` getLine :: IO Int -- numCases <- readLn :: IO Int replicateM_ numCases solve solve = do … • replicateM_ 필요 • 정수 읽기 (테스트 케이스 개수 T) • 또는 이렇게 • 문제 풀이를 T번 실행 • 실제 문제 푸는 코드 1. 입출력 대부분의 코딩은 이렇게 시작
  • 5.
    입력 파싱 • 수하나 • n <- read `fmap` getLine :: IO Int • x <- read `fmap` getLine :: IO Double • 띄어쓰기로 구분된 수 목록 • ns <- (map read . words) `fmap` getLine :: IO [Int] • 줄바꿈으로 구분된 좌표 목록 • n <- read `fmap` getLine :: IO Int • ps <- replicateM n $ do [x,y] <- (map read . words) `fmap` getLine :: IO [Int] return (x,y) 1. 입출력 • 입력: 10 • n = 10 • x = 10.0 • 입력: 1 2 3 4 • ns = [1, 2, 3, 4] • 입력: 3 0 1 3 3 2 5 • ps = [(0,1),(3,3),(2,5)]
  • 6.
    calc xs =f xs acc where f [] acc = acc f (x:xs) acc = f xs (acc + g x) where g x = … feasible candies = f (length candies) candies 0 where f _ [] _ = True f i (x:xs) acc | (x+acc) `mod` i /= 0 = False | otherwise = f (i-1) xs (acc + div (acc + x) i) • 누적 변수(accumulator)가 유용하다 • 코딩이 쉬워진다 • 컴파일러가 최적화하기 좋다 • 실제 예시 2. 재귀 재귀 함수 코딩하기
  • 7.
    임의 개수의 결과를반환하는 비결정적 계산 • 뭔 소리야 • 컴퓨터 과학 시간이 아니다 • 그냥 문제 풀 때는 경우의 수 생성기로 생각 • 격자 생성 • 1 <= x <= 10, 1 <= y <= 10 격자점들 • grid = [ (x,y) | y <- [1..10], x <- [1..10]] • 모델링하기 좋은 계산 • 초기 리스트에 반복적으로 map 적용, concat으로 연결 • 상태 공간 트리 탐색 3. 리스트 모나드
  • 8.
    랭포드 쌍의 생성 •랭포드 쌍의 정의 • {1,1,2,2,…,n,n}의 모든 두 x에 대해 x와 x 사이에 다른 수가 x개 오는 배치 • {1,1,2,2,3,3} -> 2 3 1 2 1 3 • {1,1,2,2,3,3,4,4} -> 4 1 3 1 2 4 3 2 • 구현 방법 • 빈 배치로 시작 • 가능한 모든 위치에 두 n을 놓는다 • 남은 가능한 모든 위치에 두 n-1을 놓는다 • … • 남은 가능한 모든 위치에 두 1을 놓는다 • 상태 공간 트리 • 탐색 순서에 따라 백트래킹, 레벨 우선 순회, 가지치기하면 분기 한정(branch and bound) 등… 3. 리스트 모나드
  • 9.
    리스트 모나드를 이용한레벨 우선 순회 langford2 n = f [n,n-1..1] [] where f :: [Int] -> [(Int,Int)] -> [[(Int,Int)]] f [] alignment = return alignment f (x:xs) alignment = do pos <- possiblePositions x n alignment let align' = (pos,x) : (pos+x+1,x) : alignment soln <- f xs align' return soln possiblePositions x n alignment = [i | i <- [1..2*n-x-1], notElems (i,i+x+1)] where notElems (a,b) = all ((i,_) -> i /= a && i /= b) alignment 3. 리스트 모나드
  • 10.
    import Data.List main =do xs <- words `fmap` getLine :: IO [String] minimumBy comp (permutations xs) comp :: String -> String -> Ord comp x y = … • minimumBy, permutations 함수 • 문자열 10개의 리스트 • 순열들의 지연 리스트 • minimumBy로 최소 원소 검색 • 그럼 공간 복잡도는 상수겠네? 4. 지연 평가와 가비지 컬렉션 10!개의 순열
  • 11.
    공간 복잡도 상수아닌데요 • 왜요 • 놀랍게도 GHC 탓 • minimumBy는 평가가 끝날 때까지 리스트 전체를 메모리에 붙잡아둔다 • 왜 그러냐고? 저도 모르겠어요 • 즉 이미 비교가 끝난 원소를 가비지 컬렉션하지 못한다 • 길이 10!이고 완전 평가된 리스트 = 메모리 한계 초과 • 최소 원소 구하는 함수를 직접 정의. 가비지 컬렉션 잘됨 least (x:xs) = f x xs where f x [] = x f x (y:ys) = case comp x y of LT -> f x ys _ -> f y ys 4. 지연 평가와 가비지 컬렉션
  • 12.
    문자열 데이터 타입 •문자열 = String = [Char] 아닌가 • 아닌데요 • 입력이 몇 개 없거나 처리할 양이 적으면 무관 • 문자열 대규모 입출력시 성능이 심각하게 구리다 • String을 썼는데 메모리 or 시간 초과하면 • Text 또는 ByteString 검토 • 일반적인 문자열 처리는 Text • Ascii 문자만 쓰면 ByteString이 제일 빠르다 5. 문자열 ≠ String
  • 13.
    아주 긴 문자열입력 • String으로 읽은 다음 변환 solve = do ns <- getLine let candies = (map read . words) ns :: [Int] • 그런데 숫자는 최대 10만개, 각 숫자는 최대 10글자 • 띄어쓰기로 구분하면 최대 110만 글자 정도 • 110만 글자 = 1100000 Bytes = 1.1 MB 뭐가 문제? 5. 문자열 ≠ String
  • 14.
    String의 메모리상 표현 •단방향 링크드 리스트 • 문자열의 한 문자당 5 워드 = 40 바이트 (1워드 = 8바이트 가정) • 110만 글자 = 1.1 * 40 MB = 44 MB • 잉? 5. 문자열 ≠ String
  • 15.
    String 대신 ByteString •ByteString은 메모리상에서 C의 char[] 배열과 유사하게 표현된다 • String 읽는 코드를 다음과 같이 대체 import Data.Maybe import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as BS8 solve = do ns <- BS.getLine let candies = (map (fst . fromJust . BS8.readInt) . BS8.words) ns • 실행 속도가 3배 빨라진다 5. 문자열 ≠ String
  • 16.