Cách Haskell cho vấn đề 3n + 1


12

Đây là một vấn đề lập trình đơn giản từ SPOJ: http://www.spoj.com/probols/PROBTRES/ .

Về cơ bản, bạn được yêu cầu xuất chu kỳ Collatz lớn nhất cho các số giữa i và j. (Chu kỳ Collatz của một số $ n $ là số bước cuối cùng nhận được từ $ n $ đến 1.)

Tôi đã tìm kiếm một cách Haskell để giải quyết vấn đề với hiệu năng so sánh hơn so với Java hoặc C ++ (để phù hợp với giới hạn thời gian chạy được phép). Mặc dù một giải pháp Java đơn giản ghi nhớ độ dài chu kỳ của bất kỳ chu trình đã được tính toán nào sẽ hoạt động, tôi đã không thành công khi áp dụng ý tưởng để có được giải pháp Haskell.

Tôi đã thử Data.Feft.Memoize, cũng như kỹ thuật ghi nhớ thời gian đăng nhập tại nhà bằng cách sử dụng ý tưởng từ bài đăng này: /programming/3208258/memoization-in-haskell . Thật không may, ghi nhớ thực sự làm cho việc tính toán chu kỳ (n) thậm chí chậm hơn. Tôi tin rằng sự chậm lại đến từ phía trên của cách Haskell. (Tôi đã thử chạy với mã nhị phân được biên dịch, thay vì diễn giải.)

Tôi cũng nghi ngờ rằng việc lặp lại các số từ i đến j có thể tốn kém ($ i, j \ le10 ^ 6 $). Vì vậy, tôi thậm chí đã thử tính toán trước mọi thứ cho truy vấn phạm vi, sử dụng ý tưởng từ http://blog.openendings.net/2013/10/range-trees-and-profiling-in-haskell.html . Tuy nhiên, điều này vẫn gây ra lỗi "Vượt quá giới hạn thời gian".

Bạn có thể giúp thông báo một chương trình Haskell cạnh tranh gọn gàng cho việc này không?


10
Bài đăng này có vẻ tốt với tôi. Đây là một vấn đề thuật toán cần một thiết kế phù hợp để đạt được hiệu suất đầy đủ. Điều chúng tôi thực sự không muốn ở đây là "làm cách nào để sửa mã bị hỏng".
Robert Harvey

Câu trả lời:


7

Tôi sẽ trả lời bằng Scala, vì Haskell của tôi không còn mới, và vì vậy mọi người sẽ tin rằng đây là một câu hỏi về thuật toán lập trình chức năng chung. Tôi sẽ tuân theo các cấu trúc dữ liệu và khái niệm có thể chuyển nhượng được.

Chúng ta có thể bắt đầu với một hàm tạo ra chuỗi collatz, tương đối đơn giản, ngoại trừ việc cần truyền kết quả dưới dạng đối số để làm cho nó theo đuôi đệ quy:

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)
   }
 }

Điều này thực sự đặt chuỗi theo thứ tự ngược lại, nhưng đó là hoàn hảo cho bước tiếp theo của chúng tôi, đó là lưu trữ độ dài trong bản đồ:

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)))
}

Bạn sẽ gọi đây là câu trả lời từ bước đầu tiên, độ dài ban đầu và một bản đồ trống, như thế nào calculateLengths(collatz(22), 1, Map.empty)). Đây là cách bạn ghi nhớ kết quả. Bây giờ chúng tôi cần sửa đổi collatzđể có thể sử dụng điều này:

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)
  }
}

Chúng tôi loại bỏ n == 1kiểm tra vì chúng tôi chỉ có thể khởi tạo bản đồ với 1 -> 1, nhưng chúng tôi cần thêm 1vào độ dài chúng tôi đặt vào bản đồ bên trong calculateLengths. Bây giờ nó cũng trả về độ dài ghi nhớ nơi nó dừng đệ quy, mà chúng ta có thể sử dụng để khởi tạo calculateLengths, như:

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

Bây giờ chúng ta đã thực hiện các phần tương đối hiệu quả, chúng ta cần tìm cách đưa các kết quả của phép tính trước vào đầu vào của phép tính tiếp theo. Điều này được gọi là a fold, và trông giống như:

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)

Bây giờ để tìm câu trả lời thực tế, chúng ta chỉ cần lọc các khóa trong bản đồ giữa phạm vi đã cho và tìm giá trị tối đa, đưa ra kết quả cuối cùng là:

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
}

Trong REPL của tôi cho các phạm vi kích thước 1000 hoặc hơn, như đầu vào ví dụ, câu trả lời trả về khá nhiều ngay lập tức.


3

Karl Bielefeld đã trả lời tốt câu hỏi, tôi sẽ chỉ thêm một phiên bản Haskell.

Đầu tiên, một phiên bản đơn giản, không ghi nhớ của thuật toán cơ bản để thể hiện đệ quy hiệu quả:

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)

Điều đó nên gần như tự giải thích.

Tôi cũng vậy, sẽ sử dụng một cách đơn giản Mapđể lưu trữ kết quả.

-- 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

Chúng tôi luôn có thể tra cứu kết quả cuối cùng của mình trong cửa hàng, vì vậy với một giá trị duy nhất, chữ ký là

memoCollatz :: Int -> Store -> Store

Hãy bắt đầu với trường hợp kết thúc

memoCollatz 1 store = Map.insert 1 1 store

Vâng, chúng tôi có thể thêm nó trước, nhưng tôi không quan tâm. Trường hợp đơn giản tiếp theo xin vui lòng.

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

Nếu giá trị là có, thì nó là. Vẫn không làm gì cả.

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

Nếu giá trị không ở đó, chúng ta phải làm một cái gì đó . Hãy đặt một chức năng địa phương. Lưu ý cách phần này trông rất gần với giải pháp "đơn giản", chỉ có đệ quy phức tạp hơn một chút.

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

Bây giờ chúng tôi cuối cùng đã làm một cái gì đó. Nếu chúng ta tìm thấy giá trị được tính toán trong store''(sidenote: có hai tô sáng cú pháp haskell, nhưng một là xấu, thì cái kia bị nhầm lẫn bởi biểu tượng nguyên tố. Đó là lý do duy nhất cho nguyên tố kép.), Chúng ta chỉ cần thêm mới giá trị. Nhưng bây giờ nó trở nên thú vị. Nếu chúng tôi không tìm thấy giá trị, chúng tôi phải tính toán và cập nhật. Nhưng chúng tôi đã có chức năng cho cả hai! Vì thế

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

Và bây giờ chúng ta có thể tính toán một giá trị duy nhất một cách hiệu quả. Nếu chúng tôi muốn tính toán một số, chúng tôi chỉ cần chuyển qua cửa hàng thông qua một lần.

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

(Ở đây bạn có thể khởi tạo trường hợp 1/1.)

Bây giờ tất cả chúng ta phải làm là để trích xuất tối đa. Hiện tại không thể có một giá trị trong cửa hàng cao hơn một trong phạm vi, vì vậy, đủ để nói

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

Tất nhiên, nếu bạn muốn tính toán một số phạm vi và chia sẻ cửa hàng giữa các tính toán đó (các nếp gấp là bạn của bạn), bạn sẽ cần một bộ lọc, nhưng đó không phải là trọng tâm chính ở đây.


1
Để thêm tốc độ, Data.IntMap.Strictnên được sử dụng.
Oledit
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.