[Haskell-cafe] Looking for suggestions to improve my algorithm
David Frey
dpfrey at shaw.ca
Wed Aug 29 17:12:22 EDT 2007
Hello Haskellers,
I have been trying to learn a bit about Haskell by solving Project Euler
problems. For those of you who don't know what Project Euler is, see
http://projecteuler.net
After solving problem 21, which is related to amicable pairs, I decided
to attempt problem 95 since it is an extension of the same topic.
The full description of problem 95 is here:
http://projecteuler.net/index.php?section=problems&id=95
This is the summary:
"Find the smallest member of the longest amicable chain with no element
exceeding one million."
I have attempted to solve this problem, but my solution is too resource
hungry to search the full set of 1000000 numbers.
I am hoping someone reading this list can suggest :
- How I can improve my algorithm
- An alternative algorithm that will be more efficient
- Ways to profile a Haskell program to help me understand why my
implementation is performing poorly.
In addition to the question above, I would also be interested in
comments on the style of the code. If there is a more idiomatic way to
write portions of the code, I would like to see what it is.
My solution is at the bottom of this e-mail. The program will probably
run obscenely slow or die from stack overflow unless you replace
[1..999999] with [1..somethingSmaller] in main.
Thanks,
David Frey
--- BEGIN Main.hs ---
module Main where
import ProjectEuler (takeUntil, divisors)
import qualified Data.Map as M
import qualified Data.IntSet as I
main = let initialContext = Context (I.fromList []) 0 0 in
print $ cycleStart $ foldl checkForChain initialContext [1..999999]
{- Idea:
* Put all the numbers that have been visited into a map regardless of
whether
they are a part of a chain or not.
* Store the min element in the cycle and the number of elements in the
cycle
* As you process, from 1->n if the stopping conditions for a sumOfDivisors
result are:
* has already been seen before
* number is less than the start of this chain attempt
* >= 1,000,000
-}
data Context = Context {seenNum::I.IntSet, cycleStart::Int,
cycleLength::Int}
hasBeenSeen :: Int -> Context -> Bool
hasBeenSeen n context = I.member n (seenNum context)
markSeen :: Int -> Context -> Context
markSeen n context = context { seenNum = (I.insert n (seenNum context)) }
deleteFromSeen :: Int -> Context -> Context
deleteFromSeen n context = context { seenNum = (I.delete n (seenNum
context)) }
{-
- Examines the context to see if the input has potential to be a chain
or not
- based on whether the input number has been visited before. If it
could be a
- chain, an attempt is made to build the chain.
-}
checkForChain :: Context -> Int -> Context
checkForChain context n =
if hasBeenSeen n context
then deleteFromSeen n context
else buildChain context (sum $ divisors n) n [n]
{-
- Builds a chain until ones of the 3 stopping conditions are met or a
chain is
- found. If a chain is found the context will be updated with the new
chain if
- it is the longest.
-
- Stopping Conditions:
- * Number has already been seen before
- * Number is less than the start of this chain attempt
- * Number >= 1,000,000
-}
buildChain :: Context -> Int -> Int -> [Int] -> Context
buildChain context candidate first cycleList =
if elem candidate cycleList
then foundChain (takeUntil ((==) candidate) cycleList) context
else if candidate < first ||
candidate >= 1000000 ||
hasBeenSeen candidate context
then context
else buildChain (markSeen candidate context)
(sum $ divisors candidate)
first
(candidate : cycleList)
{-
- Updates the context with the new longest chain and the start value if
the
- chain input parameter is longer than the one currently in the context.
-}
foundChain :: [Int] -> Context -> Context
foundChain ls context = let
l = length ls
m = minimum ls in
if l > (cycleLength context)
then context { cycleLength = l, cycleStart = m }
else if l == (cycleLength context)
then let m = minimum ls in
if m < (cycleStart context)
then context { cycleStart = m }
else context
else context
--- END Main.hs ---
I put a bunch of common functions in ProjectEuler.hs. Here are the
relevant functions for this problem:
{-
- Gets all of the proper divisors of a number. That is all divisors
starting
- from 1, but not including itself.
-}
divisors :: (Integral a) => a -> [a]
divisors n = let
p1 = [x | x <- [1 .. floor $ sqrt $ fromIntegral n], n `mod` x == 0]
p2 = map (div n) (tail p1)
in p1 `concatNoDupe` (reverse p2) where
{-
- Concatenate two lists. If the last element in the first list
and the
- first element in the second list are ==, produce only the
value from
- the first list in the output.
-}
concatNoDupe :: (Eq a) => [a] -> [a] -> [a]
concatNoDupe [] ys = ys
concatNoDupe [x] (y:ys) = if x == y then (y : ys) else (x : y :
ys)
concatNoDupe (x:xs) ys = x : (concatNoDupe xs ys)
{-
- Similar to takeWhile, but also takes the first element that fails the
- predicate.
-}
takeUntil :: (a -> Bool) -> [a] -> [a]
takeUntil pred (x:xs) = (x : if pred x then [] else takeUntil pred xs)
takeUntil _ [] = []
More information about the Haskell-Cafe
mailing list