2011-07-10 32 views
22

Tôi đang xem Problem thirty one trên Project Euler, hỏi có bao nhiêu cách khác nhau để tạo ra £ 2 bằng cách sử dụng số tiền 1p, 2p, 5p, 10p, 20p, 50p, £ 1 (100p) và £ 2 (200p).Lập trình động trong mô hình chức năng

Có nhiều giải pháp đệ quy như thế này ở Scala (tín dụng cho Pavel Fatin)

def f(ms: List[Int], n: Int): Int = ms match { 
    case h :: t => 
    if (h > n) 0 else if (n == h) 1 else f(ms, n - h) + f(t, n) 
    case _ => 0 
} 
val r = f(List(1, 2, 5, 10, 20, 50, 100, 200), 200) 

và mặc dù nó chạy đủ nhanh, nó tương đối hiệu quả, gọi f chức năng khoảng 5,6 triệu lần.

tôi thấy giải pháp của người khác trong Java được lập trình tự động (tín dụng để wizeman từ Bồ Đào Nha)

final static int TOTAL = 200; 

public static void main(String[] args) { 
    int[] coins = {1, 2, 5, 10, 20, 50, 100, 200}; 
    int[] ways = new int[TOTAL + 1]; 
    ways[0] = 1; 

    for (int coin : coins) { 
     for (int j = coin; j <= TOTAL; j++) { 
      ways[j] += ways[j - coin]; 
     } 
    } 

    System.out.println("Result: " + ways[TOTAL]); 
} 

Đây là hiệu quả hơn và vượt qua các vòng trong chỉ 1220 lần.

Trong khi tôi rõ ràng có thể dịch nguyên văn này ít nhiều thành Scala khi sử dụng các đối tượng Array, có cách chức năng thành ngữ để thực hiện điều này bằng cấu trúc dữ liệu không thay đổi được không?

Tôi đã cố gắng và trở nên khó khăn khi cố gắng đệ quy cập nhật List trước khi quyết định tôi có thể đang tiếp cận nó theo cách sai.

+3

tôi nhìn vào phiên bản Scala trong 1 phút và có thể nói như thế nào nó làm việc. Nhìn vào Java trong 5 phút, và tôi vẫn không biết nó hoạt động như thế nào. Đối với tôi một ví dụ tốt cho thấy chức năng không phức tạp hơn mệnh lệnh. – huynhjl

+8

@huynhjl Nhưng bên cạnh nhìn vào một algortihm chức năng và imerative, bạn đang đồng thời nhìn vào một thuật toán kinh điển và tối ưu hóa. – ziggystar

Trả lời

20

Bất cứ khi nào một phần nào đó của danh sách dữ liệu được tính toán dựa trên yếu tố trước đó, tôi nghĩ đến việc đệ quy Stream. Thật không may, đệ quy như vậy không thể xảy ra bên trong các định nghĩa hoặc hàm của phương thức, vì vậy tôi phải chuyển một hàm thành một lớp để làm cho nó hoạt động.

class IterationForCoin(stream: Stream[Int], coin: Int) { 
    val (lower, higher) = stream splitAt coin 
    val next: Stream[Int] = lower #::: (higher zip next map { case (a, b) => a + b }) 
} 
val coins = List(1, 2, 5, 10, 20, 50, 100, 200) 
val result = coins.foldLeft(1 #:: Stream.fill(200)(0)) { (stream, coin) => 
    new IterationForCoin(stream, coin).next 
} last 

Các định nghĩa của lowerhigher là không cần thiết - tôi có thể dễ dàng thay thế chúng với stream take coinstream drop coin, nhưng tôi nghĩ rằng đó là rõ ràng hơn một chút (và hiệu quả hơn) theo cách này.

+0

Luồng có thực sự đang làm gì ở đây hay là thực tế là mức cao hơn và thấp hơn đang được sử dụng lại (vì vậy đây là mẹo "lưu trữ" ngăn cản sự cố sao chép?) – PeteyPabPro

