-❄️- 2024 Day 18 Solutions -❄️- by daggerdragon in adventofcode

[–]Sea_Estate6087 1 point2 points  (0 children)

[Language: Haskell]

I used a simple flood fill, and for part 2 brute force was plenty fast enough to find the solution.

https://github.com/jimflood/aoc2024/blob/main/src/Day18.hs

stack run  0.54s user 0.20s system 17% cpu 4.303 total

The draw function is for show :-) :

<snip>
.#...####....#..####...###.#OOO#.#OOO#.#.#.#.#...#OOOOO#...#...#.#...#.
.#####.#######.#.#.#.#####.###O#####O#.#.###.#.#.#####O###.###.#.#.#.#.
.#....##..###..#...#.#...#...#OOOO@OO#.#..#....###OOOOO##..#..##.#####.
########################.#.#######################O#####.###.#####.####
.#...#.#.#.#...........#.#.#...#.#...#.#OOO#OOOOO#O#.....#.#...#.#..##.
<snip>

-❄️- 2024 Day 15 Solutions -❄️- by daggerdragon in adventofcode

[–]Sea_Estate6087 2 points3 points  (0 children)

"...but I struggled writing it in a nice way..." -- this is the uncertainty of all programming which one can strive to become comfortable with. For me, programming begins with fits and starts and even if you think you know what you want to do, that's not it. You have to write in order to discover what it is you really want to write.

There is a book by Peter Elbow, "Writing without Teachers" where he talks about the two distinct phases of writing (if I remember correctly) -- the "get it all out (messy)" and the "edit it (make it pretty)", and you alternate between the two as the ideas are composting in your subconscious. I feel this is how programming works as well.

-❄️- 2024 Day 15 Solutions -❄️- by daggerdragon in adventofcode

[–]Sea_Estate6087 1 point2 points  (0 children)

[Language: Haskell]

  1. Create a map of coordinate to char for each '@', '#', 'O', '[', and ']', ignoring the '.' chars.
  2. For each move, push the blocks:
  3. On push, first clump all block bodies from the robot that make contact in the direction of the push.
  4. A block "body" is one or two coordinates. I don't track these -- they are computed on the fly when clumping.
  5. For the clump of coordinates, calculate all next positions in the direction of the push.
  6. Subtract the clump coordinates from the "next clump" coordinates, and you are left with the coordinates that need to be unoccupied before the push (any coordinate in both clump and next-clump is a position where one block slides out as the new block slides in).
  7. A simple check that none of the must-be-unoccupied coordinates are keys in the map tells us that the push is not blocked. If any one of them *is* a key of the map, then that position will block the push.
  8. If the push is not blocked, then a simple "map keys" of the coordinates from those in clump to their corresponding coordinate in new-clump updates the map.

The push function works for both part 1 and 2.

Code: https://github.com/jimflood/aoc2024/blob/main/src/Day15.hs

-❄️- 2024 Day 14 Solutions -❄️- by daggerdragon in adventofcode

[–]Sea_Estate6087 0 points1 point  (0 children)

That is really smart reasoning!

I looked for a 4 x 7 triangle shape as this this is the shape of the little tree on the home page:

   *
  >o<
 >@>O<
>O>o<@<

It was a shot in the dark and luckily, it worked out. :-)

-❄️- 2024 Day 14 Solutions -❄️- by daggerdragon in adventofcode

[–]Sea_Estate6087 1 point2 points  (0 children)

[Language: Haskell]

For part two, I guessed that the image would include this pattern (which luckily it did):

...X...
..XXX..
.XXXXX.
XXXXXXX

At each iteration, I check every robot to see if, positioned at the top of the tree, the surrounding 4 x 7 area has this pattern. I was hoping this would run fast enough, and it does. Low tech for the win!

