
As part of a solution I'm working on for Project Euler problem 119, I wanted to create an ordered list of all powers of all positive integers (starting with squares). This is what I came up with: powers = ( uniq . map fst . iterate next ) ( 1, ( 0, powertable ) ) powertable = map (\ n -> map (\ p -> n ^ p ) [ 2 .. ] ) [ 2 .. ] next ( _, ( lim, ps ) ) = ( p, ( lim', ps' ) ) where ( c, p ) = minimumBy ( compare `on` snd ) . zip [ 0 .. lim ] . head . transpose $ ps lim' = lim + ( if c == lim then 1 else 0 ) ( front, b : back ) = splitAt c ps ps' = front ++ ( tail b : back ) -- like the unix utility uniq [] = [] uniq [x] = [x] uniq (x:y:ys) | x == y = uniq (y:ys) | otherwise = x : uniq (y:ys) Basically, think of a grid of numbers, each row is the list of powers for one integer. To find the next power in the sequence, look at the the first number in each row (since that will be the smallest number in the row), up to limit number of rows. The limit is calculated as the number of rows that we've already taken one number from, plus one. This exploits the fact that the row heads are initially ordered. If we use a number from a row, shift that row down to remove the number used. It does pretty well, but I'd welcome any comments, or even suggestions of a completely different algorithm (for this little exercise, or problem 119). Thanks. Kurt Hutchinson

