Memoization ใน Haskell?


139

คำแนะนำใด ๆ เกี่ยวกับวิธีแก้ปัญหาอย่างมีประสิทธิภาพฟังก์ชันต่อไปนี้ใน Haskell สำหรับตัวเลขจำนวนมาก (n > 108)

f(n) = max(n, f(n/2) + f(n/3) + f(n/4))

ฉันเคยเห็นตัวอย่างของการช่วยจำใน Haskell เพื่อแก้ตัวเลข fibonacci ซึ่งเกี่ยวข้องกับการคำนวณ (อย่างเกียจคร้าน) ตัวเลข fibonacci ทั้งหมดจนถึง n ที่ต้องการ แต่ในกรณีนี้สำหรับ n ที่กำหนดเราจำเป็นต้องคำนวณผลลัพธ์ระดับกลางเพียงไม่กี่รายการเท่านั้น

ขอบคุณ


110
เฉพาะในแง่ที่ว่าเป็นงานที่ฉันทำที่บ้าน :-)
Angel de Vicente

คำตอบ:


258

เราสามารถทำได้อย่างมีประสิทธิภาพโดยการสร้างโครงสร้างที่เราสามารถจัดทำดัชนีในเวลาเชิงเส้นย่อย

แต่แรก,

{-# LANGUAGE BangPatterns #-}

import Data.Function (fix)

มากำหนดfกัน แต่ให้ใช้ 'open recursion' แทนที่จะเรียกตัวเองโดยตรง

f :: (Int -> Int) -> Int -> Int
f mf 0 = 0
f mf n = max n $ mf (n `div` 2) +
                 mf (n `div` 3) +
                 mf (n `div` 4)

คุณสามารถรับ unmemoized fโดยใช้fix f

สิ่งนี้จะช่วยให้คุณทดสอบว่าfสิ่งที่คุณหมายถึงสำหรับค่าเล็ก ๆ ของการfโทรตัวอย่างเช่น:fix f 123 = 144

เราสามารถบันทึกสิ่งนี้ได้โดยกำหนด:

f_list :: [Int]
f_list = map (f faster_f) [0..]

faster_f :: Int -> Int
faster_f n = f_list !! n

ที่ทำงานได้ดีพอสมควรและแทนที่สิ่งที่จะรับO (n ^ 3)ด้วยสิ่งที่จดจำผลลัพธ์ระดับกลาง

แต่ยังต้องใช้เวลาเชิงเส้นในการจัดทำดัชนีเพื่อค้นหาคำตอบที่mfบันทึกไว้ ซึ่งหมายความว่าผลลัพธ์เช่น:

*Main Data.List> faster_f 123801
248604

สามารถทนได้ แต่ผลลัพธ์ก็ไม่ได้ดีไปกว่านั้น เราทำได้ดีกว่านี้!

ก่อนอื่นให้กำหนดต้นไม้ที่ไม่มีที่สิ้นสุด:

data Tree a = Tree (Tree a) a (Tree a)
instance Functor Tree where
    fmap f (Tree l m r) = Tree (fmap f l) (f m) (fmap f r)

จากนั้นเราจะกำหนดวิธีการจัดทำดัชนีเพื่อให้เราสามารถค้นหาโหนดที่มีดัชนีnในเวลาO (log n)แทน:

index :: Tree a -> Int -> a
index (Tree _ m _) 0 = m
index (Tree l _ r) n = case (n - 1) `divMod` 2 of
    (q,0) -> index l q
    (q,1) -> index r q

... และเราอาจพบต้นไม้ที่เต็มไปด้วยตัวเลขธรรมชาติเพื่อความสะดวกดังนั้นเราจึงไม่ต้องไปยุ่งกับดัชนีเหล่านั้น:

nats :: Tree Int
nats = go 0 1
    where
        go !n !s = Tree (go l s') n (go r s')
            where
                l = n + s
                r = l + s
                s' = s * 2

เนื่องจากเราสามารถจัดทำดัชนีได้คุณจึงสามารถแปลงต้นไม้เป็นรายการ:

toList :: Tree a -> [a]
toList as = map (index as) [0..]

คุณสามารถตรวจสอบงานจนถึงตอนนี้ได้โดยการยืนยันว่าtoList natsให้คุณ[0..]

ตอนนี้

f_tree :: Tree Int
f_tree = fmap (f fastest_f) nats

fastest_f :: Int -> Int
fastest_f = index f_tree

ทำงานเหมือนกับรายการด้านบน แต่แทนที่จะใช้เวลาเชิงเส้นในการค้นหาแต่ละโหนดสามารถไล่ตามเวลาลอการิทึมได้

ผลลัพธ์เร็วกว่ามาก:

*Main> fastest_f 12380192300
67652175206

*Main> fastest_f 12793129379123
120695231674999

ในความเป็นจริงมันเร็วกว่ามากที่คุณสามารถผ่านและแทนที่Intด้วยIntegerด้านบนและรับคำตอบขนาดใหญ่ที่น่าขันเกือบจะในทันที

*Main> fastest_f' 1230891823091823018203123
93721573993600178112200489

*Main> fastest_f' 12308918230918230182031231231293810923
11097012733777002208302545289166620866358

3
ฉันลองใช้รหัสนี้และที่น่าสนใจคือ f_faster ดูเหมือนจะช้ากว่า f ฉันเดาว่าการอ้างอิงรายการเหล่านั้นทำให้สิ่งต่างๆช้าลงจริงๆ คำจำกัดความของ nats และ index ดูลึกลับสำหรับฉันดังนั้นฉันจึงเพิ่มคำตอบของตัวเองซึ่งอาจทำให้สิ่งต่างๆชัดเจนขึ้น
Pitarou

5
กรณีรายการที่ไม่มีที่สิ้นสุดต้องจัดการกับรายการที่เชื่อมโยง 111111111 รายการที่ยาว กรณีต้นไม้กำลังจัดการกับ log n * จำนวนโหนดที่เข้าถึง
Edward KMETT

2
กล่าวคือเวอร์ชันรายการต้องสร้าง thunks สำหรับโหนดทั้งหมดในรายการในขณะที่เวอร์ชันต้นไม้หลีกเลี่ยงการสร้างจำนวนมาก
Tom Ellis

7
ฉันรู้ว่านี่เป็นโพสต์ที่ค่อนข้างเก่า แต่ไม่ควรf_treeกำหนดไว้ในwhereประโยคเพื่อหลีกเลี่ยงการบันทึกเส้นทางที่ไม่จำเป็นในแผนภูมิระหว่างการโทร?
dfeuer

17
เหตุผลในการบรรจุไว้ใน CAF คือคุณสามารถรับการบันทึกระหว่างการโทรได้ ถ้าฉันโทรแพงฉันก็จำฉันก็คงทิ้งไว้ที่ CAF ด้วยเหตุนี้เทคนิคที่แสดงไว้ที่นี่ ในแอปพลิเคชันจริงมีการแลกเปลี่ยนระหว่างผลประโยชน์และค่าใช้จ่ายของการบันทึกถาวรแน่นอน แม้ว่าคำถามจะเกี่ยวกับวิธีการบรรลุการบันทึก แต่ฉันคิดว่ามันน่าจะทำให้เข้าใจผิดที่จะตอบด้วยเทคนิคที่จงใจหลีกเลี่ยงการบันทึกระหว่างการโทรและหากไม่มีอะไรอื่นคำอธิบายนี้จะชี้ให้ผู้คนเข้าใจว่ามีรายละเอียดปลีกย่อย ;)
Edward KMETT