+0

@PeteyPabPro Cả hai. 'Stream' làm cho nó có thể thực hiện đệ quy trên dữ liệu, làm cho nó có thể thực hiện việc tái sử dụng. –

+0

@DanielCSobral Nhưng người ta có thể tạo một phiên bản được sửa đổi một chút mà không có 'Dòng', đúng không? (xem câu trả lời của tôi dưới đây). – PeteyPabPro

16

Tôi không biết đủ về Scala để nhận xét cụ thể về điều đó, nhưng cách điển hình để dịch một giải pháp DP trong một đệ quy là ghi nhớ (sử dụng http://en.wikipedia.org/wiki/Memoization). Điều này về cơ bản là lưu vào bộ nhớ cache kết quả của hàm của bạn cho tất cả các giá trị của miền

Tôi cũng tìm thấy giá trị này http://michid.wordpress.com/2009/02/23/function_mem/. HTH

+1

Trong khi nó hoạt động và là giải pháp yêu thích của tôi, tôi đã quan sát các vấn đề hiệu suất lớn với điều này. Sử dụng các vòng lặp trên mảng là giải pháp hiệu quả nhất (trên Scala 2.8.1), và tôi đang nói (ít nhất) các yếu tố trong hàng chục ở đây. – Raphael

+0

@Raphael: khi thời gian chạy chỉ là một vài phần nghìn giây, một vài yếu tố của mười không thực sự quan trọng! –

+0

@Gareth Tôi có lẽ đã thay đổi vấn đề để nó yêu cầu số lượng các cách để làm cho một cái gì đó lớn hơn như £ 10. Giải pháp động hoàn thành ngay lập tức với 7620 vòng lặp. Các giải pháp đệ quy vẫn còn chạy sau 10 phút và tôi nghi ngờ giá trị tiền tệ cần thiết cho tính toán để mất nhiều thời gian hơn so với tuổi thọ của vũ trụ sẽ không phải là rất cao. –

7

Ok, đây là phiên bản được ghi nhớ của mã Pavel Fatin. Tôi đang sử dụng công cụ ghi nhớ Scalaz, mặc dù thật đơn giản để viết lớp ghi nhớ của riêng bạn.

import scalaz._ 
import Scalaz._ 

val memo = immutableHashMapMemo[(List[Int], Int), Int] 
def f(ms: List[Int], n: Int): Int = ms match { 
    case h :: t => 
    if (h > n) 0 else if (n == h) 1 else memo((f _).tupled)(ms, n - h) + memo((f _).tupled)(t, n) 
    case _ => 0 
} 
val r = f(List(1, 2, 5, 10, 20, 50, 100, 200), 200) 
+0

Nó hoạt động nhưng ngăn xếp tràn ở mức £ 7,50 trở lên: o –

+0

@ Luigi bạn sử dụng một ngăn xếp quá nhỏ. :-) Các tràn không có gì để làm với các memoization - vì có hai cuộc gọi đến 'f', một trong số họ sẽ không bao giờ được đệ quy đuôi. Trong trường hợp này, hãy xem xét rằng cuộc gọi đầu tiên sẽ recurse với cùng một 'ms' tham số, giảm' n' bởi 'h' mỗi lần cho đến khi' n == h' hoặc 'h> n'. Khi được gọi đầu tiên, 'n' sẽ bắt đầu là 750 và' h' là 1, vì vậy nó sẽ thu hồi 750 lần. Tôi đã thử mười pound mà không có vấn đề, nhưng tôi thường đặt '-Xss1m'. –

11

lập trình năng động chức năng thực sự có thể được thực sự xinh đẹp trong một ngôn ngữ lười biếng, chẳng hạn như Haskell (có an article on it trên wiki Haskell). Đây là một giải pháp quy hoạch động để các vấn đề:

import Data.Array 