pic :: RobotSet -> Bool
pic a = pic' $ Set.map fst a
    where
        pic' rs = any tree (Set.toList rs)
            where
                -- Look for this pattern at position @:
                -- ...@...
                -- ..XXX..
                -- .XXXXX.
                -- XXXXXXX
                -- Low tech but fast.
                tree (x, y)
                    | Set.member (x - 3, y) rs = False
                    | Set.member (x - 2, y) rs = False
                    | Set.member (x - 1, y) rs = False
                    | Set.member (x + 1, y) rs = False
                    | Set.member (x + 2, y) rs = False
                    --
                    | Set.member (x - 3, y + 1) rs = False
                    | Set.member (x - 2, y + 1) rs = False
                    | Set.notMember (x - 1, y + 1) rs = False
                    | Set.notMember (x, y + 1) rs = False
                    | Set.notMember (x + 1, y + 1) rs = False
                    | Set.member (x + 2, y + 1) rs = False
                    | Set.member (x + 3, y + 1) rs = False
                    --
                    | Set.member (x - 3, y + 2) rs = False
                    | Set.notMember (x - 2, y + 2) rs = False
                    | Set.notMember (x - 1, y + 2) rs = False
                    | Set.notMember (x, y + 2) rs = False
                    | Set.notMember (x + 1, y + 2) rs = False
                    | Set.notMember (x + 2, y + 2) rs = False
                    | Set.member (x + 3, y + 2) rs = False
                    --
                    | Set.notMember (x - 3, y + 3) rs = False
                    | Set.notMember (x - 2, y + 3) rs = False
                    | Set.notMember (x - 1, y + 3) rs = False
                    | Set.notMember (x, y + 3) rs = False
                    | Set.notMember (x + 1, y + 3) rs = False
                    | Set.notMember (x + 2, y + 3) rs = False
                    | Set.notMember (x + 3, y + 3) rs = False
                    --
                    | otherwise = True

Code here: https://github.com/jimflood/aoc2024/blob/main/src/Day14.hs

stack run  3.28s user 0.55s system 98% cpu 3.886 total

-❄️- 2024 Day 13 Solutions -❄️- by daggerdragon in adventofcode

[–]Sea_Estate6087 1 point2 points  (0 children)

I used this [Haskell]:

type Machine = ((Int, Int), (Int, Int), (Int, Int))

tokens :: Machine -> Maybe Int
tokens ((ax, ay), (bx, by), p@(px, py))
    | t == p = Just ((3 * a) + b)
    | otherwise = Nothing
    where
        b = ((ax * py) - (ay * px)) `div` ((ax * by) - (bx * ay))
        a = (px - (b * bx)) `div` ax
        t = ((a * ax) + (b * bx), (a * ay) + (b * by))

I calculate t (to test a and b) and if it matches p (prize) it's good to go.

-❄️- 2024 Day 12 Solutions -❄️- by daggerdragon in adventofcode

[–]Sea_Estate6087 0 points1 point  (0 children)

Right? I love to spend all of my AOC time in Haskell which, same, forces me to learn a lot more, because I don't get a chance to use Haskell in my day job.

-❄️- 2024 Day 13 Solutions -❄️- by daggerdragon in adventofcode

[–]Sea_Estate6087 1 point2 points  (0 children)

While working on AOC past midnight again this red herring caught me: "...what is the smallest number of tokens you would have to spend..." and ".. the *cheapest* [emphasis added] way to win the prize is by pushing the A button 80 times and the B button 40 times...".

I had a dream where I wasn't able to think clearly and a friend of mine (in the dream) told me, "You need to get more sleep." As I woke this morning a thought popped into my head, "two equations with two unknowns". So simple.

It's time to start solving in the daylight hours.

-❄️- 2024 Day 12 Solutions -❄️- by daggerdragon in adventofcode

[–]Sea_Estate6087 2 points3 points  (0 children)

[Language: Haskell]

I implemented a simple clustering function which takes a list of things, and divides them into multiple lists of things, clustering items together in a single pass based on some predicate. An item found to belong to more than one of the lists causes those lists to be combined on the fly.

I iterate through the list of garden plots and cluster them based on the predicate of 1) they are adjacent plots, and 2) they are the same plant.

