Background
Saw this problem on TheJobOverflow which seems to be a LeetCode question.
It bothered me that the "challenge" of this problem was to recognize the need for a specific data-type (in this case a MaxHeap
/PriorityQueue
).
I did another, more complicated solution, that uses folding
Problem
You are given a list of stones' weights (positive integer numbers);
While there are at least two stones in the list, take the two heaviest stones and smash them together;
if the weights were equal, both stones are destroyed;
otherwise the lighter stone gets destroyed and the heavier one becomes the difference of the two values.
You know need to consider this "remainder" stone as well as all the other stones
The process ends when there is one or no stones in the list, and the result is either the last remaining stone, or zero if there are no stones left.
Notes
The main problem that you need to solve for here is the fact that, at all times, you need perfect knowledge/information on the state of the list. You need to know EXACTLY which two stones are CURRENTLY the two heaviest. WHILE adding to the list of known stones.
As you smash stones you will inevitably be left with a remainder stone that you need to put somewhere. The new stone might now be the heaviest or the lightest, you don't know yet and you now need to keep track of it.
The Reference Solution (written in CPP) uses a
MaxHeap
/PriorityQueue
as it is the most efficient way to keep the maximum sized stone (integer) in a convenient position while being able to add to the structure in efficient time.
Personal Goals
- Attempt to not use special data types outside of base lib.
(There is a Heap package on Hackage, but can we do it without it?) - Attempt Idiomatic Haskell
(nothing too fancy) - Attempt a "naive" approach
(could a beginner stumble upon this with a rudimentary grasp of Haskell?) - Should be performant
(probably won't be better than the cpp implementation but should be about as fast and show the same time complexity
The Code
naiveLastStone :: (Num a, Ord a) => [a] -> a
naiveLastStone = proccess . revSort
where
revCompare = flip compare
revSort = sortBy revCompare
revInsert = insertBy revCompare
proccess [] = 0
proccess [x] = x
proccess (x:y:ls)
| n == 0 = proccess ls
| otherwise = proccess $ revInsert n ls
where n = x - y
Explanation
Init
naiveLastStone :: (Num a, Ord a) => [a] -> a
naiveLastStone = proccess . revSort
compose the process
with the reverse sort
we process the list in order, heaviest to smallest
Helpers
revCompare = flip compare
revSort = sortBy revCompare
revInsert = insertBy revCompare
idiomatic acceding sort
and insert
process
first the base cases: empty list means return 0 and one stone left means return it.
proccess [] = 0
proccess [x] = x
then the actual processing of the list
| n == 0 = proccess ls
| otherwise = proccess $ revInsert n ls
where n = x - y
- We are operating on the presumption that the
list
is ordered, large to small. The first 2 items will be the largest, and the second largest. (we need to keep the list sorted as we work with it) - If
remainder
is 0, the rocks were destroyed and we process the next rocks | else - The
remainder
rock exists, we now need to put it into the the list in the correct position.
We useinsert
(using our accedingrevInsert
) as it has the property of keeping a sorted list, sorted. Our list is now in the correct order for the next iteration.
Performance
Checked against TheJobOverflow cpp solution
code | time | array size |
---|---|---|
cpp (-O3) | ~0.8s | 10 000 000 |
cpp (-O3) | ~2.5s | 20 000 000 |
cpp (-O3) | ~3.8s | 40 000 000 |
cpp (-O3) | ~8.0s | 80 000 000 |
cpp (-O3) | ~10.0s | 100 000 000 |
cpp (-O3) | ~30.0s | 800 000 000 |
haskell (-O2) | ~0.5s | 10000 |
haskell (-O2) | ~2.5 | 20000 |
haskell (-O2) | ~11.3s | 40000 |
haskell (-O2) | ~60.3s | 80000 |
Sadly my solution is showing O(n^2) as apposed to the O(n log n) of the cpp solution.
Personal Goals Met
- Yes
- Yes
- Yes
- No^2
process 0 [10,9,8,3,2] == 4
but it should be[10-9,8,3,2] -> [8-3,2,1] -> [5-2,1] -> [3-1] -> 2
. \$\endgroup\$