Haskell шляхи вирішення проблеми 3n + 1


12

Ось проста проблема програмування від SPOJ: http://www.spoj.com/problems/PROBTRES/ .

В основному, вас просять вивести найбільший цикл Колатца для чисел між i і j. (Цикл Колатца з числа $ n $ - це кількість кроків, щоб в кінцевому підсумку отримати від $ n $ до 1.)

Я шукав спосіб Haskell вирішити проблему з порівняльною продуктивністю, ніж у Java або C ++ (так, щоб вписатись у дозволений ліміт часу виконання). Хоча просте рішення Java, яке запам'ятовує довжину циклу будь-яких уже обчислених циклів, буде працювати, я не мав успіху в застосуванні ідеї отримати рішення Haskell.

Я спробував Data.Function.Memoize, а також техніку запам’ятовування домашнього часу, використовуючи ідею з цього посту: /programming/3208258/memoization-in-haskell . На жаль, запам'ятовування насправді робить обчислення циклу (n) ще повільнішим. Я вважаю, що уповільнення відбувається від верхніх шляхів Haskell. (Я спробував запустити компільований двійковий код, а не інтерпретувати.)

Я також підозрюю, що просто повторення чисел від i до j може бути дорогим ($ i, j \ le10 ^ 6 $). Тому я навіть спробував попередньо обчислити все для запиту діапазону, використовуючи ідею з http://blog.openendings.net/2013/10/range-trees-and-profiling-in-haskell.html . Однак це все-таки призводить до помилки "Перевищення обмежень у часі".

Чи можете ви допомогти повідомити про це акуратну конкурентну програму Haskell?


10
Цей пост мені здається прекрасним. Це алгоритмічна проблема, яка потребує належного проектування для досягнення належної продуктивності. Чого ми насправді не хочемо тут, - це питання "як виправити свій зламаний код"?
Роберт Харві

Відповіді:


7

Я відповім у Scala, тому що мій Haskell не такий свіжий, і тому люди повірять, що це загальне питання функціонального алгоритму програмування. Я буду дотримуватися структури даних та понять, які легко передаються.

Ми можемо почати з функції, яка генерує послідовність колацу, яка є досить простою, за винятком того, що потрібно передавати результат як аргумент, щоб зробити хвіст рекурсивним:

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)

Тепер ми маємо відносно ефективні реалізації фрагментів, нам потрібно знайти спосіб подати результати попереднього обчислення на вхід наступного обчислення. Це називається a 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
}

У моїй REPL для діапазонів розміром 1000 або більше, як у прикладі введення, відповідь повертається майже миттєво.


3

Карл Білефельд уже добре відповів на питання, я просто додам версію 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''(sidenote: є два маркери синтаксису haskell, але один некрасивий, інший заплутається символом простих. Це єдина причина подвійного прайму.), Ми просто додамо нове значення. Але зараз стає цікаво. Якщо ми не знайдемо значення, нам доведеться як обчислити його, так і виконати оновлення. Але ми вже маємо функції для обох! Так

                                | 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слід використовувати.
Олате
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.