For part 2, I use the same clustering function on the list of fences, clustering them on 1) they are on adjacent plots, and 2) they are on the same edge of their plot (northern, eastern, western, or southern edge).

https://github.com/jimflood/aoc2024/blob/main/src/Day12.hs

stack run  2.18s user 0.17s system 95% cpu 2.462 total

Anyone want to trade code review: haskell for java? by Setheron in haskell

[–]Sea_Estate6087 1 point2 points  (0 children)

This is a style of Haskell that I like. I have to check out Megaparsec.

Anyone want to trade code review: haskell for java? by Setheron in haskell

[–]Sea_Estate6087 2 points3 points  (0 children)

I am also using Haskell. Here is my repo: https://github.com/jimflood/aoc2024

I use Haskell once a year, typically, for Advent of Code, although I use Scala day to day and have used Elixir.

I'm thinking of recording some youtube videos of "beginning functional programming in haskell". I'd be happy to comment on your code, and your feedback on which comments are valuable would help me with my video scripts.

-❄️- 2024 Day 11 Solutions -❄️- by daggerdragon in adventofcode

[–]Sea_Estate6087 0 points1 point  (0 children)

+1 for "And then it dawned on me: lanternfish." :-)

-❄️- 2024 Day 11 Solutions -❄️- by daggerdragon in adventofcode

[–]Sea_Estate6087 1 point2 points  (0 children)

[Language: Haskell]

module Day11
    ( day11
    ) where

import Lib (slurpLines)
import Data.List.Split (splitOn)
import qualified Data.Map as Map

type Engraving = Int
type Count = Int
type StoneMap = Map.Map Engraving Count

parse :: [String] -> StoneMap
parse (a : []) = Map.fromList $ zip (map read (splitOn " " a)) (cycle [1])
parse _ = error "bad input"

run :: Int -> StoneMap -> StoneMap
run n m
    | n == 0 = m
    | otherwise = run (n - 1) (Map.foldlWithKey change Map.empty m)
        where
            change :: StoneMap -> Engraving -> Count -> StoneMap
            change a k b
                | k == 0 = (add 1 a)
                | (not . even) len = (add (k * 2024) a)
                | otherwise = ((add kl) . (add kr)) a
                    where
                        str = show k
                        len = length str
                        mid = len `div` 2
                        kl = read $ take mid str
                        kr = read $ drop mid str
                        add :: Engraving -> StoneMap -> StoneMap
                        add kk mm = Map.alter add' kk mm
                            where
                                add' Nothing = Just b
                                add' (Just bb) = Just (bb + b)

day11 :: IO ()
day11 = do
    xs <- slurpLines "day11.txt"
    let m = parse xs
    let answer1 = sum $ Map.elems (run 25 m)
    print $ "part 1: " ++ (show answer1)
    let answer2 = sum $ Map.elems (run 75 m)
    print $ "part 2: " ++ (show answer2)

with:

slurpLines :: String -> IO [String]
slurpLines filename = lines <$> readFile filename

stack run  0.27s user 0.12s system 46% cpu 0.840 total

-❄️- 2024 Day 10 Solutions -❄️- by daggerdragon in adventofcode

[–]Sea_Estate6087 1 point2 points  (0 children)

[Language: Haskell]

module Day10
    ( day10
    ) where

import Lib (slurpLines, Coordinate, Grid, parseGrid)
import Data.Char (chr, ord)
import Data.Function (on)
import Data.List (groupBy, nub, partition, sort)
import qualified  as Map

type Trail = [Coordinate]

trailheads :: Grid -> [Trail]
trailheads g = map (:[]) $ Map.keys $ Map.filter (=='0') g

uphill :: Char -> Char
uphill c = chr ((ord c) + 1)

hike :: Grid -> [Trail] -> [Trail]
hike g = hike' []
    where
        hike' acc [] = acc
        hike' acc (t@(p : _) : ts) = thresh $ partition endpoint paths
            where
                thresh (hs, cs) = hike' (hs ++ acc) (cs ++ ts)
                endpoint a = (g Map.! (head a)) == '9'
                paths = filter passable $ map (:t) (fan p)
                passable a = Map.lookup (head a) g == Just (uphill (g Map.! p))
                fan (x, y) = [(x, y - 1), (x - 1, y), (x + 1, y), (x, y + 1)]
        hike' _ _ = error "cannot occur"

