Haskell หาทางแก้ไขปัญหา 3n + 1


12

นี่เป็นปัญหาที่การเขียนโปรแกรมอย่างง่ายจาก Spoj: http://www.spoj.com/problems/PROBTRES/

โดยพื้นฐานแล้วคุณจะถูกขอให้แสดงรอบวัฏจักร Collatz ที่ใหญ่ที่สุดสำหรับตัวเลขระหว่าง i และ j (รอบ Collatz ของจำนวน $ n $ คือจำนวนขั้นตอนที่จะได้รับจาก $ n $ ถึง 1 ในที่สุด)

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

ฉันได้พยายาม Data.Function.Memoize เช่นเดียวกับเทคนิค memoization ที่่กลั่นในบ้านเวลาเข้าสู่ระบบโดยใช้ความคิดจากการโพสต์นี้: /programming/3208258/memoization-in-haskell น่าเสียดายที่การบันทึกช่วยจำทำให้การคำนวณรอบ (n) ช้าลง ฉันเชื่อว่าช้าลงมาจากค่าใช้จ่ายของทาง Haskell (ฉันพยายามเรียกใช้ด้วยรหัสไบนารีที่คอมไพล์แล้วแทนการตีความ)

ฉันยังสงสัยด้วยว่าการวนซ้ำตัวเลขจาก i ถึง j นั้นอาจมีค่าใช้จ่ายสูง ($ i, j \ le10 ^ 6 $) ดังนั้นผมจึงพยายามทุกอย่างแม้กระทั่ง precompute สำหรับการค้นหาช่วงที่ใช้ความคิดจากhttp://blog.openendings.net/2013/10/range-trees-and-profiling-in-haskell.html อย่างไรก็ตามยังคงมีข้อผิดพลาด "เกินขีด จำกัด เวลา"

คุณสามารถช่วยแจ้งโปรแกรม Haskell ที่มีการแข่งขันสูงได้หรือไม่?


10
โพสต์นี้ดูเหมือนดีสำหรับฉัน มันเป็นปัญหาของอัลกอริทึมที่ต้องการการออกแบบที่เหมาะสมเพื่อให้ได้ประสิทธิภาพที่เพียงพอ สิ่งที่เราไม่ต้องการที่นี่จริงๆคือคำถาม "ฉันจะแก้ไขรหัสที่ใช้งานไม่ได้" ได้อย่างไร
Robert Harvey

คำตอบ:


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 ของฉันสำหรับช่วงขนาด 1,000 หรือมากกว่านั้นเช่นอินพุตตัวอย่างคำตอบจะกลับมาทันที


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''(sidenote: มีปากกาเน้นข้อความไวยากรณ์สองรายการ แต่หนึ่งน่าเกลียดอีกอันหนึ่งสับสนโดยสัญลักษณ์สำคัญนั่นคือเหตุผลเดียวสำหรับ double-prime) เราเพิ่งเพิ่มใหม่ ราคา. แต่ตอนนี้มันน่าสนใจ หากเราไม่พบค่าเราจะต้องคำนวณและทำการอัปเดต แต่เรามีฟังก์ชั่นสำหรับทั้งสอง! ดังนั้น

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