3n + 1 문제에 대한 Haskell 방법


12

다음은 SPOJ에서 간단한 프로그래밍 문제 http://www.spoj.com/problems/PROBTRES/는 .

기본적으로 i와 j 사이의 숫자에 대해 가장 큰 Collatz주기를 출력하라는 메시지가 표시됩니다. ($ n $의 콜라 츠주기는 결국 $ n $에서 1까지의 단계 수입니다.)

Java 또는 C ++보다 비교 성능 문제를 해결하는 Haskell 방법을 찾고 있습니다 (허용 된 런타임 제한에 맞도록). 이미 계산 된주기의주기 길이를 기억하는 간단한 Java 솔루션이 작동하지만 Haskell 솔루션을 얻기위한 아이디어를 성공적으로 적용하지 못했습니다.

/programming/3208258/memoization-in-haskell 에서 아이디어를 사용하여 Data.Function.Memoize와 자체 제작 로그 시간 메모 기술을 시도했습니다 . 불행하게도, 메모 화는 실제로 cycle (n)의 계산을 더 느리게 만듭니다. 나는 둔화가 Haskell 방식의 오버 헤드에서 비롯된 것이라고 생각합니다. (나는 해석하는 대신 컴파일 된 이진 코드로 실행을 시도했습니다.)

또한 i에서 j까지의 숫자를 단순히 반복하는 데 비용이 많이 든다고 생각합니다 ($ i, j \ le10 ^ 6 $). 그래서 http://blog.openendings.net/2013/10/range-trees-and-profiling-in-haskell.html의 아이디어를 사용하여 범위 쿼리에 대한 모든 것을 미리 계산하려고했습니다 . 그러나 여전히 "시간 제한 초과"오류가 발생합니다.

이를 위해 깔끔한 경쟁 하스켈 프로그램을 알리는 데 도움을 줄 수 있습니까?


10
이 게시물은 나에게 잘 보인다. 적절한 성능을 달성하려면 적절한 디자인이 필요한 알고리즘 문제입니다. 여기서 실제로 원하지 않는 것은 "깨진 코드를 수정하는 방법"질문입니다.
Robert Harvey

답변:


7

나는 Haskell이 신선하지 않기 때문에 Scala로 대답 할 것이므로 사람들은 이것이 일반적인 기능 프로그래밍 알고리즘 질문이라고 생각할 것입니다. 쉽게 전송할 수있는 데이터 구조와 개념을 고수하겠습니다.

collatz 시퀀스를 생성하는 함수로 시작할 수 있는데, 결과를 꼬리 재귀로 만들기 위해 인수로 결과를 전달할 필요가있는 경우를 제외하고는 비교적 간단합니다.

def collatz(n: Int, result: List[Int] = List()): List[Int] = {
   if (n == 1) {
     1 :: result
   } else if ((n & 1) == 1) {
     collatz(3 * n + 1, n :: result)
   } else {
     collatz(n / 2, n :: result)
   }
 }

이것은 실제로 시퀀스를 역순으로하지만, 다음 단계는 맵에 길이를 저장하는 데 적합합니다.

def calculateLengths(sequence: List[Int], length: Int,
  lengths: Map[Int, Int]): Map[Int, Int] = sequence match {
    case Nil     => lengths
    case x :: xs => calculateLengths(xs, length + 1, lengths + ((x, length)))
}

첫 번째 단계의 답변, 초기 길이 및 빈 맵과 같이 이것을 호출합니다 calculateLengths(collatz(22), 1, Map.empty)). 이것이 결과를 기억하는 방법입니다. 이제 collatz이것을 사용할 수 있도록 수정해야합니다 :

def collatz(n: Int, lengths: Map[Int, Int], result: List[Int] = List()): (List[Int], Int) = {
  if (lengths contains n) {
     (result, lengths(n))
  } else if ((n & 1) == 1) {
    collatz(3 * n + 1, lengths, n :: result)
  } else {
    collatz(n / 2, lengths, n :: result)
  }
}

를 사용 n == 1하여지도를 초기화 할 수 있기 때문에 검사를 제거 1 -> 1하지만 1내부에지도에 넣은 길이 를 추가해야합니다 calculateLengths. 또한 되풀이가 중지 된 메모리 길이를 반환합니다. 초기화하는 데 사용할 수 있습니다 calculateLengths.

