Thank you for showing the floodfill-algorithm using explored/open sets, mine was hellish inefficiently, reminds me of A*.
Thank you for showing the floodfill-algorithm using explored/open sets, mine was hellish inefficiently, reminds me of A*.
Detecting regions is a floodfill. For Part 2, I select all adjacent tiles that are not part of a region and group them by the direction relative to the closest region tile, then group adjacent tiles with the same direction again and count.
Edit:
Takes 0.06s
import Control.Arrow
import Data.Array.Unboxed (UArray)
import Data.Set (Set)
import Data.Map (Map)
import qualified Data.List as List
import qualified Data.Set as Set
import qualified Data.Map as Map
import qualified Data.Array.Unboxed as UArray
parse :: String -> UArray (Int, Int) Char
parse s = UArray.listArray ((1, 1), (n, m)) . filter (/= '\n') $ s
where
n = takeWhile (/= '\n') >>> length $ s
m = filter (== '\n') >>> length >>> pred $ s
neighborCoordinates (p1, p2) = [(p1-1, p2), (p1, p2-1), (p1, p2+1), (p1+1, p2)]
allNeighbors p a = neighborCoordinates
>>> filter (UArray.inRange (UArray.bounds a))
$ p
regionNeighbors p a = allNeighbors p
>>> filter ((a UArray.!) >>> (== pTile))
$ a
where
pTile = a UArray.! p
floodArea :: Set (Int, Int) -> Set (Int, Int) -> UArray (Int, Int) Char -> Set (Int, Int)
floodArea e o a
| Set.null o = e
| otherwise = floodArea e' o' a
where
e' = Set.union e o
o' = Set.fold (Set.union . Set.fromDistinctAscList . (filter (`Set.notMember` e')) . (flip regionNeighbors a)) Set.empty o
findRegions garden = findRegions' (Set.fromList . UArray.indices $ garden) garden
findRegions' remainingIndices garden
| Set.null remainingIndices = []
| otherwise = removedIndices : findRegions' remainingIndices' garden
where
removedIndices = floodArea Set.empty (Set.singleton . Set.findMin $ remainingIndices) garden
remainingIndices' = Set.difference remainingIndices removedIndices
perimeter region = Set.fold ((+) . length . filter (`Set.notMember` region) . neighborCoordinates) 0 region
part1 rs = map (Set.size &&& perimeter)
>>> map (uncurry (*))
>>> sum
$ rs
turnLeft ( 0, 1) = (-1, 0) -- right
turnLeft ( 0,-1) = ( 1, 0) -- left
turnLeft ( 1, 0) = ( 0, 1) -- down
turnLeft (-1, 0) = ( 0,-1) -- up
turnRight = turnLeft . turnLeft . turnLeft
move (py, px) (dy, dx) = (py + dy, px + dx)
tupleDelta (y1, x1) (y2, x2) = (y1-y2, x1-x2)
isRegionInner region p = all (`Set.member` region) (neighborCoordinates p)
groupEdges d ps
| Set.null ps = []
| otherwise = collectedEdge : groupEdges d ps'
where
ps' = Set.difference ps collectedEdge
collectedEdge = Set.union leftPoints rightPoints
leftPoints = iterate (move dl)
>>> takeWhile (`Set.member` ps)
>>> Set.fromList
$ currentPoint
rightPoints = iterate (move dr)
>>> takeWhile (`Set.member` ps)
>>> Set.fromList
$ currentPoint
currentPoint = Set.findMin ps
dr = turnRight d
dl = turnLeft d
linearPerimeter region = Map.foldr ((+) . length) 0 $ groupedEdges
where
edgeTiles = Set.filter (not . isRegionInner region) region
regionNeighbors = List.concatMap (\ p -> map (p,). filter (`Set.notMember` region) . neighborCoordinates $ p) . Set.toList $ region
groupedNeighbors = List.map (uncurry tupleDelta &&& Set.singleton . snd)
>>> Map.fromListWith (Set.union)
$ regionNeighbors
groupedEdges = Map.mapWithKey groupEdges
$ groupedNeighbors
part2 rs = map (Set.size &&& linearPerimeter)
>>> map (uncurry (*))
>>> sum
$ rs
main = getContents
>>= print
. (part1 &&& part2)
. findRegions
. parse
Thank you for the hint, I wouldn’t have recognized it because I haven’t yet looked into it, I might try it this afternoon if I find the time, I could probably put both the Cache and the current stone count into the monad state?
Does the IORef go upwards the recursion tree? If you modify the IORef at some depth of 15, does the calling function also receive the update, is there also a Non-IO-Ref?
Sometimes I want something mutable, this one takes 0.3s, profiling tells me 30% of my time is spent creating new objects. :/
import Control.Arrow
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import qualified Data.Maybe as Maybe
type StoneCache = Map Int Int
type BlinkCache = Map Int StoneCache
parse :: String -> [Int]
parse = lines >>> head >>> words >>> map read
memoizedCountSplitStones :: BlinkCache -> Int -> Int -> (Int, BlinkCache)
memoizedCountSplitStones m 0 _ = (1, m)
memoizedCountSplitStones m i n
| Maybe.isJust maybeMemoized = (Maybe.fromJust maybeMemoized, m)
| n == 0 = do
let (r, rm) = memoizedCountSplitStones m (pred i) (succ n)
let rm' = cacheWrite rm i n r
(r, rm')
| digitCount `mod` 2 == 0 = do
let (r1, m1) = memoizedCountSplitStones m (pred i) firstSplit
let (r2, m2) = memoizedCountSplitStones m1 (pred i) secondSplit
let m' = cacheWrite m2 i n (r1+r2)
(r1 + r2, m')
| otherwise = do
let (r, m') = memoizedCountSplitStones m (pred i) (n * 2024)
let m'' = cacheWrite m' i n r
(r, m'')
where
secondSplit = n `mod` (10 ^ (digitCount `div` 2))
firstSplit = (n - secondSplit) `div` (10 ^ (digitCount `div` 2))
digitCount = succ . floor . logBase 10 . fromIntegral $ n
maybeMemoized = cacheLookup m i n
foldMemoized :: Int -> (Int, BlinkCache) -> Int -> (Int, BlinkCache)
foldMemoized i (r, m) n = (r + r2, m')
where
(r2, m') = memoizedCountSplitStones m i n
cacheWrite :: BlinkCache -> Int -> Int -> Int -> BlinkCache
cacheWrite bc i n r = Map.adjust (Map.insert n r) i bc
cacheLookup :: BlinkCache -> Int -> Int -> Maybe Int
cacheLookup bc i n = do
sc <- bc Map.!? i
sc Map.!? n
emptyCache :: BlinkCache
emptyCache = Map.fromList [ (i, Map.empty) | i <- [1..75]]
part1 = foldl (foldMemoized 25) (0, emptyCache)
>>> fst
part2 = foldl (foldMemoized 75) (0, emptyCache)
>>> fst
main = getContents
>>= print
. (part1 &&& part2)
. parse
Thank you for the link, this is crazy!
Cool task, nothing to optimize
import Control.Arrow
import Data.Array.Unboxed (UArray)
import Data.Set (Set)
import qualified Data.Char as Char
import qualified Data.List as List
import qualified Data.Set as Set
import qualified Data.Array.Unboxed as UArray
parse :: String -> UArray (Int, Int) Int
parse s = UArray.listArray ((1, 1), (n, m)) . map Char.digitToInt . filter (/= '\n') $ s
where
n = takeWhile (/= '\n') >>> length $ s
m = filter (== '\n') >>> length >>> pred $ s
reachableNeighbors :: (Int, Int) -> UArray (Int, Int) Int -> [(Int, Int)]
reachableNeighbors p@(py, px) a = List.filter (UArray.inRange (UArray.bounds a))
>>> List.filter ((a UArray.!) >>> pred >>> (== (a UArray.! p)))
$ [(py-1, px), (py+1, px), (py, px-1), (py, px+1)]
distinctTrails :: (Int, Int) -> UArray (Int, Int) Int -> Int
distinctTrails p a
| a UArray.! p == 9 = 1
| otherwise = flip reachableNeighbors a
>>> List.map (flip distinctTrails a)
>>> sum
$ p
reachableNines :: (Int, Int) -> UArray (Int, Int) Int -> Set (Int, Int)
reachableNines p a
| a UArray.! p == 9 = Set.singleton p
| otherwise = flip reachableNeighbors a
>>> List.map (flip reachableNines a)
>>> Set.unions
$ p
findZeros = UArray.assocs
>>> filter (snd >>> (== 0))
>>> map fst
part1 a = findZeros
>>> map (flip reachableNines a)
>>> map Set.size
>>> sum
$ a
part2 a = findZeros
>>> map (flip distinctTrails a)
>>> sum
$ a
main = getContents
>>= print
. (part1 &&& part2)
. parse
Maths degree at least explains the choice of language
Thank you for trying, oh well. Maybe we are simply at the limits.
Trees are a poor mans Sets and vice versa .-.
I only now found your edit after I had finished my previous comment. I think splitting into two lists may be good: one List of Files and one of Empty Blocks, I think this may not work with your checksumming so maybe not.
Thank you for the detailed explanation!, it made me realize that our solutions are very similar.
Instead of keeping a Dict[Int, List[Int]]
where the value list is ordered I have a Dict[Int, Tree[Int]]
which allows for easy (and fast!) lookup due to the nature of trees. (Also lists in haskell are horrible to mutate)
I also apply the your technique of only processing each file once, instead of calculating the checksum afterwards on the entire list of file blocks I calculate it all the time whenever I process a file. Using some maths I managed to reduce the sum to a constant expression.
It will always be a wonder to me how you manage to do so much in so few lines, even your naive solution only takes a few seconds to run. 🤯
So cool, I was very hyped when I managed to squeeze out the last bit of performance, hope you are too. Especially surprised you managed it with python, even without the simple tricks like trees ;)
I wanted to try it myself, can confirm it runs in under 0.1s in performance mode on my laptop, I am amazed though I must admin I don’t understand your newest revision. 🙈
This was fun, I optimized away quite a bit, as a result it now runs in 0.04s for both parts together on my 2016 laptop.
In part 1 I just run through the array with a start- and an end-index whilst summing up the checksum the entire time.
In part 2 I build up Binary Trees of Free Space which allow me to efficiently search for and insert free spaces when I start traversing the disk from the back.
Marking the moved files as free is omitted because the checksum is calculated for every file that is moved or not moved directly.
import Control.Monad
import Data.Bifunctor
import Control.Arrow hiding (first, second)
import Data.Map (Map)
import Data.Set (Set)
import Data.Array.Unboxed (UArray)
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified Data.Ord as Ord
import qualified Data.List as List
import qualified Data.Char as Char
import qualified Data.Maybe as Maybe
import qualified Data.Array.Unboxed as UArray
toNumber = flip (-) (Char.ord '0') <<< Char.ord
type FileID = Int
type FileLength = Int
type DiskPosition = Int
type File = (FileID, (DiskPosition, FileLength))
type EmptyMap = Map FileLength (Set DiskPosition)
readDisk :: DiskPosition -> [(Bool, FileLength)] -> [(Bool, (DiskPosition, FileLength))]
readDisk _ [] = []
readDisk o ((True, l):fs) = (True, (o, l)) : readDisk (o+l) fs
readDisk o ((False, l):fs) = (False, (o, l)) : readDisk (o+l) fs
parse2 :: String -> ([File], EmptyMap)
parse2 s = takeWhile (/= '\n')
>>> map toNumber
>>> zip (cycle [True, False]) -- True is File, False is empty
>>> readDisk 0
>>> List.partition fst
>>> join bimap (map snd)
>>> first (zip [0..])
>>> first List.reverse
>>> second (filter (snd >>> (/= 0)))
>>> second (List.sortOn snd)
>>> second (List.groupBy (curry $ (snd *** snd) >>> uncurry (==)))
>>> second (List.map (snd . head &&& map fst))
>>> second (List.map (second Set.fromDistinctAscList))
>>> second Map.fromDistinctAscList
$ s
maybeMinimumBy :: (a -> a -> Ordering) -> [a] -> Maybe a
maybeMinimumBy _ [] = Nothing
maybeMinimumBy f as = Just $ List.minimumBy f as
fileChecksum fid fpos flen = fid * (fpos * flen + ((flen-1) * (flen-1) + (flen-1)) `div` 2)
type Checksum = Int
moveFilesAccumulate :: (Checksum, EmptyMap) -> File -> (Checksum, EmptyMap)
moveFilesAccumulate (check, spaces) (fid, (fpos, flen)) = do
let bestFit = Map.map (Set.minView)
>>> Map.toList
>>> List.filter (fst >>> (>= flen))
>>> List.filter (snd >>> Maybe.isJust)
>>> List.map (second Maybe.fromJust) -- [(FileLength, (DiskPosition, Set DiskPosition))]
>>> List.filter (snd >>> fst >>> (< fpos))
>>> maybeMinimumBy (\ (_, (p, _)) (_, (p', _)) -> Ord.compare p p')
$ spaces
case bestFit of
Nothing -> (check + fileChecksum fid fpos flen, spaces)
Just (spaceLength, (spacePosition, remainingSet)) -> do
-- remove the old empty entry by replacing the set
let updatedMap = Map.update (const $! Just remainingSet) spaceLength spaces
-- add the remaining space, if any
let remainingSpace = spaceLength - flen
let remainingSpacePosition = spacePosition + flen
let updatedMap' = if remainingSpace == 0 then updatedMap else Map.insertWith (Set.union) remainingSpace (Set.singleton remainingSpacePosition) updatedMap
(check + fileChecksum fid spacePosition flen, updatedMap')
parse1 :: String -> UArray Int Int
parse1 s = UArray.listArray (0, sum lengthsOnly - 1) blocks
where
lengthsOnly = filter (/= '\n')
>>> map toNumber
$ s :: [Int]
blocks = zip [0..]
>>> List.concatMap (\ (index, n) -> if index `mod` 2 == 0 then replicate n (index `div` 2) else replicate n (-1))
$ lengthsOnly :: [Int]
moveBlocksAccumulate :: Int -> Int -> UArray Int Int -> Int
moveBlocksAccumulate start stop array
| start == stop = if startBlock == -1 then 0 else start * startBlock
| start > stop = 0
| stopBlock == -1 = moveBlocksAccumulate start (stop - 1) array
| startBlock == -1 = movedChecksum + moveBlocksAccumulate (start + 1) (stop - 1) array
| startBlock /= -1 = startChecksum + moveBlocksAccumulate (start + 1) stop array
where
startBlock = array UArray.! start
stopBlock = array UArray.! stop
movedChecksum = stopBlock * start
startChecksum = startBlock * start
part1 a = moveBlocksAccumulate 0 arrayLength a
where
(_, arrayLength) = UArray.bounds a
part2 (files, spaces) = foldl moveFilesAccumulate (0, spaces)
>>> fst
$ files
main = getContents
>>= print
. (part1 . parse1 &&& part2 . parse2)
Unoptimized as hell, also brute-force approach (laptops are beasts).
{-# LANGUAGE MultiWayIf #-}
import Control.Arrow
import Control.Monad.ST (ST, runST)
import Data.Array.ST (STUArray)
import qualified Data.List as List
import qualified Data.Maybe as Maybe
import qualified Data.Array.MArray as MArray
toNumber '0' = 0
toNumber '1' = 1
toNumber '2' = 2
toNumber '3' = 3
toNumber '4' = 4
toNumber '5' = 5
toNumber '6' = 6
toNumber '7' = 7
toNumber '8' = 8
toNumber '9' = 9
parse :: String -> [Int]
parse s = filter (/= '\n')
>>> map toNumber
>>> zip [0..]
>>> List.concatMap (\ (index, n) -> if index `mod` 2 == 0 then replicate n (index `div` 2) else replicate n (-1))
$ s
calculateChecksum :: [Int] -> Int
calculateChecksum = zip [0..]
>>> filter (snd >>> (/= -1))
>>> map (uncurry (*))
>>> sum
moveFiles :: [Int] -> ST s Int
moveFiles bs = do
let bLength = length bs
marray <- MArray.newListArray (1, bLength) bs
moveFiles' marray 1 bLength
elems <- MArray.getElems marray
return $ calculateChecksum elems
moveFiles' :: STUArray s Int Int -> Int -> Int -> ST s ()
moveFiles' a start stop
| start == stop = return ()
| otherwise = do
stopBlock <- MArray.readArray a stop
if stopBlock == -1
then
moveFiles' a start (pred stop)
else
do
startBlock <- MArray.readArray a start
if startBlock == -1
then
do
MArray.writeArray a start stopBlock
MArray.writeArray a stop (-1)
moveFiles' a (succ start) (pred stop)
else
moveFiles' a (succ start) stop
countConsecutive :: STUArray s Int Int -> Int -> Int -> ST s Int
countConsecutive a i step = do
block <- MArray.readArray a i
let nextI = i + step
bounds <- MArray.getBounds a
if | MArray.inRange bounds nextI ->
do
nextBlock <- MArray.readArray a nextI
if nextBlock == block
then
do
steps <- countConsecutive a nextI step
return $ 1 + steps
else
return 1
| otherwise -> return 1
findEmpty :: STUArray s Int Int -> Int -> Int -> Int -> ST s (Maybe Int)
findEmpty a i l s = do
block <- MArray.readArray a i
blockLength <- countConsecutive a i 1
let nextI = i + blockLength
bounds <- MArray.getBounds a
let nextInBounds = MArray.inRange bounds nextI
if | i >= s -> return $! Nothing
| block == -1 && blockLength >= l -> return $ Just i
| block /= -1 && nextInBounds -> findEmpty a nextI l s
| blockLength <= l && nextInBounds -> findEmpty a nextI l s
| not nextInBounds -> return $! Nothing
moveDefragmenting :: [Int] -> ST s Int
moveDefragmenting bs = do
let bLength = length bs
marray <- MArray.newListArray (1, bLength) bs
moveDefragmenting' marray bLength
elems <- MArray.getElems marray
return $ calculateChecksum elems
moveDefragmenting' :: STUArray s Int Int -> Int -> ST s ()
moveDefragmenting' a 1 = return ()
moveDefragmenting' a stop
| otherwise = do
stopBlock <- MArray.readArray a stop
stopLength <- countConsecutive a stop (-1)
targetBlock <- findEmpty a 1 stopLength stop
elems <- MArray.getElems a
let nextStop = stop - stopLength
bounds <- MArray.getBounds a
let nextStopInRange = MArray.inRange bounds nextStop
if | stopBlock == -1
-> moveDefragmenting' a nextStop
| Maybe.isJust targetBlock
-> do
let target = Maybe.fromJust targetBlock
mapM_ (\ o -> MArray.writeArray a (stop - o) (-1)) [0..stopLength - 1]
mapM_ (\ o -> MArray.writeArray a (target + o) stopBlock) [0..stopLength - 1]
if nextStopInRange then moveDefragmenting' a nextStop else return ()
| nextStopInRange -> moveDefragmenting' a nextStop
| otherwise -> return ()
part1 bs = runST $ moveFiles bs
part2 bs = runST $ moveDefragmenting bs
main = getContents
>>= print
. (part1 &&& part2)
. parse
This is so cool, it’s going to replace the lambda in my function pipeline for calculating pairs.
Whaaat? It is possible to declare mutliple signatures on one line? 🤯
Does that function (.+.
) add tuples/coordinates?
I overslept 26 minutes (AoC starts at 06:00 here) which upsets me more than it should.
I thought this one was going to be hard on performance or memory but it was surprisingly easy.
import Control.Arrow hiding (first, second)
import Data.Bifunctor
import Data.Array.Unboxed (UArray)
import qualified Data.List as List
import qualified Data.Set as Set
import qualified Data.Array.Unboxed as Array
parse :: String -> UArray (Int, Int) Char
parse s = Array.listArray ((1, 1), (n, m)) . filter (/= '\n') $ s :: UArray (Int, Int) Char
where
n = takeWhile (/= '\n') >>> length $ s
m = List.filter (== '\n') >>> length >>> pred $ s
groupSnd:: Eq b => (a, b) -> (a', b) -> Bool
groupSnd = curry (uncurry (==) <<< snd *** snd)
cartesianProduct xs ys = [(x, y) | x <- xs, y <- ys]
calculateAntitone ((y1, x1), (y2, x2)) = (y1 + dy, x1 + dx)
where
dy = y1 - y2
dx = x1 - x2
antennaCombinations = Array.assocs
>>> List.filter (snd >>> (/= '.'))
>>> List.sortOn snd
>>> List.groupBy groupSnd
>>> map (map fst)
>>> map (\ xs -> cartesianProduct xs xs)
>>> map (filter (uncurry (/=)))
part1 a = antennaCombinations
>>> List.concatMap (map calculateAntitone)
>>> List.filter (Array.inRange (Array.bounds a))
>>> Set.fromList
>>> Set.size
$ a
calculateAntitones ((y1, x1), (y2, x2)) = iterate (bimap (+dy) (+dx)) (y1, x1)
where
dy = y1 - y2
dx = x1 - x2
part2 a = antennaCombinations
>>> List.map (map calculateAntitones)
>>> List.concatMap (List.concatMap (takeWhile (Array.inRange (Array.bounds a))))
>>> Set.fromList
>>> Set.size
$ a
main = getContents
>>= print
. (part1 &&& part2)
. parse
I had my code run all the time while I coded up the solution for the second part, needless to say, it wouldn’t finish.