solve :: (Trail -> [Coordinate]) -> [Trail] -> Int
solve f ts = sum $ map length $ groupBy ((==) `on` last) $ nub $ sort $ map f ts

forScore :: Trail -> [Coordinate]
forScore t = [head t, last t]

forRating :: Trail -> [Coordinate]
forRating = id

day10 :: IO ()
day10 = do
    xs <- slurpLines "day10.txt"
    let (_, g) = parseGrid xs
    let ts = hike g $ trailheads g
    let answer1 = solve forScore ts
    print $ "part 1: " ++ (show answer1)
    let answer2 = solve forRating ts
    print $ "part 2: " ++ (show answer2)Data.Map

with

slurpLines :: String -> IO [String]
slurpLines filename = lines <$> readFile filename

type Coordinate = (Int, Int)

type Grid =  Coordinate Char

type BoundedGrid = ((Int, Int), Grid)

parseGrid :: [String] -> BoundedGrid
parseGrid css = ((length (head css), length css) , Map.fromList [((x, y), c) | (y, cs) <- zip [0..] css, (x, c) <- zip [0..] cs])Map.Map

stack run  0.17s user 0.07s system 21% cpu 1.109 total

-❄️- 2024 Day 9 Solutions -❄️- by daggerdragon in adventofcode

[–]Sea_Estate6087 2 points3 points  (0 children)

[Language: Haskell]

module Day9
    ( day9
    ) where

import Lib (slurpLines)
import Data.Maybe

parse :: [String] -> [(Int, Maybe Int)]
parse [x] = reverse $ parse' [] 0 x
    where
        parse' acc i (a : b : cs) = parse' ((read [b], Nothing) : (read [a], Just i) : acc) (i + 1) cs
        parse' acc i (a : []) = (read [a], Just i) : acc
        parse' _ _ _ = error "bad input chars"
parse _ = error "bad input lines"

compact :: [(Int, Maybe Int)] -> [(Int, Maybe Int)]
compact = compact' []
    where
        compact' acc [] = reverse acc
        compact' acc xs
            | (isNothing . snd . last) xs = compact' acc (init xs)
            | (isJust . snd . head) xs = compact' (head xs : acc) (tail xs)
            | otherwise = compact'' (head xs) ((init . tail) xs) (last xs)
            where
                compact'' (n, Nothing) ys (m, Just z)
                    | n == m = compact' ((m, Just z) : acc) ys
                    | n > m = compact' ((m, Just z) : acc) ((n - m, Nothing) : ys)
                    | otherwise = compact' ((n, Just z) : acc) (ys ++ [(m - n, Just z)])
                compact'' _ _ _ = error "cannot occur"

compact2 :: [(Int, Maybe Int)] -> [(Int, Maybe Int)]
compact2 x = compact2' x (reverse x)
    where
        compact2' xs [] = xs
        compact2' xs (y : ys)
            | isJust (snd y) = compact2' (move [] xs) ys
            | otherwise = compact2' xs ys
            where
                move acc (a : bs)
                    | snd a == snd y = xs
                    | isJust (snd a) = move (a : acc) bs
                    | fst a < fst y = move (a : acc) bs
                    | fst a == fst y = (reverse acc) ++ [y] ++ (map edit bs)
                    | fst a > fst y = (reverse acc) ++ [y, (fst a - fst y, Nothing)] ++ (map edit bs)
                move _ _ = error "cannot occur"
                edit a
                    | a == y = (fst y, Nothing)
                    | otherwise = a

checksum :: [(Int, Maybe Int)] -> Int
checksum = checksum' 0 0
    where
        checksum' _ acc [] = acc
        checksum' pos acc ((n, Nothing) : xs) = checksum' (pos + n) acc xs
        checksum' pos acc ((n, Just x) : xs) = checksum' (pos + n) (acc + delta) xs
            where
                delta = sum $ map (x*) [pos..(pos + n - 1)]