val initialMap = Map(1 -> 1)
val (result, length) = collatz(22, initialMap)
val newMap = calculateLengths(result, lengths, initialMap)

이제 조각을 비교적 효율적으로 구현 했으므로 이전 계산의 결과를 다음 계산의 입력에 공급하는 방법을 찾아야합니다. 이것을이라고 fold하며 다음과 같습니다.

def iteration(lengths: Map[Int, Int], n: Int): Map[Int, Int] = {
  val (result, length) = collatz(n, lengths)
  calculateLengths(result, length, lengths)
}

val lengths = (1 to 10).foldLeft(Map(1 -> 1))(iteration)

실제 답변을 찾으려면 주어진 범위 사이의 맵에서 키를 필터링하고 최대 값을 찾아 최종 결과를 얻습니다.

def answer(start: Int, finish: Int): Int = {
  val lengths = (start to finish).foldLeft(Map(1 -> 1))(iteration)
  lengths.filterKeys(x => x >= start && x <= finish).values.max
}

예제 입력과 같이 크기가 1000 정도 인 REPL에서 대답은 거의 즉시 반환됩니다.


3

Karl Bielefeld는 이미 그 질문에 잘 대답했습니다. Haskell 버전을 추가하겠습니다.

효율적인 재귀를 보여주는 기본 알고리즘의 단순하고 메모리 화되지 않은 버전입니다.

simpleCollatz :: Int -> Int -> Int
simpleCollatz count 1 = count + 1
simpleCollatz count n | odd n     = simpleCollatz (count + 1) (3 * n + 1)
                      | otherwise = simpleCollatz (count + 1) (n `div` 2)

그것은 거의 스스로 설명해야합니다.

나도 Map결과를 저장하기 위해 간단한 것을 사용할 것 입니다.

-- double imports to make the namespace pretty
import           Data.Map  ( Map )
import qualified Data.Map as Map

-- a new name for the memoizer
type Store = Map Int Int

우리는 항상 매장에서 최종 결과를 조회 할 수 있으므로 단일 값의 경우 서명은

memoCollatz :: Int -> Store -> Store

최종 사례부터 시작하겠습니다

memoCollatz 1 store = Map.insert 1 1 store

예, 미리 추가 할 수 있지만 상관 없습니다. 다음 간단한 사건주세요.

memoCollatz n store | Just _ <- Map.lookup n store = store

값이 있으면 그 값입니다. 여전히 아무것도하지 않습니다.

                    | odd n     = processNext store (3 * n + 1)
                    | otherwise = processNext store (n `div` 2)

가치가 없다면 무언가 를해야 합니다 . 를 로컬 함수에 넣겠습니다. 이 부분이 어떻게 "간단한"솔루션에 매우 가깝게 보이는지 주목하십시오. 재귀 만 조금 더 복잡합니다.

  where processNext store'' next | Just count <- Map.lookup next store''
                                 = Map.insert n (count + 1) store''

이제 우리는 마침내 무언가를합니다. 계산 된 값을 찾으면 store''(주 : 두 개의 하스켈 구문 형광펜이 있지만 하나는 추악하고 다른 하나는 프라임 기호와 혼동됩니다. 이는 더블 프라임의 유일한 이유입니다). 값. 그러나 이제는 흥미로워집니다. 값을 찾지 못하면 값을 계산하고 업데이트해야합니다. 그러나 우리는 이미 두 기능을 모두 갖추고 있습니다! 그래서

                                | otherwise
                                = processNext (memoCollatz next store'') next

이제 단일 값을 효율적으로 계산할 수 있습니다. 여러 개를 계산하려면 접기를 통해 상점을 전달하십시오.

collatzRange :: Int -> Int -> Store
collatzRange lower higher = foldr memoCollatz Map.empty [lower..higher]

(여기서 1/1 사례를 초기화 할 수 있습니다.)

이제 최대 값을 추출하기 만하면됩니다. 지금은 상점에서 범위의 값보다 높은 값을 가질 수 없으므로 말하기에 충분합니다.

collatzRangeMax :: Int -> Int -> Int
collatzRangeMax lower higher = maximum $ collatzRange lower higher

물론 여러 범위를 계산하고 계산 사이에서 저장소를 공유하려면 (배가 친구입니다) 필터가 필요하지만 여기서는 주요 초점이 아닙니다.


1
속도를 높이려면 Data.IntMap.Strict사용해야합니다.
Olathe
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.