On Nov 14, 2007 12:32 PM, Kurt Hutchinson
As part of a solution I'm working on for Project Euler problem 119, I wanted to create an ordered list of all powers of all positive integers (starting with squares). This is what I came up with:
powers = ( uniq . map fst . iterate next ) ( 1, ( 0, powertable ) )
powertable = map (\ n -> map (\ p -> n ^ p ) [ 2 .. ] ) [ 2 .. ]
next ( _, ( lim, ps ) ) = ( p, ( lim', ps' ) ) where ( c, p ) = minimumBy ( compare `on` snd ) . zip [ 0 .. lim ] . head . transpose $ ps lim' = lim + ( if c == lim then 1 else 0 ) ( front, b : back ) = splitAt c ps ps' = front ++ ( tail b : back )
-- like the unix utility uniq [] = [] uniq [x] = [x] uniq (x:y:ys) | x == y = uniq (y:ys) | otherwise = x : uniq (y:ys)
Basically, think of a grid of numbers, each row is the list of powers for one integer. To find the next power in the sequence, look at the the first number in each row (since that will be the smallest number in the row), up to limit number of rows. The limit is calculated as the number of rows that we've already taken one number from, plus one. This exploits the fact that the row heads are initially ordered. If we use a number from a row, shift that row down to remove the number used.
It does pretty well, but I'd welcome any comments, or even suggestions of a completely different algorithm (for this little exercise, or problem 119). Thanks.
The merging can be done much more simply and efficiently (this is code I wrote when computing squarefree numbers in a blog posthttp://byorgey.wordpress.com/2007/09/01/squarefree-numbers-in-haskell/a few months ago): -- merge two nondecreasing lists. ( # ) :: (Ord a) => [a] -> [a] -> [a] [] # ys = ys xs # [] = xs xs@(x:xt) # ys@(y:yt) | x < y = x : (xt # ys) | x > y = y : (xs # yt) | otherwise = x : (xt # yt) -- merge an infinite list of lists, assuming that each list -- is nondecreasing and the lists occur in order of their first -- element. mergeAll :: (Ord a) => [[a]] -> [a] mergeAll ([] : zs) = mergeAll zs mergeAll (xxs@(x:xs) : yys@(y:ys) : zs) | x < y = x : mergeAll (xs : yys : zs) | otherwise = mergeAll ((xxs # yys) : zs) Then you can simply define powers = 1 : mergeAll powertable I wrote some code to sum the first n powers and print the result, and compiled (using -O2) first with your method, then with mergeAll. Summing the first 7000 powers took ~8s and ~0.1s respectively, so that's a pretty big speedup. Based on seeing how the times scale, I suspect that your code is O(n^2) or something of that sort, whereas mergeAll is essentially linear, although without scrutinizing your code more closely I'm not exactly sure why that would be the case. -Brent

On Nov 14, 2007 1:06 PM, Brent Yorgey
On Nov 14, 2007 12:32 PM, Kurt Hutchinson
wrote: The merging can be done much more simply and efficiently (this is code I wrote when computing squarefree numbers in a blog post a few months ago):
Wow, thanks for the tip. That really is a huge speed-up. I attempted something like this, but just tried doing a fold of the simple merge over the list of lists. That didn't work (not lazy enough?), which is what sent me down the complicated path above. Kurt

On Nov 14, 2007 2:57 PM, Kurt Hutchinson
On Nov 14, 2007 1:06 PM, Brent Yorgey
wrote: On Nov 14, 2007 12:32 PM, Kurt Hutchinson
wrote: The merging can be done much more simply and efficiently (this is code I wrote when computing squarefree numbers in a blog post a few months ago): Wow, thanks for the tip. That really is a huge speed-up. I attempted something like this, but just tried doing a fold of the simple merge over the list of lists. That didn't work (not lazy enough?), which is what sent me down the complicated path above.
Exactly. A standard fold of merges will never get around to generating any elements, since it must inspect the first element of each of the (infinitely many) lists in order to decide which is the smallest. Of course, this doesn't take advantage of the fact that the lists are guaranteed to be ordered in nondecreasing order of their first elements. mergeAll takes advantage of this by producing as many elements from the head of the first list as possible before merging. The fact that the lists are ordered by first element means that any elements from the first list which are less than the head of the second list are guaranteed to be less than everything else, and can be immediately produced as the beginning of the output list, without inspecting any more of the input lists. -Brent

Brent Yorgey wrote:
Kurt Hutchinson wrote:
As part of a solution I'm working on for Project Euler problem 119, I wanted to create an ordered list of all powers of all positive integers (starting with squares).
The merging can be done much more simply and efficiently (this is code I wrote when computing squarefree numbers in a blog posthttp://byorgey.wordpress.com/2007/09/01/squarefree-numbers-in-haskell/a few months ago):
-- merge two nondecreasing lists. ( # ) :: (Ord a) => [a] -> [a] -> [a] [] # ys = ys xs # [] = xs xs@(x:xt) # ys@(y:yt) | x < y = x : (xt # ys) | x > y = y : (xs # yt) | otherwise = x : (xt # yt)
-- merge an infinite list of lists, assuming that each list -- is nondecreasing and the lists occur in order of their first -- element. mergeAll :: (Ord a) => [[a]] -> [a] mergeAll ([] : zs) = mergeAll zs mergeAll (xxs@(x:xs) : yys@(y:ys) : zs) | x < y = x : mergeAll (xs : yys : zs) | otherwise = mergeAll ((xxs # yys) : zs)
Then you can simply define
powers = 1 : mergeAll powertable
I wrote some code to sum the first n powers and print the result, and compiled (using -O2) first with your method, then with mergeAll. Summing the first 7000 powers took ~8s and ~0.1s respectively, so that's a pretty big speedup. Based on seeing how the times scale, I suspect that your code is O(n^2) or something of that sort, whereas mergeAll is essentially linear, although without scrutinizing your code more closely I'm not exactly sure why that would be the case.
In principle, both Kurt's and even your mergeAll are O(n^2), but that depends on the actual distribution of the numbers in the powertable. In both cases, the optimization over a naive implementation is to increase the number of rows to be considered only if the next output came from the last row. This is ok since of the last row, only the head element was considered so far and the non-considered elements all have to be bigger than this. Kurt's code is slower because it takes the minimum over _all_ the considered rows at every step. This is unnecessary since only one element changed, many comparisons can be "cached". In other words, this calls for a heap. Brent's code does not produce a heap, but I it's still able to cache lots of comparisons. Kurt may want to transpose the powertable to 2^2 3^2 4^2 5^2 .. 2^3 3^3 4^3 5^3 .. 2^4 3^4 4^4 .. .. instead of the current 2^2 2^3 2^4 2^5 .. 3^2 3^3 3^4 3^5 .. 4^2 4^3 4^4 .. .. since the first elements of the rows of the former grows far steeper than the ones of the latter. This means that only few rows are to be considered each turn. However, Brent may not want to transpose the powertable since the steep increase in every single row (as opposed to across rows) is exactly what makes his code fast. During evaluation, his tower of calls to # will compare something like the following numbers: 2^5 3^4 4^3 5^2 Thanks the form of the tower, the comparisons of the first elements are "cached". It looks like mergeAll $ (2^5:(__ # 3^4:__) # 4^3:__) : (5^2:xs) : __ The winner is 5^2 and mergeAll will proceeds by expecting the head of xs . But this one will first be compared to 2^5 = minimum [2^5,3^4,4^3] that's what I mean with "cached". Similarly, the winner 3^4 = minimum [2^5,3^4] is cached, too. In other words, the minimum of every initial segment of the numbers considered is "cached". The crucial point now is that those initial numbers quickly become very large (2^7 jumps exponentially to 2^8 and so on) and the winners are mostly to be found at the end of the list. With the caching, this is cheap to check. If Brent were to transpose the powertable , winners are more to the front of the list and the caching is useless, most likely rendering his implementation inferior to Kurt's one. Now, back to the heap and O(n*log n). There is no need to use an explicit heap data structure, it's possible to arrange the merges to form an implicit heap, i.e. using the best form of "caching". Here's an explanation of the technique with mergesort http://www.mail-archive.com/haskell@haskell.org/msg19980.html (Hm, does gmane gradually forget old messages?). The only problem is to make this work on an infinite list. Dave Bayer discovered a great way to do this, here's an explanation http://thread.gmane.org/gmane.comp.lang.haskell.cafe/26426/focus=26493 Regards, apfelmus

apfelmus, does someone pay you to write so many thorough, insightful and well-explained analyses on haskell-cafe? I'm guessing the answer is 'no', but clearly someone should! =) thanks! -Brent

byorgey:
apfelmus, does someone pay you to write so many thorough, insightful and well-explained analyses on haskell-cafe? I'm guessing the answer is 'no', but clearly someone should! =)
Having met apfelmus last month in Freiburg I can inform the cafe that he is thorough and insightful not just in writing, but in person too :) -- Don

On 11/14/2007 03:19 PM,, Brent Yorgey wrote:
apfelmus, does someone pay you to write so many thorough, insightful and well-explained analyses on haskell-cafe? I'm guessing the answer is 'no', but clearly someone should! =)
Agreed. I really look forward to apfelmus' consistently outstanding explanations on haskell-cafe. If some of the especially good ones were bundled up as book -- *Intermediate/Advanced Functional Programming with Haskell* -- I would buy it sight unseen (hint, hint). -calvin

Calvin Smith wrote:
I really look forward to apfelmus' consistently outstanding explanations on haskell-cafe.
If some of the especially good ones were bundled up as book -- *Intermediate/Advanced Functional Programming with Haskell* -- I would buy it sight unseen (hint, hint).
:") I intend the Haskell wikibook http://en.wikibooks.org/wiki/Haskell to be(come) "the one" Beginner/Intermediate/Advanced Functional Programming book and the mailing list can also be seen as a marvelous source of materials, like "real world" questions, problems, techniques etc for that. The wikibook hasn't gained much momentum yet, but I guess that's also partly to the fact that writing a good tutorial is time consuming and harder than I imagined, mailing list rants are far easier :) Regards, apfelmus

Brent Yorgey wrote:
apfelmus, does someone pay you to write so many thorough, insightful and well-explained analyses on haskell-cafe? I'm guessing the answer is 'no', but clearly someone should! =)
Depending on length, my prices for posts range between λ9.99 and λ29.99 ;) Regards, apfelmus

On Wed, 14 Nov 2007, Kurt Hutchinson wrote:
As part of a solution I'm working on for Project Euler problem 119, I wanted to create an ordered list of all powers of all positive integers (starting with squares). This is what I came up with:
powers = ( uniq . map fst . iterate next ) ( 1, ( 0, powertable ) )
powertable = map (\ n -> map (\ p -> n ^ p ) [ 2 .. ] ) [ 2 .. ]
iterate (n*) (n^2) should be much more efficient.
participants (6)
-
apfelmus
-
Brent Yorgey
-
Calvin Smith
-
Don Stewart
-
Henning Thielemann
-
Kurt Hutchinson