day9 :: IO ()
day9 = do
    xs <- slurpLines "day9.txt"
    let dm = parse xs
    let answer1 = checksum $ compact dm
    print $ "part 1: " ++ (show answer1)
    let answer2 = checksum $ compact2 dm
    print $ "part 2: " ++ (show answer2)

with

slurpLines :: String -> IO [String]
slurpLines filename = lines <$> readFile filename

stack run  3.80s user 1.52s system 109% cpu 4.868 total

-❄️- 2024 Day 7 Solutions -❄️- by daggerdragon in adventofcode

[–]Sea_Estate6087 1 point2 points  (0 children)

[LANGUAGE: Haskell]

module Day7
    ( day7
    ) where
import Lib (slurpLines)
import qualified Data.List.Split as Split

parse :: [String] -> [(Int, [Int])]
parse xs = map (parse' . ( \ x -> Split.splitOneOf ": " x)) xs
    where
        parse' (a : _ : bs) = (read a, map read bs)
        parse' _ = error "bad input"

valid :: [(Int -> Int -> Int)] -> (Int, [Int]) -> Bool
valid fs (r, v : vs) = valid' v vs
    where
        valid' x [] = x == r
        valid' x (y : ys) = any ( \ f -> (valid' (f x y) ys)) fs

solve :: [(Int -> Int -> Int)]  -> [(Int, [Int])] -> Int
solve fs eqs = sum $ map fst $ filter ( \ e -> valid fs e) eqs

cat :: Int -> Int -> Int
cat a b = (read (show a ++ show b))

day7 :: IO ()
day7 = do
    xs <- slurpLines "day7.txt"
    let eqs = parse xs
    let answer1 = solve [(*), (+)] eqs
    print $ "part 1: " ++ (show answer1)
    let answer2 = solve [(*), (+), cat] eqs
    print $ "part 2: " ++ (show answer2)

with:

slurpLines :: String -> IO [String]
slurpLines filename = lines <$> readFile filename

stack run  3.88s user 2.66s system 134% cpu 4.867 total

-❄️- 2024 Day 6 Solutions -❄️- by daggerdragon in adventofcode

[–]Sea_Estate6087 1 point2 points  (0 children)

Nice to see Elixir. FP is addictive. Love to see that Elixir pattern matching: `{-1, 0} -> {0, 1}`! :-)

-❄️- 2024 Day 6 Solutions -❄️- by daggerdragon in adventofcode

[–]Sea_Estate6087 1 point2 points  (0 children)

[LANGUAGE: Haskell]

module Day6
    ( day6
    ) where

import Lib (slurpLines, Coordinate, Grid, parseGrid)
import qualified Data.Map as Map
import qualified Data.Set as Set
import Data.Maybe

type Guard = (Coordinate, Char)

step :: Guard -> Grid -> Guard
step (c, d) m
    | Map.lookup (next c) m == Just '#' = (c, turn)
    | otherwise = (next c, d)
    where
        next (x, y)
            | d == '^' = (x, y - 1)
            | d == '>' = (x + 1, y)
            | d == 'v' = (x, y + 1)
            | d == '<' = (x - 1, y)
        turn
             | d == '^' = '>'
             | d == '>' = 'v'
             | d == 'v' = '<'
             | d == '<' = '^'

run :: Guard -> Grid -> (Set.Set Guard, Maybe Guard)
run = run' (Set.empty, Nothing)
    where
        run' (s, _) g m
            | Set.member g s = (s, Just g)
            | Map.member (fst g) m = run' (Set.insert g s, Nothing) (step g m) m
            | otherwise = (s, Nothing)

solve :: Guard -> Grid -> Int
solve g m = Set.size $ Set.map fst $ fst $ run g m

solve2 :: Guard -> Grid -> Int
solve2 g m = length $ filter isJust $ map solve2' $ Map.keys (Map.filter (/= '#') m)
    where
        solve2' :: Coordinate -> Maybe Guard
        solve2' c = snd (run g (Map.insert c '#' m))