17

คำตอบของเอ็ดเวิร์ดเป็นอัญมณีที่ยอดเยี่ยมที่ฉันได้ทำซ้ำและจัดเตรียมการใช้งานmemoListและตัวmemoTreeประสานที่ช่วยจดจำฟังก์ชันในรูปแบบเปิดซ้ำ

{-# LANGUAGE BangPatterns #-}

import Data.Function (fix)

f :: (Integer -> Integer) -> Integer -> Integer
f mf 0 = 0
f mf n = max n $ mf (div n 2) +
                 mf (div n 3) +
                 mf (div n 4)


-- Memoizing using a list

-- The memoizing functionality depends on this being in eta reduced form!
memoList :: ((Integer -> Integer) -> Integer -> Integer) -> Integer -> Integer
memoList f = memoList_f
  where memoList_f = (memo !!) . fromInteger
        memo = map (f memoList_f) [0..]

faster_f :: Integer -> Integer
faster_f = memoList f


-- Memoizing using a tree

data Tree a = Tree (Tree a) a (Tree a)
instance Functor Tree where
    fmap f (Tree l m r) = Tree (fmap f l) (f m) (fmap f r)

index :: Tree a -> Integer -> a
index (Tree _ m _) 0 = m
index (Tree l _ r) n = case (n - 1) `divMod` 2 of
    (q,0) -> index l q
    (q,1) -> index r q

nats :: Tree Integer
nats = go 0 1
    where
        go !n !s = Tree (go l s') n (go r s')
            where
                l = n + s
                r = l + s
                s' = s * 2

toList :: Tree a -> [a]
toList as = map (index as) [0..]

-- The memoizing functionality depends on this being in eta reduced form!
memoTree :: ((Integer -> Integer) -> Integer -> Integer) -> Integer -> Integer
memoTree f = memoTree_f
  where memoTree_f = index memo
        memo = fmap (f memoTree_f) nats

fastest_f :: Integer -> Integer
fastest_f = memoTree f

12

ไม่ใช่วิธีที่มีประสิทธิภาพที่สุด แต่ช่วยจดจำ:

f = 0 : [ g n | n <- [1..] ]
    where g n = max n $ f!!(n `div` 2) + f!!(n `div` 3) + f!!(n `div` 4)

เมื่อร้องขอf !! 144จะมีการตรวจสอบว่าf !! 143มีอยู่ แต่ไม่ได้คำนวณค่าที่แน่นอน ยังคงตั้งค่าเป็นผลการคำนวณที่ไม่รู้จัก ค่าที่แน่นอนเท่านั้นที่คำนวณได้คือค่าที่จำเป็น

ดังนั้นในขั้นต้นเท่าที่คำนวณแล้วโปรแกรมไม่รู้อะไรเลย

f = .... 

เมื่อเราส่งคำขอf !! 12มันจะเริ่มทำการจับคู่รูปแบบบางอย่าง:

f = 0 : g 1 : g 2 : g 3 : g 4 : g 5 : g 6 : g 7 : g 8 : g 9 : g 10 : g 11 : g 12 : ...

ตอนนี้มันเริ่มคำนวณ

f !! 12 = g 12 = max 12 $ f!!6 + f!!4 + f!!3

สิ่งนี้ทำให้เกิดความต้องการอีกครั้งบน f ดังนั้นเราจึงคำนวณ

f !! 6 = g 6 = max 6 $ f !! 3 + f !! 2 + f !! 1
f !! 3 = g 3 = max 3 $ f !! 1 + f !! 1 + f !! 0
f !! 1 = g 1 = max 1 $ f !! 0 + f !! 0 + f !! 0
f !! 0 = 0

ตอนนี้เราสามารถหยดสำรองได้

f !! 1 = g 1 = max 1 $ 0 + 0 + 0 = 1

ซึ่งหมายความว่าตอนนี้โปรแกรมรู้แล้ว:

f = 0 : 1 : g 2 : g 3 : g 4 : g 5 : g 6 : g 7 : g 8 : g 9 : g 10 : g 11 : g 12 : ...

หยดต่อไป:

f !! 3 = g 3 = max 3 $ 1 + 1 + 0 = 3

ซึ่งหมายความว่าตอนนี้โปรแกรมรู้แล้ว:

f = 0 : 1 : g 2 : 3 : g 4 : g 5 : g 6 : g 7 : g 8 : g 9 : g 10 : g 11 : g 12 : ...

ตอนนี้เราดำเนินการคำนวณต่อไปนี้f!!6:

f !! 6 = g 6 = max 6 $ 3 + f !! 2 + 1
f !! 2 = g 2 = max 2 $ f !! 1 + f !! 0 + f !! 0 = max 2 $ 1 + 0 + 0 = 2
f !! 6 = g 6 = max 6 $ 3 + 2 + 1 = 6

ซึ่งหมายความว่าตอนนี้โปรแกรมรู้แล้ว:

f = 0 : 1 : 2 : 3 : g 4 : g 5 : 6 : g 7 : g 8 : g 9 : g 10 : g 11 : g 12 : ...

ตอนนี้เราดำเนินการคำนวณต่อไปนี้f!!12:

f !! 12 = g 12 = max 12 $ 6 + f!!4 + 3
f !! 4 = g 4 = max 4 $ f !! 2 + f !! 1 + f !! 1 = max 4 $ 2 + 1 + 1 = 4
f !! 12 = g 12 = max 12 $ 6 + 4 + 3 = 13

ซึ่งหมายความว่าตอนนี้โปรแกรมรู้แล้ว:

f = 0 : 1 : 2 : 3 : 4 : g 5 : 6 : g 7 : g 8 : g 9 : g 10 : g 11 : 13 : ...

ดังนั้นการคำนวณจึงค่อนข้างเฉื่อยชา โปรแกรมรู้ว่ามีค่าบางค่าf !! 8เท่ากับg 8แต่ไม่รู้ว่าอะไรg 8คืออะไร


ขอบคุณสำหรับสิ่งนี้ คุณจะสร้างและใช้พื้นที่โซลูชัน 2 มิติได้อย่างไร? นั่นจะเป็นลิสต์ไหม และg n m = (something with) f!!a!!b
vikingsteve

1
แน่นอนคุณทำได้ สำหรับวิธีแก้ปัญหาที่แท้จริงฉันอาจใช้ไลบรารีบันทึกช่วยจำเช่นmemocombinators
rampion

มันน่าเสียดายที่ O (n ^ 2)
Qumeric

9

ตามที่ระบุไว้ในคำตอบของ Edward Kmett เพื่อเร่งความเร็วคุณต้องแคชการคำนวณที่มีค่าใช้จ่ายสูงและสามารถเข้าถึงได้อย่างรวดเร็ว

เพื่อให้ฟังก์ชันไม่เป็น monadic การแก้ปัญหาของการสร้างต้นไม้ขี้เกียจที่ไม่มีที่สิ้นสุดด้วยวิธีที่เหมาะสมในการจัดทำดัชนี (ดังที่แสดงในโพสต์ก่อนหน้านี้) บรรลุเป้าหมายดังกล่าว หากคุณละทิ้งลักษณะที่ไม่ใช่เชิงเดี่ยวของฟังก์ชันคุณสามารถใช้คอนเทนเนอร์เชื่อมโยงมาตรฐานที่มีอยู่ใน Haskell ร่วมกับ monads แบบ "เหมือนรัฐ" (เช่น State หรือ ST)

ในขณะที่ข้อเสียเปรียบหลักคือคุณได้รับฟังก์ชันที่ไม่ใช่ monadic คุณไม่จำเป็นต้องทำดัชนีโครงสร้างด้วยตัวเองอีกต่อไปและสามารถใช้การใช้งานมาตรฐานของคอนเทนเนอร์แบบเชื่อมโยงได้

ในการทำเช่นนั้นก่อนอื่นคุณต้องเขียนฟังก์ชันของคุณใหม่เพื่อยอมรับ monad ประเภทใดก็ได้:

fm :: (Integral a, Monad m) => (a -> m a) -> a -> m a
fm _    0 = return 0
fm recf n = do
   recs <- mapM recf $ div n <$> [2, 3, 4]
   return $ max n (sum recs)

สำหรับการทดสอบของคุณคุณยังคงกำหนดฟังก์ชันที่ไม่มีการบันทึกโดยใช้ Data.Function.fix ได้แม้ว่าจะเป็นแบบละเอียดกว่าเล็กน้อย:

noMemoF :: (Integral n) => n -> n
noMemoF = runIdentity . fix fm

จากนั้นคุณสามารถใช้ State monad ร่วมกับ Data.Map เพื่อเร่งความเร็ว:

import qualified Data.Map.Strict as MS

withMemoStMap :: (Integral n) => n -> n
withMemoStMap n = evalState (fm recF n) MS.empty
   where
      recF i = do
         v <- MS.lookup i <$> get
         case v of
            Just v' -> return v' 
            Nothing -> do
               v' <- fm recF i
               modify $ MS.insert i v'
               return v'

ด้วยการเปลี่ยนแปลงเล็กน้อยคุณสามารถปรับรหัสให้ทำงานกับ Data.HashMap แทน:

import qualified Data.HashMap.Strict as HMS

withMemoStHMap :: (Integral n, Hashable n) => n -> n
withMemoStHMap n = evalState (fm recF n) HMS.empty
   where
      recF i = do
         v <- HMS.lookup i <$> get
         case v of
            Just v' -> return v' 
            Nothing -> do
               v' <- fm recF i
               modify $ HMS.insert i v'
               return v'

แทนที่จะใช้โครงสร้างข้อมูลแบบถาวรคุณอาจลองใช้โครงสร้างข้อมูลที่เปลี่ยนแปลงได้ (เช่น Data.HashTable) ร่วมกับ ST monad:

import qualified Data.HashTable.ST.Linear as MHM

withMemoMutMap :: (Integral n, Hashable n) => n -> n
withMemoMutMap n = runST $
   do ht <- MHM.new
      recF ht n
   where
      recF ht i = do
         k <- MHM.lookup ht i
         case k of
            Just k' -> return k'
            Nothing -> do 
               k' <- fm (recF ht) i
               MHM.insert ht i k'
               return k'

เมื่อเทียบกับการนำไปใช้งานโดยไม่มีการบันทึกช่วยจำใด ๆ การใช้งานเหล่านี้ช่วยให้คุณได้รับอินพุตจำนวนมากเพื่อให้ได้ผลลัพธ์เป็นไมโครวินาทีแทนที่จะต้องรอหลายวินาที

การใช้เกณฑ์เป็นเกณฑ์มาตรฐานฉันสังเกตได้ว่าการใช้งานกับ Data HashMap ทำงานได้ดีกว่า Data.Map และ Data HashTable เล็กน้อย (ประมาณ 20%) ซึ่งการกำหนดเวลาใกล้เคียงกันมาก

ฉันพบว่าผลลัพธ์ของเกณฑ์มาตรฐานค่อนข้างน่าแปลกใจ ความรู้สึกเริ่มแรกของฉันคือ HashTable จะมีประสิทธิภาพดีกว่าการใช้งาน HashMap เนื่องจากไม่สามารถเปลี่ยนแปลงได้ อาจมีข้อบกพร่องด้านประสิทธิภาพบางอย่างซ่อนอยู่ในการใช้งานครั้งล่าสุดนี้


2
GHC ทำงานได้ดีมากในการเพิ่มประสิทธิภาพโครงสร้างที่ไม่เปลี่ยนรูป สัญชาตญาณจาก C ไม่ได้เลื่อนออกไปเสมอไป
John Tyree

8

นี่เป็นภาคผนวกสำหรับคำตอบที่ยอดเยี่ยมของ Edward Kmett

เมื่อฉันลองใช้รหัสของเขาคำจำกัดความของมันnatsและindexดูลึกลับมากดังนั้นฉันจึงเขียนเวอร์ชันอื่นที่ฉันคิดว่าเข้าใจง่ายขึ้น

ฉันกำหนดindexและnatsในแง่ของและindex'nats'

index' t n[1..]ถูกกำหนดไว้ในช่วงช่วง (เรียกคืนที่index tกำหนดไว้ในช่วง[0..]) มันทำงานค้นหาต้นไม้โดยถือว่าnเป็นสตริงของบิตและอ่านผ่านบิตในทางกลับกัน ถ้าเป็นบิตจะ1ใช้สาขาขวามือ ถ้าเป็นบิตจะ0ใช้สาขาซ้ายมือ จะหยุดเมื่อถึงบิตสุดท้าย (ซึ่งต้องเป็น a 1)

index' (Tree l m r) 1 = m
index' (Tree l m r) n = case n `divMod` 2 of
                          (n', 0) -> index' l n'
                          (n', 1) -> index' r n'

เช่นเดียวกับที่natsมีการกำหนดไว้สำหรับindexเพื่อให้index nats n == nเป็นความจริงเสมอถูกกำหนดไว้สำหรับnats'index'

nats' = Tree l 1 r
  where
    l = fmap (\n -> n*2)     nats'
    r = fmap (\n -> n*2 + 1) nats'
    nats' = Tree l 1 r

ตอนนี้natsและindexเป็นเพียงnats'และindex'แต่ด้วยค่าที่เลื่อนโดย 1:

index t n = index' t (n+1)
nats = fmap (\n -> n-1) nats'

ขอบคุณ. ฉันกำลังจำฟังก์ชันหลายตัวแปรและสิ่งนี้ช่วยให้ฉันทราบได้ว่าดัชนีและนัตกำลังทำอะไรอยู่
Kittsil

3

สองสามปีต่อมาฉันได้ดูสิ่งนี้และตระหนักว่ามีวิธีง่ายๆในการจดจำสิ่งนี้ในเวลาเชิงเส้นโดยใช้zipWithฟังก์ชันตัวช่วย:

dilate :: Int -> [x] -> [x]
dilate n xs = replicate n =<< xs

dilateมีคุณสมบัติที่dilate n xs !! i == xs !! div i nสะดวก

ดังนั้นสมมติว่าเราได้รับ f (0) สิ่งนี้จะทำให้การคำนวณง่ายขึ้น

fs = f0 : zipWith max [1..] (tail $ fs#/2 .+. fs#/3 .+. fs#/4)
  where (.+.) = zipWith (+)
        infixl 6 .+.
        (#/) = flip dilate
        infixl 7 #/

ดูคล้ายกับคำอธิบายปัญหาดั้งเดิมของเราและให้คำตอบเชิงเส้น ( sum $ take n fsจะใช้ O (n))


2
ดังนั้นจึงเป็นการกำเนิด (corecursive?) หรือการเขียนโปรแกรมแบบไดนามิกโซลูชัน ใช้เวลา O (1) ต่อค่าที่สร้างขึ้นเช่นเดียวกับ Fibonacci ปกติ เยี่ยมมาก! และวิธีการแก้ปัญหาของ EKMETT ก็เหมือนกับลอการิทึม big-Fibonacci ซึ่งไปถึงตัวเลขจำนวนมากได้เร็วขึ้นมากโดยไม่ต้องข้ามสิ่งที่เป็นประโยชน์ ประมาณนี้ใช่ไหม
Will Ness

หรืออาจจะใกล้เคียงกว่าสำหรับตัวเลข Hamming โดยมีตัวชี้ย้อนกลับสามตัวในลำดับที่กำลังถูกผลิตขึ้นและความเร็วที่แตกต่างกันสำหรับแต่ละตัวที่ก้าวหน้าไปตามนั้น น่ารักจริงๆ.
Will Ness

2

อีกหนึ่งภาคผนวกสำหรับคำตอบของ Edward Kmett: ตัวอย่างที่มีอยู่ในตัว:

data NatTrie v = NatTrie (NatTrie v) v (NatTrie v)

memo1 arg_to_index index_to_arg f = (\n -> index nats (arg_to_index n))
  where nats = go 0 1
        go i s = NatTrie (go (i+s) s') (f (index_to_arg i)) (go (i+s') s')
          where s' = 2*s
        index (NatTrie l v r) i
          | i <  0    = f (index_to_arg i)
          | i == 0    = v
          | otherwise = case (i-1) `divMod` 2 of
             (i',0) -> index l i'
             (i',1) -> index r i'

memoNat = memo1 id id 

ใช้ดังต่อไปนี้เพื่อบันทึกฟังก์ชันด้วยจำนวนเต็มอาร์กิวเมนต์เดียว (เช่น fibonacci):

fib = memoNat f
  where f 0 = 0
        f 1 = 1
        f n = fib (n-1) + fib (n-2)

เฉพาะค่าสำหรับอาร์กิวเมนต์ที่ไม่เป็นลบเท่านั้นที่จะถูกแคช

หากต้องการแคชค่าสำหรับอาร์กิวเมนต์เชิงลบให้ใช้memoIntกำหนดดังนี้:

memoInt = memo1 arg_to_index index_to_arg
  where arg_to_index n
         | n < 0     = -2*n
         | otherwise =  2*n + 1
        index_to_arg i = case i `divMod` 2 of
           (n,0) -> -n
           (n,1) ->  n

ในการแคชค่าสำหรับฟังก์ชันที่มีการใช้อาร์กิวเมนต์จำนวนเต็มสองอาร์กิวเมนต์memoIntIntกำหนดดังนี้:

memoIntInt f = memoInt (\n -> memoInt (f n))

2

โซลูชันที่ไม่มีการจัดทำดัชนีและไม่อิงตาม Edward KMETT's

ฉันแยกโครงสร้างย่อยทั่วไปออกเป็นพาเรนต์ทั่วไป ( f(n/4)แชร์ระหว่างf(n/2)และf(n/4)และf(n/6)แชร์ระหว่างf(2)และf(3)) การบันทึกเป็นตัวแปรเดียวในพาเรนต์การคำนวณทรีย่อยจะทำครั้งเดียว

data Tree a =
  Node {datum :: a, child2 :: Tree a, child3 :: Tree a}

f :: Int -> Int
f n = datum root
  where root = f' n Nothing Nothing


-- Pass in the arg
  -- and this node's lifted children (if any).
f' :: Integral a => a -> Maybe (Tree a) -> Maybe (Tree a)-> a
f' 0 _ _ = leaf
    where leaf = Node 0 leaf leaf
f' n m2 m3 = Node d c2 c3
  where
    d = if n < 12 then n
            else max n (d2 + d3 + d4)
    [n2,n3,n4,n6] = map (n `div`) [2,3,4,6]
    [d2,d3,d4,d6] = map datum [c2,c3,c4,c6]
    c2 = case m2 of    -- Check for a passed-in subtree before recursing.
      Just c2' -> c2'
      Nothing -> f' n2 Nothing (Just c6)
    c3 = case m3 of
      Just c3' -> c3'
      Nothing -> f' n3 (Just c6) Nothing
    c4 = child2 c2
    c6 = f' n6 Nothing Nothing

    main =
      print (f 123801)
      -- Should print 248604.

โค้ดไม่สามารถขยายไปยังฟังก์ชันบันทึกช่วยจำทั่วไปได้อย่างง่ายดาย (อย่างน้อยฉันก็ไม่รู้ว่าจะทำอย่างไร) และคุณต้องคิดว่าปัญหาย่อยทับซ้อนกันอย่างไร แต่กลยุทธ์ควรใช้ได้กับพารามิเตอร์ทั่วไปที่ไม่ใช่จำนวนเต็ม . (ฉันคิดว่ามันขึ้นสำหรับพารามิเตอร์สตริงสองตัว)

บันทึกจะถูกทิ้งหลังจากการคำนวณแต่ละครั้ง (อีกครั้งฉันกำลังคิดถึงพารามิเตอร์สตริงสองตัว)

ฉันไม่รู้ว่าจะมีประสิทธิภาพมากกว่าคำตอบอื่น ๆ หรือไม่ ในทางเทคนิคการค้นหาแต่ละครั้งมีเพียงหนึ่งหรือสองขั้นตอนเท่านั้น ("ดูลูกของคุณหรือลูกของคุณ") แต่อาจมีการใช้หน่วยความจำเพิ่มเติมมาก

แก้ไข: วิธีนี้ยังไม่ถูกต้อง การแบ่งปันไม่สมบูรณ์

แก้ไข: ตอนนี้ควรแชร์ลูกย่อยอย่างเหมาะสม แต่ฉันตระหนักว่าปัญหานี้มีการแบ่งปันที่ไม่สำคัญมากมายn/2/2/2และn/3/3อาจจะเหมือนกัน ปัญหาไม่เหมาะสำหรับกลยุทธ์ของฉัน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.