makeChange :: [Int] -> Int -> Int 
makeChange coinsList target = arr ! (0,target) 
    where numCoins = length coinsList 
     coins = listArray (0,numCoins-1) coinsList 
     bounds = ((0,0),(numCoins,target)) 
     arr  = listArray bounds . map (uncurry go) $ range bounds 
     go i n | i == numCoins = 0 
       | otherwise  = let c = coins ! i 
            in case c `compare` n of 
             GT -> 0 
             EQ -> 1 
             LT -> (arr ! (i, n-c)) + (arr ! (i+1,n)) 

main :: IO() 
main = putStrLn $ "Project Euler Problem 31: " 
       ++ show (makeChange [1, 2, 5, 10, 20, 50, 100, 200] 200) 

Phải thừa nhận rằng, điều này sử dụng O (cn) bộ nhớ, nơi c là số tiền xu và n được mục tiêu (như trái ngược với Bộ nhớ O (n) của phiên bản Java); để có được điều đó, bạn phải sử dụng một số kỹ thuật chụp trạng thái có thể thay đổi (có thể là STArray).Tuy nhiên, cả hai đều chạy trong thời gian O (cn). Ý tưởng là mã hóa giải pháp đệ quy gần như trực tiếp đệ quy, nhưng thay vì đệ quy trong phạm vi , hãy truy cập, chúng tôi tra cứu câu trả lời trong mảng. Và làm thế nào để chúng tôi xây dựng mảng? Bằng cách gọi , hãy truy cập trên mọi chỉ mục. Kể từ khi Haskell là lười biếng, nó chỉ tính toán những điều khi được yêu cầu, do đó, thứ tự đánh giá các công cụ cần thiết cho lập trình năng động là tất cả xử lý minh bạch.

Và nhờ vào các thông số theo tên Scala và lazy val s, chúng ta có thể bắt chước giải pháp này trong Scala:

class Lazy[A](x: => A) { 
    lazy val value = x 
} 

object Lazy { 
    def apply[A](x: => A) = new Lazy(x) 
    implicit def fromLazy[A](z: Lazy[A]): A = z.value 
    implicit def toLazy[A](x: => A): Lazy[A] = Lazy(x) 
} 

import Lazy._ 

def makeChange(coins: Array[Int], target: Int): Int = { 
    val numCoins = coins.length 
    lazy val arr: Array[Array[Lazy[Int]]] 
    = Array.tabulate(numCoins+1,target+1) { (i,n) => 
     if (i == numCoins) { 
      0 
     } else { 
      val c = coins(i) 
      if (c > n) 
      0 
      else if (c == n) 
      1 
      else 
      arr(i)(n-c) + arr(i+1)(n) 
     } 
     } 
    arr(0)(target) 
} 

// makeChange(Array(1, 2, 5, 10, 20, 50, 100, 200), 200) 

Lớp Lazy mã hóa các giá trị mà chỉ được đánh giá theo yêu cầu, và sau đó chúng ta xây dựng một mảng đầy đủ của họ. Cả hai giải pháp này đều hoạt động với giá trị đích thực là 10000, mặc dù đi lớn hơn nhiều và bạn sẽ chạy vào tràn số nguyên hoặc (trong Scala, ít nhất) một tràn ngăn xếp.

4

Vì lợi ích của sự hoàn chỉnh, đây là một biến thể nhẹ của câu trả lời ở trên mà không sử dụng Stream:

object coins { 
    val coins = List(1, 2, 5, 10, 20, 50, 100, 200) 
    val total = 200 
    val result = coins.foldLeft(1 :: List.fill(total)(0)) { (list, coin) => 
    new IterationForCoin(list, coin).next(total) 
    } last 
} 

class IterationForCoin(list: List[Int], coin: Int) { 
    val (lower, higher) = list splitAt coin 
    def next (total: Int): List[Int] = { 
    val listPart = if (total>coin) next(total-coin) else lower 
    lower ::: (higher zip listPart map { case (a, b) => a + b }) 
    } 
} 
Các vấn đề liên quan