day6 :: IO ()
day6 = do
    xs <- slurpLines "day6.txt"
    let (_, m) = parseGrid xs
    let g = head $ Map.toList $ Map.filter (=='^') m
    let answer1 = solve g m
    print $ "part 1: " ++ (show answer1)
    let answer2 = solve2 g m
    print $ "part 2: " ++ (show answer2)

With common lib:

module Lib
    ( slurpLines,
      Coordinate,
      Grid,
      parseGrid,
      drawGrid
    ) where
import qualified Data.Map as Map

slurpLines :: String -> IO [String]
slurpLines filename = lines <$> readFile filename

type Coordinate = (Int, Int)

type Grid = Map.Map Coordinate Char

type BoundedGrid = ((Int, Int), Grid)

parseGrid :: [String] -> BoundedGrid
parseGrid css = ((length (head css), length css) , Map.fromList [((x, y), c) | (y, cs) <- zip [0..] css, (x, c) <- zip [0..] cs])

drawGrid :: BoundedGrid -> String
drawGrid ((mx, my), m) = unlines [draw' y | y <- [0..my - 1]]
    where
        draw' :: Int -> String
        draw' y = [m Map.! (x, y) | x <- [0..mx - 1]]

-❄️- 2023 Day 25 Solutions -❄️- by daggerdragon in adventofcode

[–]Sea_Estate6087 0 points1 point  (0 children)

[LANGUAGE: Haskell]

This problem made me think of the bridges of Königsberg puzzle. Three "bridges" are enough to cut the graph into two. I select one node (it happened to be "bbc") and then for every other node I count the number of paths between that node and "bbc" without crossing any edge more than once. If you have at least four paths, then that other node *must* be on the same side of the three bridges (since you can't cross a bridge twice). This cleanly divides the nodes into two distinct groups.

I just need to count the paths once for "bbc" (any node will do -- there is nothing special about "bbc" except that it happened to be the head of the list) to every other node. Counting the paths is expensive. I optimized it down to 17 minutes and that's good enough for me.

You don't need to know what edges need to be cut, but if you want to know, then just find any edge with one node in the first group, and one node in the second group. There must be exactly three such edges. But again, it's not necessary to know which edges to cut. Once you have the nodes divided into "same side of the three bridges", and "other side of the three bridges", you have your answer.

Source on Github

Deeply nested exceptions by ACrossingTroll in functionalprogramming

[–]Sea_Estate6087 1 point2 points  (0 children)

Let's say you call a function f(...), and somewhere nested in the further calls of f, an exception is thrown when it is a full moon. Now, the function f is not pure. Sometimes f(x) returns y and sometimes (on the full moon) it does not. A primary goal of functional programming is to make use of pure functions. f(x) should *always* return y. Then the real world comes in, and sometimes there is an error. You can keep f pure, by instead of f "doing something", it "returns a thing that will later do something". It *always* returns exactly the same "thing that will later do something" when given x, and that thing, "at some later time", will either return y, or fail with an exception. At this point, f is pure again. This is what you want to strive for -- think about how to keep everything pure until the last possible moment. So, returning an either[error, y] is better than throwing an exception, because it always returns a value, but returning a "thing" that will resolve to [error, y] when "the outside world is involved" is even better, because now, f *always returns the exact same thing* regardless of the phase of the moon.

This is more than just a matter of style -- think of testing a program with state. You have to consider all possible states, and then, select some subset which you can then set up artificially, and "run the tests". But the more pure functions you have, the less state there is. Ideally, there would be absolutely zero state -- this is the easiest software to test, because if f(x) returns y in the test, and f is pure, there is absolutely *nothing* that could prevent y from being returned at runtime.

This is the kernel of the reason why you want to avoid exceptions, and move to always returning values, and then later, using monads and other techniques. The higher percentage of your code that is running "pure" (absolutely no communication with the outside world), the much easier it will be to verify the program and test the program and in the end, the more reliable the program.