├── 2019 ├── 01.hs ├── 01.md ├── 02.hs ├── 02.md ├── 03.hs ├── 03.md ├── 04.hs ├── 04.md ├── 05.hs ├── 05.md ├── 06.hs ├── 06.md ├── 07.hs ├── 07.md ├── 08.hs ├── 08.md ├── 09.hs ├── 09.md ├── 10.hs ├── 10.md ├── 11.hs ├── 11.md ├── 12.hs ├── 12.md ├── 13.hs ├── 13.md ├── 14.hs ├── 14.md ├── 15.hs ├── 15.md ├── 16.hs ├── 16.md ├── 17.hs ├── 17.md ├── 18.hs ├── 18.md ├── 19.hs ├── 19.md ├── 20.hs ├── 20.md ├── 21.hs ├── 21.md ├── 22.hs ├── 22.md ├── 23.hs ├── 23.md ├── 24.hs ├── 24.md ├── 25.hs ├── 25.md ├── 25.png ├── Intcode.hs ├── Makefile └── README.md ├── 2020 ├── 01.hs ├── 01.md ├── 02.hs ├── 02.md ├── 03.hs ├── 03.md ├── 04.hs ├── 04.md ├── 05.hs ├── 05.md ├── 06.hs ├── 06.md ├── 07.hs ├── 07.md ├── 08.hs ├── 08.md ├── 09.hs ├── 09.md ├── 10.hs ├── 10.md ├── 11.hs ├── 11.md ├── 12.hs ├── 12.md ├── 13.hs ├── 13.md ├── 14.hs ├── 14.md ├── 15.hs ├── 15.md ├── 16.hs ├── 16.md ├── 17.hs ├── 17.md ├── 18.md ├── 18.sh ├── 19.hs ├── 19.md ├── 20.hs ├── 20.md ├── 21.hs ├── 21.md ├── 22.hs ├── 22.md ├── 23.cpp ├── 23.md ├── 24.hs ├── 24.md ├── 25.hs ├── 25.md ├── Makefile └── README.md ├── 2021 ├── 01.hs ├── 01.md ├── 02.hs ├── 02.md ├── 03.hs ├── 03.md ├── 04.hs ├── 04.md ├── 05.hs ├── 05.md ├── 06.hs ├── 06.md ├── 06_roots.png ├── 07.hs ├── 07.md ├── 08.hs ├── 08.md ├── 09.hs ├── 09.md ├── 09.py ├── 10.hs ├── 10.md ├── 10.py ├── 11.hs ├── 11.md ├── 12.hs ├── 12.md ├── 13.hs ├── 13.md ├── 13.py ├── 14.hs ├── 14.md ├── 15.hs ├── 15.md ├── 15.py ├── 16.hs ├── 16.md ├── 16.py ├── 17.hs ├── 17.md ├── 17.py ├── 18.hs ├── 18.md ├── 18.py ├── 19.hs ├── 19.md ├── 20.hs ├── 20.md ├── 20.py ├── 21.hs ├── 21.md ├── 21.py ├── 22.hs ├── 22.md ├── 22a.py ├── 22b.py ├── 22c.py ├── 23.hs ├── 23.md ├── 23.py ├── 24.md ├── 24.py ├── 25.hs ├── 25.md ├── 25.py ├── Makefile └── README.md ├── 2022 ├── 01.hs ├── 01.md ├── 01.py ├── 02.hs ├── 02.md ├── 02.py ├── 03.hs ├── 03.md ├── 03.py ├── 04.hs ├── 04.md ├── 04.py ├── 05.hs ├── 05.md ├── 05.py ├── 06.hs ├── 06.md ├── 06.py ├── 07.hs ├── 07.md ├── 07.py ├── 08.hs ├── 08.md ├── 08.py ├── 09.hs ├── 09.md ├── 09a.py ├── 09b.py ├── 10.hs ├── 10.md ├── 11.hs ├── 11.md ├── 11.py ├── 12.hs ├── 12.md ├── 12.py ├── 13.hs ├── 13.md ├── 13.py ├── 14.hs ├── 14.md ├── 14.py ├── 14a.png ├── 14b.png ├── 15.hs ├── 15.md ├── 15.py ├── 16.md ├── 16a.py ├── 16b.py ├── 17.hs ├── 17.md ├── 17.py ├── 18.hs ├── 18.md ├── 18.py ├── 19.md ├── 19.py ├── 20.md ├── 20.py ├── 21.md ├── 21a.hs ├── 21b.hs ├── 22.md ├── 22.png ├── 22a.py ├── 22b.py ├── 23.md ├── 23.py ├── 24.md ├── 24.py ├── 25.md ├── 25.py ├── Makefile └── README.md ├── 2023 ├── 01.md ├── 01.py ├── 02.md ├── 02.py ├── 03.md ├── 03.py ├── 04.md ├── 04.py ├── 05.md ├── 05.py ├── 06.bas ├── 06.md ├── 06.py ├── 06b.py ├── 07.md ├── 07.py ├── 08.md ├── 08.py ├── 09.md ├── 09.py ├── 10.md ├── 10.py ├── 11.md ├── 11.py ├── 12.md ├── 12a.py ├── 12b.py ├── 12c.py ├── 12d.py ├── 12e.py ├── 12f.py ├── 13.md ├── 13.py ├── 14.md ├── 14.py ├── 15.md ├── 15.py ├── 16.md ├── 16.py ├── 17.md ├── 17a.py ├── 17b.py ├── 18.md ├── 18.py ├── 19.md ├── 19.py ├── 20.md ├── 20.py ├── 20.reordered.txt ├── 21.md ├── 21.png ├── 21.py ├── 21a.py ├── 22.md ├── 22.py ├── 23.md ├── 23.py ├── 24.md ├── 24.py ├── 25.md ├── 25.py └── README.md ├── 2024 ├── 01.md ├── 01.py ├── 02.md ├── 02.py ├── 03.md ├── 03.py ├── 04.md ├── 04.py ├── 05.md ├── 05.py ├── 06.md ├── 06.py ├── 07.md ├── 07.py ├── 08.md ├── 08.py ├── 09.md ├── 09.py ├── 10.md ├── 10.py ├── 11.md ├── 11.py ├── 12.md ├── 12.py ├── 13.md ├── 13.py ├── 14.md ├── 14.py ├── 14_0000.png ├── 14_0018.png ├── 14_0076.png ├── 14_7492.png ├── 15.md ├── 15.py ├── 16.md ├── 16.py ├── 17.md ├── 17.py ├── 18.md ├── 18.py ├── 19.md ├── 19.py ├── 20.md ├── 20.py ├── 21.md ├── 21.py ├── 21a.py ├── 22.md ├── 22.py ├── 23.md ├── 23.py ├── 24.md ├── 24.py ├── 24_fulladder.png ├── 24_halfadder.png ├── 25.md ├── 25.py ├── 25a.py ├── README.md └── aocimports.py ├── .gitignore ├── Dijkstra.hs ├── Direction.hs ├── Modulo.hs ├── README.md ├── Range.hs ├── Utils.hs ├── Vector.hs └── utils ├── dijkstra.py ├── matrix.py ├── modular.py ├── myutils.py ├── prioqueue.py └── vector.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.hi 2 | *.o 3 | [0-9] 4 | [0-9][0-9] 5 | *.sublime-project 6 | *.sublime-workspace 7 | 2020/18.hs 8 | 2020/20*.ppm 9 | 2022/21a 10 | 2022/21b 11 | */[0-9][0-9].txt 12 | *.pyc 13 | -------------------------------------------------------------------------------- /2019/01.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.List 3 | import Control.Exception 4 | 5 | calcFuel :: Integer -> Integer 6 | calcFuel x = (x `div` 3) - 2 7 | 8 | calcFuelFuel :: Integer -> Integer 9 | calcFuelFuel x = sum $ unfoldr iterfunc x 10 | where iterfunc x = let f = calcFuel x in if f > 0 then Just (f, f) else Nothing 11 | 12 | getInput :: IO [Integer] 13 | getInput = do 14 | dat <- readFile "01.txt" 15 | return $ map read $ lines dat 16 | 17 | tests :: IO () 18 | tests = do 19 | check $ calcFuel 12 == 2 20 | check $ calcFuel 14 == 2 21 | check $ calcFuel 1969 == 654 22 | check $ calcFuel 100756 == 33583 23 | check $ calcFuelFuel 14 == 2 24 | check $ calcFuelFuel 1969 == 966 25 | check $ calcFuelFuel 100756 == 50346 26 | where 27 | check True = return () 28 | check False = throwIO $ AssertionFailed "test failed" 29 | 30 | main :: IO () 31 | main = do 32 | weights <- getInput 33 | print $ sum $ map calcFuel weights 34 | print $ sum $ map calcFuelFuel weights 35 | -------------------------------------------------------------------------------- /2019/01.md: -------------------------------------------------------------------------------- 1 | # 1 – Some simple number crunching 2 | Nothing too exciting here, as expected for the first puzzle. 3 | 4 | Though, part B did finally give me a reason to learn how to use `unfoldr` which was neat. 5 | 6 | Also, I spent an amount of time beating my head against a misreading for part B that each module did the full recursive fuel chain individually, and _then_ you add them all up. I misread that each module had its own fuel, but then you do the recursive fuel tail on the entire fuel load as a single unit, which (naturally) gave me the wrong answer, and it took me a little while to figure out what I was doing wrong. 7 | -------------------------------------------------------------------------------- /2019/02.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.Array 3 | import Data.List 4 | import Control.Exception 5 | import Control.Monad 6 | import Intcode 7 | import Utils 8 | 9 | type Val = Integer 10 | 11 | getInput :: IO (IntcodeMem Val) 12 | getInput = do 13 | dat <- readFile "02.txt" 14 | return $ readProg dat 15 | 16 | tryVals :: IntcodeMem Val -> Val -> Val -> Val 17 | tryVals prog a b = (!0) $ icrunMem $ icinit (prog // [(1, a), (2, b)]) 18 | 19 | testVals :: IntcodeMem Val -> Val -> Val -> Val -> IO () 20 | testVals prog a b target = do 21 | let catchfunc = (\e -> return 0) :: (SomeException -> IO Val) 22 | res <- handle catchfunc $ evaluate $ tryVals prog a b 23 | if res == target 24 | then print (a,b) 25 | else return () 26 | 27 | testAll :: IntcodeMem Val -> Val -> IO () 28 | testAll prog target = do 29 | sequence_ [testVals prog x y target | x <- indices prog, y <- indices prog] 30 | 31 | main :: IO () 32 | main = do 33 | code <- getInput 34 | print $ tryVals code 12 2 35 | testAll code 19690720 36 | -------------------------------------------------------------------------------- /2019/03.md: -------------------------------------------------------------------------------- 1 | # 3 – Crossing guard 2 | Pretty simple algorithm here – build a list of all the points each wire touches, convert each list to a set, get the intersection, find the closest. 3 | 4 | Or, at least, that was the plan until the second part, as now each point in the wire needs to know how far along the wire it is, and now the two points on the two wires aren't equal. 5 | 6 | At first I tried doing this with a simple loop through each list, but the O(n²) loops were taking too long on the full puzzle data. I ended up figuring out a way to do it with a Map where the coordinates are keys and the wire lengths are values – which still lets us find intersections efficiently, and then add the lengths together, with the magic of `intersectionWith (+)`. 7 | 8 | As an aside, I've been working with Python for too long... _every_ time I use `minimumBy` or `sortBy` or suchlike, I try to pass a function like Python's `list.sort(key=func)` parameter... it always takes me seeing the compiler error to remember the equivalent is ``sortBy (compare `on` func)``, not `sortBy func`. 9 | -------------------------------------------------------------------------------- /2019/04.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.Char 3 | import Data.List 4 | import Control.Exception 5 | 6 | input = [123456..234567] 7 | 8 | checkVal :: [Int] -> Bool 9 | checkVal s = all id [ 10 | length s == 6, 11 | all (>=0) s, all (<10) s, 12 | any (\(a,b) -> a == b) $ zip s (tail s), 13 | all (\(a,b) -> a <= b) $ zip s (tail s) 14 | ] 15 | 16 | checkValExt :: [Int] -> Bool 17 | checkValExt s = any (\(a,b,c,d) -> (a /= b && b == c && c /= d)) $ zip4 padded (tail padded) (tail $ tail padded) (tail $ tail $ tail padded) 18 | where padded = [-1] ++ s ++ [-1] 19 | 20 | digits :: Integer -> [Int] 21 | digits x = map digitToInt $ prefixed 22 | where 23 | shown = show x 24 | prefixed = (replicate (6 - length shown) '0') ++ shown 25 | 26 | tests = do 27 | check $ digits 123456 == [1,2,3,4,5,6] 28 | check $ digits 1234 == [0,0,1,2,3,4] 29 | 30 | check $ checkVal $ digits 111111 31 | check $ not $ checkVal $ digits 223450 32 | check $ not $ checkVal $ digits 123789 33 | 34 | check $ checkVal $ digits 112233 35 | check $ checkValExt $ digits 112233 36 | check $ checkVal $ digits 123444 37 | check $ not $ checkValExt $ digits 123444 38 | check $ checkVal $ digits 111122 39 | check $ checkValExt $ digits 111122 40 | where 41 | check True = return () 42 | check False = throwIO $ AssertionFailed "test failed" 43 | 44 | main = do 45 | let range = map digits $ input 46 | let candidates = filter checkVal range 47 | print $ length candidates 48 | let candidatesext = filter checkValExt candidates 49 | print $ length candidatesext 50 | -------------------------------------------------------------------------------- /2019/04.md: -------------------------------------------------------------------------------- 1 | # 4 – Password crackers 2 | Not a lot of excitement here, mostly just implementing the rules directly as spelled out. 3 | 4 | Ony real trick here is mapping a function over `zip s (tail s)` which is a trick I've also used in Python for iterating over pairs of consecutive elements in a list. No idea whether that's actually good or just fancy, but I like it. 5 | 6 | Using `zip4 s (tail s) (tail$tail s) (tail$tail$tail s)` to sweep across 4 consecutive elements is probably excessive, especially when the strings are only 6 digits long anyway. 7 | -------------------------------------------------------------------------------- /2019/05.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.Array 3 | import Data.List 4 | import Control.Exception 5 | import Control.Monad 6 | import Intcode 7 | import Utils 8 | 9 | type Val = Integer 10 | 11 | getInput :: IO (IntcodeMem Val) 12 | getInput = do 13 | dat <- readFile "05.txt" 14 | return $ readProg dat 15 | 16 | main :: IO () 17 | main = do 18 | code <- getInput 19 | print $ icrunOutp $ icinitInp code [1] 20 | print $ icrunOutp $ icinitInp code [5] 21 | -------------------------------------------------------------------------------- /2019/05.md: -------------------------------------------------------------------------------- 1 | # 5 – Input and Output 2 | A whole bunch of extra features to be added to the Intcode interpreter today. 3 | 4 | Handling the IO has been a bit of an interesting puzzle on how to retrofit it in to the system. A more ideomatic way would probably have been to have any inputs passed as a parameter to `icstep` and have the outputs as an extra return value from it (plus, I guess, some sort of flag to indicate whether the input was consumed). And then have `icrun` pass in the inputs and collect the outputs as needed. However that would add a fair amount of complication to the framework that seemed like it would be a problem (and was more than I could keep in mind at once) so instead I went with the solution of having the lists of inputs and outputs stored as part of the machine state. 5 | 6 | The outputs as part of the machine state have been a problem, though... I don't want to just append new outputs to the end of the list, as that would be O(n²) given the single-linked list structure. At first I tried appending new outputs to the front of the list, and then just reversing it when the machine was complete. However, that meant we couldn't lazy-read outputs from the machine – it would have to fully complete before any outputs could be read. That works for now, but will be a problem in the future. 7 | 8 | All the other complications – immediate-mode addressing, comparison and branch instructions, were all pretty simple to add to the machine as it was. 9 | -------------------------------------------------------------------------------- /2019/06.md: -------------------------------------------------------------------------------- 1 | # 6 – Tree building 2 | All pretty simple here, just building up a tree (which in this case is a map from each node to its parent, because we never need to worry about iterating a node's children), and keeping track of each node's depth. Part A is just the sum of the depths of every node in the tree. 3 | 4 | Part B is just a case of finding the depth of the deepest common ancestor of the two target nodes, and then measuring the length of the path from source, to ancestor, to destination. Minus 2, because we're really wanting to measure the distance between the direct parents of the source and destination. 5 | -------------------------------------------------------------------------------- /2019/08.md: -------------------------------------------------------------------------------- 1 | # 8 – Pixel bashing 2 | What a weird image format. 3 | -------------------------------------------------------------------------------- /2019/09.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.Array 3 | import Data.List 4 | import Control.Exception 5 | import Control.Monad 6 | import Intcode 7 | import Utils 8 | 9 | type Val = Integer 10 | 11 | getInput :: IO (IntcodeMem Val) 12 | getInput = do 13 | dat <- readFile "09.txt" 14 | return $ readProg dat 15 | 16 | main :: IO () 17 | main = do 18 | code <- getInput 19 | print $ icrunOutp $ icinitInp code [1] 20 | print $ icrunOutp $ icinitInp code [2] 21 | -------------------------------------------------------------------------------- /2019/09.md: -------------------------------------------------------------------------------- 1 | # 9 – Base pointers 2 | Another day, another extra field that needs to be added to the machine state. This is getting a bit awkward, as every time I change the structure of the `Intcode` type, I have to go back and edit all the previous puzzles to add the new field, even though those machines don't use it, so that they still compile. 3 | 4 | Adding the base pointer to the code wasn't ultimately that complicated. And we're already using bignums here. The complication that is added, though, is wanting to be able to read/write outside of the bounds of the memory. I'd considered before whether this would be necessary, but never bothered to implement it as the programs we'd been given up until now all allocated extra space at the end of the program to use as scratch space. But now we needed to have the memory be an expanding array. 5 | 6 | Haskell's base library doesn't seem to have any special handling for that... the best thing I could find for expanding an array was `ixmap`, which lets you change the array's bounds, but the values for the new array need to be mapped to values from the old array – you can't specify "hey, fill some of these values with this other specific value". So we use this to expand the array and then set all the new values to `0` as a second pass. Which seems inefficient but it works. But then I don't really have a good grasp yet as to what's efficient and what isn't in Haskell, for the time being all I care about is whether the results are correct, and this seems to work just fine. 7 | 8 | With 9A done, 9B seems to be pretty trivial... and this seems to be borne out in the stats, less than 100 silver medals for this puzzle as I write this. 9 | -------------------------------------------------------------------------------- /2019/17.md: -------------------------------------------------------------------------------- 1 | # 17 – Pattern recognition 2 | This one... turned out to be a lot easier to solve by hand, than to figure out how to solve it generally... 3 | 4 | Part A is pretty simple – get the program's output, read it in, count the tiles of floor that have >2 neighbouring floor tiles. Mostly a challenge in parsing the Intcode output as ASCII. 5 | 6 | Part B, though... I solved it by hand with a number of simplifying assumptions based on how the maze appears to be generated. Generalising it would be a challenge. 7 | 8 | But based on the assumptions that (a) all the intended subpatterns are some number of repetitions of "one turn, one move count, one turn, one move count, ..." and that the intended route is not self-overlapping (other than going straight-through at intersections) then the final route isn't too hard to figure out, and the intended separation into subpatterns wasn't too hard to figure out by hand. But generalising this, especially if you _don't_ include those simplifying assumptions, would be a _lot_ harder. 9 | 10 | I may come back at some point and solve this programmattically, leaving the assumptions in (but actually codifying them), but for now this has a hard-coded hand-rolled solution (and thus will not be useful for anyone else's input). 11 | -------------------------------------------------------------------------------- /2019/19.md: -------------------------------------------------------------------------------- 1 | # 19 – Following the beams 2 | Another puzzle where I'm making some simplifying assumptions based on my input... 3 | 4 | Part A is pretty simple, just measure the various locations and count up the returns. 5 | 6 | For part B, though, I worked from first looking at the plot and approximating the angles by hand – one edge of the beam is slightly to the left of 45 degrees, and the other edge is somewhere further to the left of that. So we only look in that direction on each line to figure out where to expect the beam to be. And this, combined with some hand-checked endpoints for the binary search gives us a solution that will only really work for my input values. 7 | -------------------------------------------------------------------------------- /2019/20.md: -------------------------------------------------------------------------------- 1 | # 20 – This maze is full of mazes 2 | Nothing super complicated here, ultimately. The hardest part is just in parsing the input, isolating all the labels and figuring out which tiles in the map they correspond to. Once we have all the portals matched up, it's a relatively simple extension of the Dijkstra implementation we already have to add the other end of the portal to the `neighbours` function in the appropriate place. 3 | 4 | For part B, I tried attacking it directly, just doing the same thing again but keeping track of our depth into the maze, but this proved to take too long to run. So we borrow a trick from day 18, and pre-calculate the distances between the various portals (since we'll be visiting those routes repeatedly) and then Dijkstra on a more generalised graph where our nodes are the various portals at each depth. This works well, and gives us our answer... though we do have to put an upper limit on the depth of the maze, otherwise it will potentially run forever, and I had to expand this a couple of times before getting a result. 5 | -------------------------------------------------------------------------------- /2019/21.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.Array 3 | import Data.List 4 | import Data.Char 5 | import Intcode 6 | import Utils 7 | 8 | getInput :: IO (IntcodeMem Integer) 9 | getInput = do 10 | dat <- readFile "21.txt" 11 | return $ readProg dat 12 | 13 | runProg :: IntcodeMem Integer -> String -> String 14 | runProg prog str = shown 15 | where 16 | inputs = map (toInteger.ord) str 17 | outputs = icrunOutp $ icinitInp prog inputs 18 | shown = do 19 | outp <- outputs 20 | if outp < 256 21 | then [chr $ fromInteger outp] 22 | else "[" ++ show outp ++ "]" 23 | 24 | partA = unlines [ 25 | -- There is a hole in front of us 26 | "NOT A J", 27 | "NOT B T", 28 | "OR T J", 29 | "NOT C T", 30 | "OR T J", 31 | -- and somewhere to land 32 | "AND D J", 33 | "WALK", ""] 34 | partB = unlines [ 35 | -- There is a hole in front of us 36 | "NOT A J", 37 | "NOT B T", 38 | "OR T J", 39 | "NOT C T", 40 | "OR T J", 41 | -- and somewhere to land 42 | "AND D J", 43 | -- reset temp register (if J is true this will set T to false; if J is false we don't care how T ends up) 44 | "NOT J T", 45 | -- and we can continue safely from there 46 | -- can we take two steps, or take a step and then jump? 47 | "OR I T", 48 | "OR F T", 49 | "AND E T", 50 | -- or jump again immediately? 51 | "OR H T", 52 | -- combine that with the first half 53 | "AND T J", 54 | "RUN", ""] 55 | main :: IO () 56 | main = do 57 | prog <- getInput 58 | putStrLn $ runProg prog partA 59 | putStrLn $ runProg prog partB 60 | -------------------------------------------------------------------------------- /2019/21.md: -------------------------------------------------------------------------------- 1 | # 21 – Jump for joy 2 | Not too complex here – just need to write the appropriate programs for the two robots. 3 | 4 | For part B, the logic is basically trying to determine "if I jump now, can I see that I'm guaranteed screwed" and if so, it doesn't jump... but if there's a route it can see that would take it to beyond its sensor range, it assumes everything is fine. And it seems that this logic is good enough to pass the test. 5 | 6 | The full expression for part B we're implementing is: 7 | ``` 8 | J = (not A || not B || not C) && D && (H || (E && I) || (E && F)) 9 | ``` 10 | In order, those terms are: 11 | * There is a hole coming up 12 | * There's somewhere for us to land 13 | * Once we land, we can either: 14 | * Jump again immediately 15 | * Take one step and them jump, or 16 | * Take two steps (at which point our jump would take us outside sensor range) 17 | 18 | However, implementing that logic within the architecture provided, given it has multiple nested operations and we only have one temp register, was not trivial, but some amount of rearranging got us there. 19 | -------------------------------------------------------------------------------- /2019/25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/e3fe0fbd8a26e4e0941ed72a032202416f1cc20c/2019/25.png -------------------------------------------------------------------------------- /2019/Makefile: -------------------------------------------------------------------------------- 1 | all: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 2 | 3 | clean: 4 | rm -f [0-9][0-9] *.hi *.o ../*.hi ../*.o 5 | 6 | .PHONY: all clean 7 | 8 | %: %.hs 9 | ghc -threaded -with-rtsopts="-N" -o $@ $^ 10 | 11 | 01: 01.hs 12 | 02: 02.hs ../Utils.hs Intcode.hs 13 | 03: 03.hs ../Utils.hs 14 | 04: 04.hs 15 | 05: 05.hs ../Utils.hs Intcode.hs 16 | 06: 06.hs 17 | 07: 07.hs ../Utils.hs Intcode.hs 18 | 08: 08.hs ../Utils.hs 19 | 09: 09.hs ../Utils.hs Intcode.hs 20 | 10: 10.hs ../Utils.hs 21 | 11: 11.hs ../Utils.hs Intcode.hs 22 | 12: 12.hs ../Utils.hs 23 | 13: 13.hs ../Utils.hs Intcode.hs 24 | 14: 14.hs ../Utils.hs 25 | 15: 15.hs ../Utils.hs Intcode.hs ../Direction.hs ../Dijkstra.hs 26 | 16: 16.hs ../Utils.hs 27 | 17: 17.hs ../Utils.hs Intcode.hs ../Direction.hs 28 | 18: 18.hs ../Utils.hs ../Direction.hs ../Dijkstra.hs 29 | 19: 19.hs ../Utils.hs Intcode.hs ../Direction.hs 30 | 20: 20.hs ../Utils.hs ../Direction.hs ../Dijkstra.hs 31 | 21: 21.hs ../Utils.hs Intcode.hs 32 | 22: 22.hs ../Utils.hs 33 | 23: 23.hs ../Utils.hs Intcode.hs 34 | 24: 24.hs ../Utils.hs ../Direction.hs 35 | 25: 25.hs ../Utils.hs Intcode.hs 36 | -------------------------------------------------------------------------------- /2019/README.md: -------------------------------------------------------------------------------- 1 | # Advent of Code 2019 2 | 3 | My solutions for the [Advent of Code 2019](https://adventofcode.com/2019) 4 | 5 | I use these types of challenges as an excuse to learn (or brush up on) programming languages I'm not particularly familiar with. This time around: Haskell. 6 | 7 | So don't expect this code to be particularly efficient, clean, or idiomatic. 8 | 9 | But, it works. 10 | 11 | For each puzzle, eg `01.hs` is my code, and `01.txt` is my version of the data file from AOC. `01.md` is my comments on the puzzle, and my solution for it. Some common code between the puzzles has been collected in `Intcode.hs` and `../Utils.hs`. 12 | -------------------------------------------------------------------------------- /2020/01.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import qualified Data.Set as S 3 | import Control.Exception 4 | 5 | getInput :: IO (S.Set Integer) 6 | getInput = do 7 | dat <- readFile "01.txt" 8 | return $ S.fromList $ map read $ lines dat 9 | 10 | findTwoSum :: Integer -> S.Set Integer -> [Integer] 11 | findTwoSum total values = [a * b | 12 | a <- S.toList values, 13 | let b = total - a, 14 | b `S.member` values] 15 | 16 | findThreeSum :: Integer -> S.Set Integer -> [Integer] 17 | findThreeSum total values = [a * b * c | 18 | a <- S.toList values, 19 | b <- S.toList values, 20 | let c = total - a - b, 21 | c `S.member` values] 22 | 23 | tests :: IO () 24 | tests = do 25 | check $ head (findTwoSum 2020 values) == 514579 26 | check $ head (findThreeSum 2020 values) == 241861950 27 | where 28 | values = S.fromList [1721, 979, 366, 299, 675, 1456] 29 | check True = return () 30 | check False = throwIO $ AssertionFailed "test failed" 31 | 32 | main :: IO () 33 | main = do 34 | values <- getInput 35 | print $ head $ findTwoSum 2020 values 36 | print $ head $ findThreeSum 2020 values 37 | -------------------------------------------------------------------------------- /2020/01.md: -------------------------------------------------------------------------------- 1 | # 1 – Balancing the books 2 | I'll be honest, I solved this one in Python at first, because it was like 2am when I was reminded that AoC existed, and I wanted to get this done quickly so I could get to sleep. This is the same strategy, ported to Haskell. 3 | 4 | It's a pretty direct algorithm, the only real speedup over the dumb algorithm is in storing the data as a `Set` rather than a list, so that checking each possibility is faster. 5 | -------------------------------------------------------------------------------- /2020/02.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.List 3 | import qualified Text.ParserCombinators.ReadP as P 4 | import Control.Exception 5 | import Utils 6 | 7 | data Password = Password Integer Integer Char String deriving Eq 8 | 9 | getInput :: IO [Password] 10 | getInput = do 11 | dat <- readFile "02.txt" 12 | return $ map parseInputLine $ lines dat 13 | 14 | parseInputLine :: String -> Password 15 | parseInputLine line = runReadP readLine line 16 | where 17 | readLine :: P.ReadP Password 18 | readLine = do 19 | low <- readInt 20 | P.char '-' 21 | high <- readInt 22 | P.skipSpaces 23 | char <- P.get 24 | P.char ':' 25 | P.skipSpaces 26 | password <- P.munch (const True) 27 | return $ Password low high char password 28 | readInt = P.readS_to_P reads :: P.ReadP Integer 29 | 30 | validateA :: Password -> Bool 31 | validateA (Password low high char password) = low <= count && count <= high 32 | where count = genericLength $ filter (==char) password 33 | 34 | validateB :: Password -> Bool 35 | validateB (Password low high char password) = isgood low /= isgood high 36 | where isgood n = (password `genericIndex` (n - 1)) == char 37 | 38 | tests :: IO () 39 | tests = do 40 | check $ values == [Password 1 3 'a' "abcde", Password 1 3 'b' "cdefg", Password 2 9 'c' "ccccccccc"] 41 | check $ map validateA values == [True, False, True] 42 | check $ map validateB values == [True, False, False] 43 | where 44 | values = map parseInputLine ["1-3 a: abcde", "1-3 b: cdefg", "2-9 c: ccccccccc"] 45 | check True = return () 46 | check False = throwIO $ AssertionFailed "test failed" 47 | 48 | main = do 49 | passwords <- getInput 50 | print $ genericLength $ filter validateA passwords 51 | print $ genericLength $ filter validateB passwords 52 | -------------------------------------------------------------------------------- /2020/02.md: -------------------------------------------------------------------------------- 1 | # 2 – Master-level security 2 | The most interesting part here is in parsing the input file. It would have been entirely possible to parse it the boring way, but I figured why not, parse it using `ReadP`, for fun. 3 | 4 | Once that's done, checking the actual password requirements is pretty simple. 5 | 6 | [141/106] 7 | -------------------------------------------------------------------------------- /2020/03.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.List 3 | import Control.Exception 4 | 5 | getInput :: IO [[Bool]] 6 | getInput = do 7 | dat <- readFile "03.txt" 8 | return $ parseInput dat 9 | 10 | parseInput :: String -> [[Bool]] 11 | parseInput = map (map (=='#')) . lines 12 | 13 | tracePath :: Integer -> Integer -> [[Bool]] -> [Bool] 14 | tracePath dx dy trees = [trees `genericIndex` (i * dy) `genericIndex` ((i * dx) `mod` cx) | i <- [0..(cy-1) `div` dy]] 15 | where cy = genericLength trees; cx = genericLength $ head trees 16 | 17 | countPath :: [Bool] -> Integer 18 | countPath = genericLength . filter id 19 | 20 | doCount :: Integer -> Integer -> [[Bool]] -> Integer 21 | doCount dx dy trees = countPath $ tracePath dx dy trees 22 | 23 | tests :: IO () 24 | tests = do 25 | check $ doCount 1 1 trees == 2 26 | check $ doCount 3 1 trees == 7 27 | check $ doCount 5 1 trees == 3 28 | check $ doCount 7 1 trees == 4 29 | check $ doCount 1 2 trees == 2 30 | where 31 | trees = parseInput "..##.......\n#...#...#..\n.#....#..#.\n..#.#...#.#\n.#...##..#.\n..#.##.....\n.#.#.#....#\n.#........#\n#.##...#...\n#...##....#\n.#..#...#.#" 32 | check True = return () 33 | check False = throwIO $ AssertionFailed "test failed" 34 | 35 | main = do 36 | trees <- getInput 37 | print $ doCount 3 1 trees 38 | print $ doCount 1 1 trees * doCount 3 1 trees * doCount 5 1 trees * doCount 7 1 trees * doCount 1 2 trees 39 | -------------------------------------------------------------------------------- /2020/03.md: -------------------------------------------------------------------------------- 1 | # 3 – Duck and weave 2 | Not really a lot to talk about here, this one was pretty simple. Did get me on the leaderboard though, that's not nothing. 3 | 4 | [77/17] 5 | -------------------------------------------------------------------------------- /2020/04.md: -------------------------------------------------------------------------------- 1 | # 4 – Crasping for a good record type 2 | Still not much of challenge from a computational front, the main hard part here is representing all this passport data in a succinct datatype within Haskell. 3 | 4 | In my usual language, of Python, I'd just store the different fields in a dict, but that doesn't seem in keeping with a more strictly typed language? It feels like I _should_ be using a more statically-defined record type here. But then, that leads to the mess of `addField` here, as we have to individually map the strings in the input to the fields in our record type. 5 | 6 | I feel like this strategy of learning languages via programming challenges is neat for learning the basics of how to write for a language, but not so much how to write _idiomatically_ for a language... 7 | 8 | [1223/235] 9 | -------------------------------------------------------------------------------- /2020/05.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.List 3 | import Control.Exception 4 | import Utils 5 | 6 | getInput :: IO [Integer] 7 | getInput = do 8 | dat <- readFile "05.txt" 9 | return $ map readPass $ lines dat 10 | 11 | readPass :: String -> Integer 12 | readPass = fromBaseN 2 . map charToBinary 13 | where 14 | charToBinary 'F' = 0 15 | charToBinary 'B' = 1 16 | charToBinary 'L' = 0 17 | charToBinary 'R' = 1 18 | 19 | tests :: IO () 20 | tests = do 21 | check $ readPass "BFFFBBFRRR" == 567 22 | check $ readPass "FFFBBBFRRR" == 119 23 | check $ readPass "BBFFBBFRLL" == 820 24 | where 25 | check True = return () 26 | check False = throwIO $ AssertionFailed "test failed" 27 | 28 | main = do 29 | passes <- getInput 30 | let sorted = sort passes 31 | print $ last sorted 32 | print [ x + 1 | (x,y) <- zip sorted (tail sorted), x /= y - 1] 33 | -------------------------------------------------------------------------------- /2020/05.md: -------------------------------------------------------------------------------- 1 | # 5 – This is a very big plane 2 | The main part of this puzzle is just in reading the input... converting these weird codes into seat ids. But then, the trick to recognise is that the binary-search algorithm that is presented in the puzzle is just ordinary binary numbers. 3 | 4 | Take one of the examples they give: `BBFFBBFRLL`, which is row 102, column 4, seat ID 820. Well, in binary, 102 is `1100110`, 4 is `100`, and 820 is `1100110100`, which you'll recognise as being identical to the seating code's first part, second part, and entire thing, respectively, just with the various letters replaced with 1s and 0s. And this makes sense – if you take a bunch of 10-digit binary numbers, the ones that start with 1 are in the upper half, and the ones that start with 0 are in the lower half, etc. So in order to process these codes, we don't need to do any of this halving and binary-search stuff, we just convert the letters into bits, and read it as a binary number. 5 | 6 | This was hampered somewhat by discovering that Haskell core libs seemingly don't include a function for converting from binary strings, only decimal, so I quickly threw one together for my Utils module. 7 | 8 | Once we have that, and we have our list of seat ids, solving the actual puzzles are pretty trivial. The first is just getting the maximum from our list, and the second is just stepping through the list after sorting it, to find the one that's missing (which can be done in any of a number of ways). 9 | -------------------------------------------------------------------------------- /2020/06.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.List 3 | import Data.List.Split (splitOn) 4 | import qualified Data.Set as S 5 | import Control.Exception 6 | 7 | getInput :: IO [[String]] 8 | getInput = do 9 | dat <- readFile "06.txt" 10 | return $ parseInput dat 11 | 12 | parseInput :: String -> [[String]] 13 | parseInput = map lines . splitOn "\n\n" 14 | 15 | countUnique :: [String] -> Integer 16 | countUnique = toInteger . S.size . S.fromList . concat 17 | 18 | countAll :: [String] -> Integer 19 | countAll = toInteger . S.size . foldl1 S.intersection . map S.fromList 20 | 21 | tests :: IO () 22 | tests = do 23 | check $ map countUnique answers == [3, 3, 3, 1, 1] 24 | check $ map countAll answers == [3, 0, 1, 1, 1] 25 | where 26 | answers = parseInput "abc\n\na\nb\nc\n\nab\nac\n\na\na\na\na\n\nb" 27 | check True = return () 28 | check False = throwIO $ AssertionFailed "test failed" 29 | 30 | main = do 31 | answers <- getInput 32 | print $ sum $ map countUnique answers 33 | print $ sum $ map countAll answers 34 | -------------------------------------------------------------------------------- /2020/06.md: -------------------------------------------------------------------------------- 1 | # 6 – FAQs 2 | Not a lot to talk about here. Just another puzzle that's trivialised by the existance of a `Set` datatype. 3 | 4 | But I'm getting concerned about the variety of plane that is servicing the north pole. The largest real-life passenger plane in service, the A380, usually holds 525 passengers, and is rated to hold as many as 850 if you really cram 'em in. And to do that, it has a two-storey interior. And yet, we're supposed to believe that, in the plane we boarded in yesterday's puzzle, we were fitting 810 passangers onto a plane that, according to its seat numbering scheme, isn't a multi-level structure. 5 | 6 | And, like, normally I would have made a joke about them being elves, so you can cram more of them into a smaller space, but we learned _two_ days ago that they're all normal human heights. Or at least, that's what it says on their passports. 7 | 8 | And now, today, the puzzle says we're transferring at a regional airport into a _larger_ plane? And for this puzzle I'm collating the survey responses of 1662 passengers? The size of these planes is getting out of hand... 9 | -------------------------------------------------------------------------------- /2020/07.md: -------------------------------------------------------------------------------- 1 | # 7 – More nested containers than a Docker deployment 2 | A lot of fun with the parser here. Once again, I'm sure this would be pretty simple to parse by just splitting on spaces, but I'm enjoying `ReadP` too much. 3 | 4 | Interestingly, this puzzle is in the inverse order than I would have expected. The data set is a list of containers, and for each their contents... it's reasonably simple to traverse that forwards (ie for this container, what does it contain?) but requires an extra non-trivial step to traverse it backwards (ie for this content, what can contain it?). And yet the latter is the first star, while the former is the second star. 5 | 6 | And, I do understand why – keeping track of the counts are a bigger complication that makes the second half definitely harder than the first half, which only has to care about existence. And it would require some amount of contrivance to come up with a puzzle that required you do work backwards through the map, and _also_ keep track of counts of some kind. So I get why it was done the way it was. Just a little surprising is all. 7 | 8 | Anyway, another day, another puzzle that I'm surprised by how simple it is to write some quite complex operations in Haskell, when the stars align just right. Today's example: `foldl (M.unionWith (+))`, which lets me take a list of maps that are serving as counters, and combine them all together into a single totals map, in one fell swoop. 9 | 10 | The one hiccup is that both of the algorithms I wrote for today include the `shiny gold` bag itself in the output, but the puzzle definition doesn't want that, so both of the calls in `main` have to `subtract 1` from the result to get the expected answer. Which is a little ugly, but the algorithms are so much simpler this way, so I stuck with it. 11 | -------------------------------------------------------------------------------- /2020/08.md: -------------------------------------------------------------------------------- 1 | # 8 — Oh no, not again 2 | This puzzle was made somewhat easier by virtue of having spent so much time last year working on the Intcode machine. This machine is obviously nowhere near as complex as that one (the code isn't self-modifying, opcodes are all the same size, etc), but that did give me the basic structure – define an program type, and a state type, and a function to calculate the next state from a given one, and then unfold to victory. 3 | 4 | The main improvement I made here from what I learned last time, is to have an explicit state to represent the halt condition (and, in this case, the looped condition). So rather than having `nextState` return `Nothing` when it enters a halt state, it instead returns `Halted`, and then `nextState` on `Halted` _then_ returns `Nothing`. Makes some things a little bit cleaner... and in particular, means we can put data there to distinguish different reasons for halting (ie halt-state vs looped). 5 | 6 | I'm just hoping this is a one-off puzzle, and this machine won't be returning a-la Intcode... I really don't want to do that whole routine again. 7 | -------------------------------------------------------------------------------- /2020/09.md: -------------------------------------------------------------------------------- 1 | # 9 – More sum-finding 2 | The first part of this uses basically the same algorithm from puzzle 1 – checking each option, using a `Set` to check if its requisite counterpart is also in the list. 3 | 4 | The second part is a little more complicated, but we can use a simplifying step – if we make a list of the cumulative totals from our original list, then any number that we can get from our original list as a sum of a consecutive range, can also be gotten from our cumulative list as the difference of two terms. This means we can just try to find two terms that differ by our target amount, which uses the same algorithm as before. Except with a bit more processing as we need to be able to get back to _where_ those two numbers are in the list, rather than the actual numbers themselves, so we use a `Map` of value to original index, rather than a `Set`. But the spirit is the same. 5 | -------------------------------------------------------------------------------- /2020/10.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.List 3 | import Control.Exception 4 | 5 | getInput :: IO [Integer] 6 | getInput = do 7 | dat <- readFile "10.txt" 8 | return $ sort $ map read $ lines dat 9 | 10 | addLimits :: [Integer] -> [Integer] 11 | addLimits xs = [0] ++ xs ++ [(last xs) + 3] 12 | 13 | getDiffs :: [Integer] -> [Integer] 14 | getDiffs xs = map (uncurry (-)) $ zip (tail xs) xs 15 | 16 | getPartA :: [Integer] -> Integer 17 | getPartA diffs = ones * threes 18 | where 19 | ones = genericLength $ filter (==1) diffs 20 | threes = genericLength $ filter (==3) diffs 21 | 22 | getPartB :: [Integer] -> Integer 23 | getPartB diffs = head result 24 | where 25 | result = map solveFor [0..length diffs - 1] 26 | maxstep n = subtract 1 $ length $ takeWhile (<=3) $ scanl (+) 0 $ drop n diffs 27 | solveFor n 28 | | n == length diffs - 1 = 1 29 | | otherwise = sum $ take (maxstep n) $ drop (n+1) result 30 | 31 | tests :: IO () 32 | tests = do 33 | check $ getPartA diffs == 220 34 | check $ getPartB diffs == 19208 35 | where 36 | testdata = sort [28,33,18,42,31,14,46,20,48,47,24,23,49,45,19,38,39,11,1,32,25,35,8,17,7,9,4,2,34,10,3] 37 | diffs = getDiffs $ addLimits testdata 38 | check True = return () 39 | check False = throwIO $ AssertionFailed "test failed" 40 | 41 | main = do 42 | vals <- getInput 43 | let diffs = getDiffs $ addLimits vals 44 | print $ getPartA diffs 45 | print $ getPartB diffs 46 | -------------------------------------------------------------------------------- /2020/11.md: -------------------------------------------------------------------------------- 1 | # 11 – Conway's game of ferries 2 | Oh man, everyone loves cellular automata! 3 | 4 | So, the premise here is pretty simple: build an array to hold the grid, build a function to iterate to the next grid state, and loop that until we reach a fixed point. 5 | 6 | Part 2 was an interesting twist, as the idea of what each node considers "neighbours" is not the part I was expecting to change between parts, so plugging that required some amount of gutting of the code. But not too much. 7 | 8 | [134/53] 9 | -------------------------------------------------------------------------------- /2020/12.md: -------------------------------------------------------------------------------- 1 | # 12 – Just as good as Apple Maps 2 | This was a fun puzzle, mostly because I got to reuse the vector-maths module I'd made for [puzzle 12 last year](../2019/12.md) that I ended up having to remove for the second half of that puzzle. It's nice that it's finally actually in use again. 3 | 4 | With that handled, all that's left is the work of writing the code to handle each operation, determine the result, and try not to get mixed up as to whether I made North or South as the positive direction on the Y axis. Don't ask me how I know that the last point is a challenge. 5 | 6 | [315/50] 7 | -------------------------------------------------------------------------------- /2020/13.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.Function 3 | import Data.List 4 | import Data.Maybe 5 | import Control.Exception 6 | import Utils 7 | 8 | getInput :: IO (Integer, [Maybe Integer]) 9 | getInput = do 10 | dat <- readFile "13.txt" 11 | return $ parseInput dat 12 | 13 | parseInput :: String -> (Integer, [Maybe Integer]) 14 | parseInput dat = (timestamp, ids) 15 | where 16 | [line1, line2] = lines dat 17 | timestamp = read line1 18 | ids = map readId $ split ',' line2 19 | readId "x" = Nothing 20 | readId n = Just $ read n 21 | 22 | partA :: Integer -> [Maybe Integer] -> Integer 23 | partA timestamp ids = result 24 | where 25 | realids = catMaybes ids 26 | getnextbus id = if timestamp `mod` id == 0 then timestamp else timestamp + id - (timestamp `mod` id) 27 | options = map (\id->(id, getnextbus id)) realids 28 | (nextbusid, nextbustime) = minimumBy (compare `on` snd) options 29 | result = nextbusid * (nextbustime - timestamp) 30 | 31 | partB :: [Maybe Integer] -> Integer 32 | partB ids = fst $ foldl1 chineseRemainder buses 33 | where 34 | buses = map (\(i, Just j) -> (-i, j)) $ filter (isJust . snd) $ enumerate ids 35 | 36 | tests :: IO () 37 | tests = do 38 | check $ partA timestamp ids == 295 39 | check $ partB ids == 1068781 40 | where 41 | (timestamp, ids) = parseInput "939\n7,13,x,x,59,x,31,19" 42 | check True = return () 43 | check False = throwIO $ AssertionFailed "test failed" 44 | 45 | main = do 46 | (timestamp, ids) <- getInput 47 | print $ partA timestamp ids 48 | print $ partB ids 49 | -------------------------------------------------------------------------------- /2020/16.md: -------------------------------------------------------------------------------- 1 | # 16 – Everyone loves a good logic grid 2 | I... was kinda dreading this one. When I solved this the first time around, I used some quick scripts to check all the different ranges, but then I generated the logic puzzle grid and solved it by hand. And the idea of trying to solve it programmatically? In a functional language? I wasn't too looking forward to that. 3 | 4 | But turns out, it wasn't quite as bad as I feared. It still wasn't great, but I got there. The fundamental idea isn't too hard: find an option which is the only available option in its row or column, and mark all the _other_ positions in its column/row as _not_ options, and repeat until the whole grid is reduced to a single option in each row/column. But actually implementing that algorithm was a bit of work. 5 | 6 | So much so that I didn't even try to attempt this, on that day... it wasn't until after I'd solved day 17 that I came back and solved this one as well. 7 | 8 | Feels good to have it done, though. 9 | 10 | One thing I did do, though, is the code for checking if a number is in one of the ranges... which I did overengineer somewhat (though there's definitely still room for improvement to make it even more complicated) but I thought came together quite nicely. 11 | 12 | [48/40] 13 | -------------------------------------------------------------------------------- /2020/17.md: -------------------------------------------------------------------------------- 1 | # 17 – Cellular automata again, but _more_ 2 | And so we have, again, a cellular-automaton puzzle. It's even actually Game of Life rules, this time. Except, the multi-dimension thing. 3 | 4 | My solution here is straight-up the direct approach. Make the grid, calculate the neighbours for each cell, to calculate the successor grid. Expanding the grid by 1 in every direction at each step, to account for the pattern's growth. 5 | 6 | There are definitely optimisations that could be done. For instance, the pattern doesn't necessarily actually expand in _every_ direction at maximum speed, so the bounds could be shrunk after each generation. But this solution calculates fast enough that I wasn't required to figure out any of those optimisations. 7 | 8 | For the second part, because of Haskell's typefulness, it's hard to work with tuples in a sort of general way... writing code that works the same for 3-value tuples and 4-value tuples can be a pain. It could be done, but I was too lazy to do it... so the second part is basically just a direct copy-paste of the first part, with a few small tweaks to add the extra dimension. 9 | 10 | ... Or, at least, I did at first. But then I changed my mind and came back to do the actual thing. The optimisation of shrinking the grid bounds after each step definitely improved the runtime, from 16s to 13s of CPU time (before multithreading). And making the code more general definitely made it a lot tidier. 11 | -------------------------------------------------------------------------------- /2020/18.md: -------------------------------------------------------------------------------- 1 | # 18 - PEASMD 2 | OK, I'll admit, this solution is a little cheesy. And it wasn't my idea, I got it from some chatter in the LRR discord. 3 | 4 | The idea is: rather than try to parse these expressions ourself, we're already using a language that can do mathematical expressions, just give it to that. 5 | 6 | So all we do is create new addition and multiplication operators, that work the same as the normal ones, except we diddle with their fixity so that they get parsed in the order(s) we want. 7 | 8 | To that end, a thrown-together shell script that takes the input file and `sed`s it into the appropriate Haskell code (changing the operators, and turning it into a list), which we then compile and run. In an interpreted language we could do this on the fly with `eval` but as a compiled language Haskell doesn't have that function. 9 | 10 | One minor hitch is that our `sed` script turns the text input into a list by adding a comma to the end of each line. But Haskell doesn't like trailing commas in lists. But the puzzle wants us to get the sum of all the puzzle results, so we can just add an extra `0` to the end of the list after the final comma, to make it parse without affecting the result. 11 | -------------------------------------------------------------------------------- /2020/18.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ( 3 | cat < 18.hs 27 | -------------------------------------------------------------------------------- /2020/22.md: -------------------------------------------------------------------------------- 1 | # 22 – Strange game. 2 | What a weird card game, first of all. You go through all that effort, and yet whoever has the highest-value card inevitably wins... as it's impossible for the highest-value card to switch decks. In the first game, it's a literal impossibility – whoever has the highest-value card will always win the round when it's revealed, so they _cannot_ run out of cards, so they _cannot_ lose. For the second game, revealing the highest-value card will never cause a subgame (because its value is as big as the total number of cards in the game, so must be larger than the number of cards remaining in that player's deck), so, again, it will always win, and the player who has it can never run out of cards... but that's not the only way to lose, so it is _theoretically_ possible if Player 2 has the highest-value card, and you trip the infinite-loop-prevention clause, for Player 1 to win. But that is exceedingly unlikely. 3 | 4 | None of that matters to the puzzle, since the puzzle doesn't ask us to figure out who wins, it is asking about the final permutation of the deck, which means we still need to run the game. But still. 5 | 6 | Actually running the game is not super complicated. Especially since they corrected from my issue yesterday, and have _extensive_ detail in their worked example. The first day is pretty straightforward, and while the second day is definitely more complicated, as someone who's theory-crafted my fair share of [Shahrazad](https://scryfall.com/card/arn/10/shahrazad) nested games, the idea of running a subgame that doesn't affect the main game, and then returning to the main game and doing something based on the subgame's winner... all makes sense to me. 7 | 8 | [70/22] 9 | -------------------------------------------------------------------------------- /2020/24.md: -------------------------------------------------------------------------------- 1 | # 24 – Cellular automata come in threes 2 | And now, our third cellular automaton for the year. This one, on a hex grid, which is different, but once you pick a coordinate system it's not that challenging. Especially compared to the earlier one that was on the 4-grid. 3 | 4 | Used a different strategy for this one... for the past cellular automata, I worked on a big rectangular array that was the size of the grid... this time, since the starting set seemed relatively sparse, I decided to instead work with a `Set` of all the active points. Upside: don't need to worry about the size of the array expanding out of control just because there's one active cell cluster off in the distance. Downsides: honestly, none are springing to mind immediately. Probably should be doing it this way all the time. Code's smaller, too. Might be slower if the grid is particularly dense? But honestly I'm not immediately sure that's the case anyway. Should probably just do any future cellular automata puzzles like this too. 5 | -------------------------------------------------------------------------------- /2020/25.md: -------------------------------------------------------------------------------- 1 | # 25 – This is just straight-up Diffie-Hellman. 2 | This is just straight-up Diffie-Hellman. 3 | 4 | When I was actually working on it, I wasn't entirely sure, it could have been something similar, just a simplification of DH, but I figured it was close enough that it probably wasn't worth trying to figure out anything smart to crack the private keys... if the forward operation is to raise a number to a power with a modulus, then nothing of value is going to be found in trying to find a clever way to invert that. People much smarter than me have tried. 5 | 6 | It did, however, let me pull in a `Modulo` type that I'd worked on before, and in particular this lets me do the _forward_ version of the transform using Haskell's builtin `(^)` which does exponentiation-by-squaring, so it's noticibly faster than the naive approach. But that doesn't help much with the inverse transform, which is still just a brute-force search. The one speedup we have is iterating through the state space once, and checking _both_ public keys, to see which one decodes first (ie, which one has the lower private key), because we only need to crack one of the two keys. 7 | 8 | Ultimately, it doesn't take that long to run (about a quarter of a second) because 20201227 isn't _that_ big a prime. 9 | 10 | [629/507] 11 | -------------------------------------------------------------------------------- /2020/Makefile: -------------------------------------------------------------------------------- 1 | all: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 2 | 3 | clean: 4 | rm -f [0-9][0-9] *.hi *.o ../*.hi ../*.o 5 | 6 | .PHONY: all clean 7 | 8 | %: %.hs 9 | ghc -O2 -threaded -with-rtsopts="-N" -o $@ $^ 10 | 11 | 01: 01.hs 12 | 02: 02.hs ../Utils.hs 13 | 03: 03.hs 14 | 04: 04.hs 15 | 05: 05.hs ../Utils.hs 16 | 06: 06.hs 17 | 07: 07.hs ../Utils.hs 18 | 08: 08.hs ../Utils.hs 19 | 09: 09.hs ../Utils.hs 20 | 10: 10.hs 21 | 11: 11.hs ../Utils.hs 22 | 12: 12.hs ../Vector.hs 23 | 13: 13.hs ../Utils.hs 24 | 14: 14.hs ../Utils.hs 25 | 15: 15.hs 26 | 16: 16.hs ../Utils.hs ../Range.hs 27 | 17: 17.hs ../Utils.hs 28 | 18: 18.hs 29 | 19: 19.hs ../Utils.hs 30 | 20: 20.hs ../Utils.hs ../Direction.hs 31 | 21: 21.hs ../Utils.hs 32 | 22: 22.hs 33 | 23: 23.cpp 34 | g++ -O3 -o $@ $^ 35 | 24: 24.hs ../Vector.hs 36 | 25: 25.hs ../Utils.hs ../Modulo.hs 37 | 38 | 18.hs: 18.sh 18.txt 39 | ./18.sh 40 | -------------------------------------------------------------------------------- /2020/README.md: -------------------------------------------------------------------------------- 1 | # Advent of Code 2020 2 | 3 | My solutions for the [Advent of Code 2020](https://adventofcode.com/2020) 4 | 5 | I use these types of challenges as an excuse to learn (or brush up on) programming languages I'm not particularly familiar with. But this year has been draining, so I'm going to extend from my work last year, and continue to use Haskell. 6 | 7 | So don't expect this code to be particularly efficient, clean, or idiomatic. 8 | 9 | But, it works. 10 | 11 | For each puzzle, eg `01.hs` is my code, and `01.txt` is my version of the data file from AOC. `01.md` is my diary of comments on the puzzle, and my solution for it. Some common code between the puzzles has been collected in `../Utils.hs`. 12 | 13 | The diary pages also include my leaderboard positions for the puzzles for which I was able to be around when the puzzle opened, to race for the charts. I'm not able to do this every day, as in my timezone they open during business hours and sometimes work just gets in the way. But I enjoy it when they do. When I'm racing for the chart, often I'll do my first attempt in Python, as it's a language I'm much more familiar with (and can code in quickly), and only after I've submitted my solution, I'll go back and re-solve the puzzle in Haskell. So the code you see committed here isn't the code I wrote during the race, that code is 100% of the time an absolute mess that isn't worth sharing (and anyway most of the time is less a script that can be shared, and more a bunch of ad-hoc stuff that was run in the REPL). 14 | -------------------------------------------------------------------------------- /2021/01.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.List 3 | import Control.Exception 4 | 5 | getInput :: IO [Integer] 6 | getInput = do 7 | dat <- readFile "01.txt" 8 | return $ map read $ lines dat 9 | 10 | numIncreases :: [Integer] -> Integer 11 | numIncreases xs = genericLength $ filter (uncurry (<)) $ zip xs (tail xs) 12 | 13 | numLongIncreases :: [Integer] -> Integer 14 | numLongIncreases xs = genericLength $ filter (uncurry (<)) $ zip xs (drop 3 xs) 15 | 16 | tests :: IO () 17 | tests = do 18 | check $ numIncreases values == 7 19 | check $ numLongIncreases values == 5 20 | where 21 | values = [199, 200, 208, 210, 200, 207, 240, 269, 260, 263] 22 | check True = return () 23 | check False = throwIO $ AssertionFailed "test failed" 24 | 25 | main :: IO () 26 | main = do 27 | tests 28 | dat <- getInput 29 | print $ numIncreases dat 30 | print $ numLongIncreases dat 31 | -------------------------------------------------------------------------------- /2021/01.md: -------------------------------------------------------------------------------- 1 | # 1 – Smoothing the data 2 | Another nice and simple first puzzle, getting our feet wet. 3 | 4 | Main points of interest in this solution are the idiom of `zip xs (tail xs)` to scan through a list with a two-element window, to compare each value to its neighbour, and also recognising that the second part boils down to just comparing the value that's _entering_ the smoothing window with the one that is _existing_ it, so we can skip the smoothing and just directly compare values that are 3 apart. 5 | -------------------------------------------------------------------------------- /2021/02.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.List 3 | import Control.Exception 4 | 5 | data Opcode = Forward | Down | Up deriving Eq 6 | type Operation = (Opcode, Integer) 7 | 8 | getInput :: IO [Operation] 9 | getInput = do 10 | dat <- readFile "02.txt" 11 | return $ parseInput dat 12 | parseInput :: String -> [Operation] 13 | parseInput dat = do 14 | line <- lines dat 15 | let [opcode, amount] = words line 16 | return (parseOpcode opcode, read amount) 17 | parseOpcode "forward" = Forward 18 | parseOpcode "up" = Up 19 | parseOpcode "down" = Down 20 | 21 | partA :: [Operation] -> (Integer, Integer) 22 | partA ops = (forwards, downs - ups) 23 | where 24 | forwards = sum $ map snd $ filter ((==Forward).fst) ops 25 | downs = sum $ map snd $ filter ((==Down).fst) ops 26 | ups = sum $ map snd $ filter ((==Up).fst) ops 27 | 28 | partB :: [Operation] -> (Integer, Integer) 29 | partB ops = fst $ foldl doOp ((0,0),0) ops 30 | where 31 | doOp ((x, y), aim) (Forward, n) = ((x + n, y + n*aim), aim) 32 | doOp ((x, y), aim) (Down, n) = ((x, y), aim + n) 33 | doOp ((x, y), aim) (Up, n) = ((x, y), aim - n) 34 | 35 | tests :: IO () 36 | tests = do 37 | check $ partA sample == (15, 10) 38 | check $ partB sample == (15, 60) 39 | where 40 | sample = parseInput "forward 5\ndown 5\nforward 8\nup 3\ndown 8\nforward 2" 41 | check True = return () 42 | check False = throwIO $ AssertionFailed "test failed" 43 | 44 | main :: IO () 45 | main = do 46 | tests 47 | dat <- getInput 48 | print $ uncurry (*) $ partA dat 49 | print $ uncurry (*) $ partB dat 50 | -------------------------------------------------------------------------------- /2021/02.md: -------------------------------------------------------------------------------- 1 | # 2 – Up Up Down Down Back Forward Back Forward 2 | The first half of this one is pretty simple, as all of the steps are interchangeable... you can just pull out all the horizontal steps and all the vertical steps, and add them up separately. 3 | 4 | The second half, however, is not so easy, since the effect of each `forward` step depends on the current state. There would be ways to do it that were a bit "cleverer" (like, pull out all the vertical operations and `scan` over those to make a list of partial-sums for the aim, then zip that with the horizontal operations to calculate the actual movement) but really it didn't save much and was quite a bit more work, so I just went with the direct state-passing `fold`. 5 | -------------------------------------------------------------------------------- /2021/03.md: -------------------------------------------------------------------------------- 1 | # 3 – Crunching the bits 2 | This was an interesting puzzle... none of the things that it needs done are, individually, all that complicated. But their combination is weird, you spend half the time just rereading the puzzle, trying to get your head around exactly what weird sequence of events it wants you to do. But once you figure it out, it's not that hard to put it all together. 3 | 4 | [151/21] 5 | -------------------------------------------------------------------------------- /2021/05.md: -------------------------------------------------------------------------------- 1 | # 5 – Some of these vent courses, intersecting at crosses 2 | The strategy here is reasonably simple: for each of the lines, we make a list of all of the grid points that line touches, and then count up, for each point, how many times it appears in the combined list. 3 | 4 | Threw together a utility function to combine together the points, based of Python's `collections.Counter` class, which builds a dictionary of values to counts, and increments the count for each value as it comes in. 5 | 6 | There is also a bit of complication for listing all the grind points touched by the diagonal lines, which is still a little messy, but my solution hinges on having a flag variable to indicate the slope of the diagonal (either `1` or `-1`) and using this as a multiplier on the counter – if the slope is `1`, then as the `x` value increases, the `y` should also increase, while if the slope is `-1` then as the `x` value increases, the `y` value should decrease. So, indexing the points as `x+i` and `y+i*slope` would count in the right direction according to the slope. (The actual formula used in the code is a little different to this, as it's re-arranged so that `x` is the free variable, since that matched up better with the other code already written.) 7 | 8 | [18/11] 9 | -------------------------------------------------------------------------------- /2021/06.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.List 3 | import Data.Maybe 4 | import qualified Data.Map.Strict as M 5 | import Control.Exception 6 | import Utils 7 | 8 | type Point = (Integer, Integer) 9 | type Line = (Point, Point) 10 | 11 | getInput :: IO [Integer] 12 | getInput = do 13 | dat <- readFile "06.txt" 14 | return $ map read $ split ',' dat 15 | 16 | getCounts :: [Integer] -> [Integer] 17 | getCounts vals = map (fromMaybe 0 . (countmap M.!?)) [0..8] 18 | where countmap = counter vals 19 | 20 | doStep :: [Integer] -> [Integer] 21 | doStep vals = withresets 22 | where 23 | (zeros:rest) = vals 24 | nextvals = rest ++ [0] 25 | withresets = [if i == 6 || i == 8 then n+zeros else n | (i,n) <- zip [0..] nextvals] 26 | 27 | doSteps :: Integer -> [Integer] -> Integer 28 | doSteps n vals = sum $ iterate doStep vals `genericIndex` n 29 | 30 | tests :: IO () 31 | tests = do 32 | check $ (doSteps 80 $ getCounts vals) == 5934 33 | check $ (doSteps 256 $ getCounts vals) == 26984457539 34 | where 35 | vals = [3,4,3,1,2] 36 | check True = return () 37 | check False = throwIO $ AssertionFailed "test failed" 38 | 39 | main :: IO () 40 | main = do 41 | tests 42 | vals <- getInput 43 | print $ doSteps 80 $ getCounts vals 44 | print $ doSteps 256 $ getCounts vals 45 | -------------------------------------------------------------------------------- /2021/06_roots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/e3fe0fbd8a26e4e0941ed72a032202416f1cc20c/2021/06_roots.png -------------------------------------------------------------------------------- /2021/07.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Control.Exception 3 | import Utils 4 | 5 | getInput :: IO [Integer] 6 | getInput = do 7 | dat <- readFile "07.txt" 8 | return $ map read $ split ',' dat 9 | 10 | metricA :: Integer -> Integer -> Integer 11 | metricA a b = abs (a - b) 12 | metricB :: Integer -> Integer -> Integer 13 | metricB a b = d * (d+1) `div` 2 14 | where d = abs (a - b) 15 | 16 | optimise metric vals = minimum $ map calcCost $ [minimum vals .. maximum vals] 17 | where calcCost target = sum $ map (metric target) vals 18 | 19 | tests :: IO () 20 | tests = do 21 | check $ optimise metricA vals == 37 22 | check $ optimise metricB vals == 168 23 | where 24 | vals = [16,1,2,0,4,2,7,1,2,14] 25 | check True = return () 26 | check False = throwIO $ AssertionFailed "test failed" 27 | 28 | main :: IO () 29 | main = do 30 | tests 31 | vals <- getInput 32 | print $ optimise metricA vals 33 | print $ optimise metricB vals 34 | -------------------------------------------------------------------------------- /2021/07.md: -------------------------------------------------------------------------------- 1 | # 7 – A rather average puzzle 2 | This one was interesting, because it _should_ be possible to solve it directly by analysis. For the first part, the ideal target position is the median of the input values (if there's an even number of input values, then anywhere between the two middle values will work the same). And for the second part, it should be _roughly_ the mean of the values, as the cost is _roughly_ the square of the distance (and so the total cost is _roughly_ the variance). 3 | 4 | However, given that it's not _exactly_ the mean, and also the target value has to be an integer (which the mean isn't)... I decided it was more effort than it was worth to solve this analytically, and just stuck with the direct brute-force method of calculating it for every position in the range, and finding the best option. 5 | 6 | [140/47] 7 | -------------------------------------------------------------------------------- /2021/08.md: -------------------------------------------------------------------------------- 1 | # 8 – Der Blinkenlights 2 | There are many strategies that could be used to identify the different digits here... we could try identifying each segment individually (segment `a` is the only one that appears in the `7` but not in the `1`, for example). Or any of a number of different identifying features of the different digits. 3 | 4 | The ones I went with are basically the first ones that came to mind when looking at the shapes of the different digits. 5 | 6 | There are three digits that use 5 segments: 7 | * `3` is the only one that uses both of the segments in `1` 8 | * `5` is the only one that uses both of the segments that are in `4` but _not_ in `1` 9 | * `2` is the one that's left after identifying the others 10 | 11 | There are three digits that use 6 segments: 12 | * `9` is the only one that uses all of the segments in `4` 13 | * `6` is the only one that _doesn't_ use both of the segments in `1` 14 | * `0` is the one that's left after identifying the others 15 | 16 | I'm sure there are other, possibly simpler ways to identify them, but defining it all in terms of subsets (and other set operations) was an easy and clean way to build it all up. 17 | 18 | [94/44] 19 | -------------------------------------------------------------------------------- /2021/09.md: -------------------------------------------------------------------------------- 1 | # 9 – Flooding in the valley 2 | This puzzle is quite interesting, from a puzzle-design point of view... because the description of the puzzle implies a lot of things about the structure of the heightmap. 3 | 4 | In particular, the requirement that every point that isn't a `9` will always flow into exactly one basin means that the boundary between any two basins _must_ be lined with `9`s. Otherwise, you'd have a structure like `012343210` where the `4` in the middle is ambiguous as to which basin it "belongs" to. So the heightmap must be a bunch of regions bounded by `9`s, and then each region has a single local-minimum point within it. 5 | 6 | So you _could_ solve this puzzle by ignoring all the stuff about smoke flowing downhill, and instead just divide the grid up into regions based on the `9`s, and then take the minimum value in each region (for part 1) and find the size of each region (for part 2). 7 | 8 | Ultimately, that's not what I did, and my solution goes a bit more by-the-books to find its answers – for part 1, I scan the grid to find all the local minima, and for part 2, I do a flood fill from each local minimum, hill-climbing in every direction, until it hits the wall of `9`s, to find the size of the basin. 9 | 10 | Unfortunately, I wasn't able to do this one right as it was released. I still timed myself, and I believe I _would_ have gotten [55/17] based on my times, but that's just an estimate and in reality I got no points for this one. 11 | -------------------------------------------------------------------------------- /2021/09.py: -------------------------------------------------------------------------------- 1 | dat = """...snip...""".split("\n") 2 | dat = [[int(j) for j in i] for i in dat] 3 | CY = len(dat) 4 | CX = len(dat[0]) 5 | def n(y,x): 6 | if y > 0: 7 | yield y-1,x 8 | if y < CY-1: 9 | yield y+1,x 10 | if x > 0: 11 | yield y,x-1 12 | if x < CX-1: 13 | yield y,x+1 14 | s=0 15 | l = [] 16 | for y in range(CY): 17 | for x in range(CX): 18 | if all(dat[y][x] < dat[ny][nx] for ny,nx in n(y,x)) and dat[y][x] < 9: 19 | s+=dat[y][x]+1 20 | l.append((y,x)) 21 | print(s) 22 | bs = [] 23 | for by,bx in l: 24 | seen = set() 25 | todo = [(by,bx)] 26 | while todo: 27 | y,x = todo.pop() 28 | if (y,x) in seen: 29 | continue 30 | seen.add((y,x)) 31 | for ny,nx in n(y,x): 32 | if dat[ny][nx] > dat[y][x] and dat[ny][nx] < 9: 33 | todo.append((ny,nx)) 34 | bs.append(len(seen)) 35 | bs.sort() 36 | print(bs[-1] * bs[-2] * bs[-3]) 37 | -------------------------------------------------------------------------------- /2021/10.md: -------------------------------------------------------------------------------- 1 | # 10 – Putting another puzzle onto the stack 2 | Matching pairs of brackets is an old classic, for handling with a stack. Read through the string from left to right, and for each opening bracket, push that bracket type onto the stack, and for each closing bracket, pop it off the stack, so the stack is a representation of how many nested brackets you're currently inside. If you ever get a closing bracket that doesn't match the head of the stack, or you try to pop off an empty stack, then you have mismatched brackets, so that's part 1. If you get to the end of your string and the stack isn't empty, then your brackets are incomplete, so that's part 2. 3 | 4 | From there it's just a question of calculating the scores, and bookkeeping to get the final answers. And reading closely enough to notice that part 1 wants the _sum_ of the scores, while part 2 wants the _median_ of the scores. 5 | 6 | [102/66] 7 | -------------------------------------------------------------------------------- /2021/10.py: -------------------------------------------------------------------------------- 1 | dat = """...snipped 100 lines...""".split("\n") 2 | mapping = {'[':']','(':')','{':'}','<':'>'} 3 | points = {']':57,')':3,'}':1197,'>':25137} 4 | points2 = {c:i for i,c in enumerate('([{<',1)} 5 | def check(s): 6 | stack = [] 7 | for i,c in enumerate(s): 8 | if c in '([{<': 9 | stack.append(c) 10 | else: 11 | a = mapping[stack.pop()] 12 | if a != c: 13 | return points[c], 0 14 | score = 0 15 | for c in stack[::-1]: 16 | score = score * 5 + points2[c] 17 | return 0, score 18 | print(sum(check(i)[0] for i in dat)) 19 | a = [res[1] for i in dat for res in [check(i)] if res[0] == 0] 20 | a.sort() 21 | #print(len(a)) 22 | print(a[len(a)//2]) 23 | -------------------------------------------------------------------------------- /2021/11.md: -------------------------------------------------------------------------------- 1 | # 11 – Watchen der blinkenmollusk 2 | This one was a bit weird to implement in Haskell, because the whole flashing mechanism feels very procedural... there is a long sequence of things that happen (some octopuses flash, which increments other octopuses, which then might also flash, which propagates again, but doesn't propagate _back_ to the first octopus, and then they're all set to 0 at the end), this sequence includes potential loops, and carrying state through, and mutating the grid in various ways... all of which would be natural in a procedural langauge, but are a bit cumbersome here. 3 | 4 | That said, once the increment function was built, the rest of the structure was pretty simple. I do love when a problem lends itself to using `unfold`, I find that function very satisfying to use. That gets us an infinite sequence of how many flashes on each step, so for part 1 we just add up the first 100 steps, and for part 2, we run it until there are 100 flashes on a single step. 5 | -------------------------------------------------------------------------------- /2021/12.md: -------------------------------------------------------------------------------- 1 | # 12 – 1 path through the maze, ah, ah, ah... 2 | I missed this one at launch, and then when I went back to look at it afterwards I saw the description and just... no, I'd rather not, thank you. So I left this one for a while, and only went back to it after day 16. 3 | 4 | Turns out it wasn't as bad as I feared – the maze in the input is small enough, that I got away with not memoizing the path-counting function... so if it revisits a node, with the same state, it'll still walk through every path from that node again. It essentially brute-forces every possible path through the maze, without any cleverness. And yet it's still able to solve both parts of the puzzle in about a quarter of a second. But it would definitely struggle on a maze that was a bit larger, and there is definitely room for improvement, performance-wise. 5 | -------------------------------------------------------------------------------- /2021/13.md: -------------------------------------------------------------------------------- 1 | # 13 – A fold of folds 2 | A nice easy puzzle again after yesterday's puzzle (which I missed for other commitments and haven't gone back to catch up on just yet)... 3 | 4 | A slight hiccup during the race that I didn't notice that part 1 only wanted you to do the first fold, not all of the folds. Since the worked example went through both folds in the sample data, I missed the little bit down the bottom that said to only do the first fold. 5 | 6 | But, as an upside, the time I spent debugging this was mostly spent in building a method to visualise the grid, so I could check it was folding correctly... which I figured wasn't going to be a waste, because I guessed that part 2 would involve seeing what the final folded pattern looked like. Which did end up being borne out. 7 | 8 | After the race, reimplenting in Haskell, I had a different problem... that is, I'm writing all this code to do folds of this grid, implemented in functions that are, naturally, named with assorted variations of the word `fold`. But then... I have this function to take an entire grid and do a single fold to it. And now I want to make a function that takes a grid and does a _sequence_ of folds to it. The functional-programming pattern for taking an object and a list of operations, and doing them in sequence, is called... a `fold` (or `reduce`, depending on who you ask, but Haskell calls them `fold`s). 9 | 10 | So, I ended up with this one line in the code that reads 11 | ```hs 12 | doFolds folds grid = foldl foldGrid grid folds 13 | ``` 14 | and... it made perfect sense when I _wrote_ it... 15 | 16 | [58/9] - first single-digit finish of the year! 17 | -------------------------------------------------------------------------------- /2021/13.py: -------------------------------------------------------------------------------- 1 | points = [(6,10), (0,14), (9,10), (0,3), (10,4), (4,11), (6,0), (6,12), (4,1), (0,13), (10,12), (3,4), (3,0), (8,4), (1,10), (2,14), (8,10), (9,0),] 2 | folds = [(1,7), (0,5),] 3 | 4 | for j,(d,f) in enumerate(folds): 5 | for i,p in enumerate(points): 6 | if p[d] > f: 7 | p = list(p) 8 | p[d] = f - (p[d] - f) 9 | points[i] = tuple(p) 10 | if j == 0: 11 | print(len(set(points))) 12 | 13 | points = set(points) 14 | xmin = min(p[0] for p in points) 15 | xmax = max(p[0] for p in points) 16 | ymin = min(p[1] for p in points) 17 | ymax = max(p[1] for p in points) 18 | for y in range(ymin,ymax+1): 19 | for x in range(xmin,xmax+1): 20 | if (x,y) in points: 21 | print('#',end='') 22 | else: 23 | print(' ',end='') 24 | print() 25 | -------------------------------------------------------------------------------- /2021/15.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.Array 3 | import Data.Char 4 | import Data.Ix 5 | import Control.Exception 6 | import Dijkstra 7 | import Direction 8 | import Utils 9 | 10 | type Point = (Integer, Integer) 11 | type Grid = Array Point Integer 12 | 13 | getInput :: IO Grid 14 | getInput = do 15 | dat <- readFile "15.txt" 16 | return $ parseInput dat 17 | parseInput :: String -> Grid 18 | parseInput = listArrayLen2 . map (map (toInteger.digitToInt)) . lines 19 | 20 | solveGrid :: Grid -> Integer -> Integer 21 | solveGrid grid multiple = distance 22 | where 23 | ((0, 0), (xmax, ymax)) = bounds grid 24 | width = xmax + 1; height = ymax + 1 25 | startloc = (0,0) 26 | endloc = (width * multiple - 1, height * multiple - 1) 27 | expandedBounds = (startloc, endloc) 28 | 29 | neighbours p = [ (p', d, getWeight p') | d <- directions, let p' = step d p, inRange expandedBounds p' ] 30 | getWeight (x, y) = modulo $ grid ! (subx, suby) + repx + repy 31 | where (repx, subx) = x `divMod` width; (repy, suby) = y `divMod` height 32 | modulo n = (n - 1) `mod` 9 + 1 33 | 34 | Just (_, distance) = findNearest expandedBounds startloc neighbours (==endloc) 35 | 36 | tests :: IO () 37 | tests = do 38 | check $ (solveGrid sample 1) == 40 39 | check $ (solveGrid sample 5) == 315 40 | where 41 | sample = parseInput "1163751742\n1381373672\n2136511328\n3694931569\n7463417111\n1319128137\n1359912421\n3125421639\n1293138521\n2311944581" 42 | check True = return () 43 | check False = throwIO $ AssertionFailed "test failed" 44 | 45 | main :: IO () 46 | main = do 47 | tests 48 | grid <- getInput 49 | print $ solveGrid grid 1 50 | print $ solveGrid grid 5 51 | -------------------------------------------------------------------------------- /2021/15.py: -------------------------------------------------------------------------------- 1 | dat="""1964778752979887222739789777935919929996793679617497991954953881381939846468999159686925929898196249 2 | [...]""" 3 | #dat = """1163751742\n1381373672\n2136511328\n3694931569\n7463417111\n1319128137\n1359912421\n3125421639\n1293138521\n2311944581""" 4 | dat = [[int(c) for c in i] for i in dat.split("\n")] 5 | 6 | CY = len(dat) 7 | CX = len(dat[0]) 8 | N = 5 9 | totrisk = [[None for x in range(CX*N)] for y in range(CY*N)] 10 | totrisk[0][0] = 0 11 | candidates = {(0,0)} 12 | done = set() 13 | ndone = 1 14 | while (CX*N-1,CY*N-1) not in done: 15 | minval = minat = None 16 | for y,x in candidates: 17 | if minval is None or totrisk[y][x] < minval: 18 | minval = totrisk[y][x] 19 | minat = y,x 20 | y,x = minat 21 | candidates.remove((y,x)) 22 | done.add((y,x)) 23 | for dx,dy in [(-1,0),(1,0),(0,-1),(0,1)]: 24 | nx = x + dx 25 | ny = y + dy 26 | if 0 <= nx < CX*N and 0 <= ny < CY*N: 27 | val = totrisk[y][x] + (dat[ny%CY][nx%CX] + (ny//CY) + (nx//CX) - 1) % 9 + 1 28 | if totrisk[ny][nx] is None or totrisk[ny][nx] > val: 29 | totrisk[ny][nx] = val 30 | candidates.add((ny,nx)) 31 | print(totrisk[-1][-1]) 32 | -------------------------------------------------------------------------------- /2021/16.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | dat = """4057231006[...]02A9336C20CE84""" 3 | #dat = "EE00D40C823060" 4 | b = lambda n: ("0000"+bin(n)[2:])[-4:] 5 | dat = StringIO(''.join(b(int(n,16)) for n in dat)) 6 | ver_total = 0 7 | def readpacket(): 8 | v = int(dat.read(3),2) 9 | global ver_total 10 | ver_total += v 11 | t = int(dat.read(3),2) 12 | if t == 4: 13 | n = readnum() 14 | return (v,t,n) 15 | else: 16 | i = int(dat.read(1),2) 17 | if i == 0: 18 | l = int(dat.read(15),2) 19 | target = dat.tell() + l 20 | packets = [] 21 | while dat.tell() < target: 22 | packets.append(readpacket()) 23 | else: 24 | n = int(dat.read(11),2) 25 | packets = [readpacket() for i in range(n)] 26 | return (v,t,packets) 27 | def readnum(): 28 | n = 0 29 | cont = True 30 | while cont: 31 | cont = int(dat.read(1),2) 32 | n = (n << 4) + int(dat.read(4),2) 33 | return n 34 | packet = readpacket() 35 | print(ver_total) 36 | def eval(pack): 37 | v,t,val = pack 38 | if t == 0: 39 | return sum(eval(i) for i in val) 40 | elif t == 1: 41 | ret = 1 42 | for i in val: 43 | ret *= eval(i) 44 | return ret 45 | elif t == 2: 46 | return min(eval(i) for i in val) 47 | elif t == 3: 48 | return max(eval(i) for i in val) 49 | elif t == 4: 50 | return val 51 | elif t == 5: 52 | return 1 if eval(val[0]) > eval(val[1]) else 0 53 | elif t == 6: 54 | return 1 if eval(val[0]) < eval(val[1]) else 0 55 | elif t == 7: 56 | return 1 if eval(val[0]) == eval(val[1]) else 0 57 | print(eval(packet)) 58 | -------------------------------------------------------------------------------- /2021/17.py: -------------------------------------------------------------------------------- 1 | from math import sqrt,floor,ceil 2 | xmin,xmax=(14,50) 3 | ymin,ymax=(-267,-225) 4 | #xmin,xmax,ymin,ymax=20,30,-10,-5 5 | 6 | def project(vx,vy): 7 | x,y = 0,0 8 | while True: 9 | x += vx 10 | y += vy 11 | if vx > 0: 12 | vx -= 1 13 | elif vx < 0: 14 | vx += 1 15 | vy -= 1 16 | #print(x,y,vx,vy) 17 | if xmin <= x <= xmax and ymin <= y <= ymax: 18 | return 0 19 | elif x > xmax: 20 | return 1 21 | elif y < ymin: 22 | return -1 23 | 24 | max_vy = abs(ymin) - 1 25 | print(max_vy * (max_vy+1) // 2) 26 | 27 | count = 0 28 | for vx in range(0,xmax+1): 29 | for vy in range(ymin, max_vy+1): 30 | if project(vx,vy) == 0: 31 | count += 1 32 | print(count) 33 | -------------------------------------------------------------------------------- /2021/18.py: -------------------------------------------------------------------------------- 1 | dat = [ 2 | [3,[5,[7,[3,9]]]], 3 | [[[[7,0],0],[2,[2,8]]],[[[7,8],1],3]], 4 | [...] 5 | ] 6 | 7 | from copy import deepcopy 8 | def add(a,b): 9 | return reduce([a,b]) 10 | def reduce(x): 11 | x = deepcopy(x) 12 | cont = True 13 | while cont: 14 | cont = False 15 | stack = [(0,x,[])] 16 | while stack: 17 | n,v,p = stack.pop() 18 | if not isinstance(v, list): 19 | continue 20 | if n >= 4: 21 | for a, ix in p: 22 | if ix > 0: 23 | while isinstance(a[ix-1], list): 24 | a = a[ix-1] 25 | ix = len(a) 26 | a[ix-1] += v[0] 27 | break 28 | for a, ix in p: 29 | if ix < len(a)-1: 30 | while isinstance(a[ix+1], list): 31 | a = a[ix+1] 32 | ix = -1 33 | a[ix+1] += v[1] 34 | break 35 | p[0][0][p[0][1]] = 0 36 | cont = True 37 | break 38 | else: 39 | for i,sub in list(enumerate(v))[::-1]: 40 | stack.append((n+1,sub,[[v,i]]+p)) 41 | if cont: 42 | continue 43 | 44 | stack = [(0,x,None,None)] 45 | while stack: 46 | n,v,p,ix = stack.pop() 47 | if not isinstance(v, list): 48 | if v >= 10: 49 | p[ix] = [v // 2, (v+1) // 2] 50 | cont = True 51 | break 52 | else: 53 | for i,sub in list(enumerate(v))[::-1]: 54 | stack.append((n+1,sub,v,i)) 55 | return x 56 | 57 | def magnitude(x): 58 | if isinstance(x, list): 59 | return 3*magnitude(x[0]) + 2*magnitude(x[1]) 60 | else: 61 | return x 62 | 63 | sum = dat[0] 64 | for i in dat[1:]: 65 | sum = add(sum, i) 66 | print(magnitude(sum)) 67 | 68 | maxval = -1 69 | for i in dat: 70 | for j in dat: 71 | val = magnitude(add(i,j)) 72 | maxval = max(maxval, val) 73 | print(maxval) 74 | -------------------------------------------------------------------------------- /2021/20.md: -------------------------------------------------------------------------------- 1 | # 20 – Your cells. They're so... automated. 2 | Ah, every year, there's always a cellular automaton puzzle. Sometimes, more than one. 3 | 4 | This one isn't really anything different than what we'd had before, but it does have one very subtle trap: the puzzle _does not_ say that the cellular grid expands every step, and the space it expands into is set to "off". What it _does_ say is that the _initial_ state is surrounded by an infinite grid of "off" cells. You're just expected to read that second thing and then implement it as the first thing. And, with the example they work through in the puzzle, this assumption works fine, so naturally you will go on to make that assumption in your code, which then works against the example testcase. However, when you plug in the real input file, suddenly you're getting the wrong answer. 5 | 6 | And why? Because the rule in the example, has a `.` as its first character, while the rule in the provided input file starts with a `#` (and ends with a `.`). This means that while the example grid is gradually growing into an infinite grid of "off" cells, as you might assume... instead the _real_ input is gradually growing into an infinite grid of cells that _are alternating_ between "off" and "on" every step. 7 | 8 | This is why part 1 makes you take two steps and then count how many cells there are – on the odd steps, there are an infinite number of active cells on the grid, as it includes every cell outside the main area. 9 | 10 | But with that hiccup sorted, the rest of this is just another cellular automaton. I really should just make a cellular automaton runner util, I'm sure this is going to come up again in future years. Or past years, when I get around to going back to those. 11 | 12 | [22/57] 13 | -------------------------------------------------------------------------------- /2021/21.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | p = [5, 10] 4 | score = [0,0] 5 | turn = 0 6 | 7 | state = 0 8 | rollcount = 0 9 | def det_roll(): 10 | global state, rollcount 11 | rollcount += 1 12 | state += 1 13 | if state > 100: 14 | state -= 100 15 | return state 16 | while True: 17 | roll = det_roll() + det_roll() + det_roll() 18 | p[turn] = (p[turn] + roll - 1) % 10 + 1 19 | score[turn] += p[turn] 20 | if score[turn] >= 1000: 21 | break 22 | turn = 1 - turn 23 | print(score[1-turn] * rollcount) 24 | 25 | 26 | p = [5,10] 27 | #p = [4,8] 28 | todo_states = defaultdict(int) 29 | todo_states[tuple(p),(0,0),0] = 1 30 | win_universes = [0,0] 31 | while todo_states: 32 | pos, score, turn = min(todo_states.keys(), key=lambda x:x[1][0]+x[1][1]) 33 | count = todo_states.pop((pos, score, turn)) 34 | for i in range(1,4): 35 | for j in range(1,4): 36 | for k in range(1,4): 37 | newpos = list(pos) 38 | newpos[turn] = (newpos[turn] + i + j + k - 1) % 10 + 1 39 | newscore = list(score) 40 | newscore[turn] += newpos[turn] 41 | if newscore[turn] >= 21: 42 | win_universes[turn] += count 43 | else: 44 | todo_states[tuple(newpos), tuple(newscore), 1-turn] += count 45 | #print(win_universes) 46 | print(max(win_universes)) 47 | -------------------------------------------------------------------------------- /2021/22a.py: -------------------------------------------------------------------------------- 1 | dat = [ 2 | (True,-6,41,-12,39,-10,42), (True,-33,13,-34,15,3,47), ... 3 | ] 4 | 5 | state = [[[False for z in range(101)] for y in range(101)] for x in range(101)] 6 | count = 0 7 | for s,xmin,xmax,ymin,ymax,zmin,zmax in dat: 8 | if xmax < -50 or ymax < -50 or zmax < -50 or xmin > 50 or ymin > 50 or zmin > 50: 9 | continue 10 | for x in range(xmin,xmax+1): 11 | for y in range(ymin,ymax+1): 12 | for z in range(zmin,zmax+1): 13 | if -50 <= x <= 50 and -50 <= y <= 50 and -50 <= z <= 50: 14 | if state[x][y][z] != s: 15 | state[x][y][z] = s 16 | if s: 17 | count += 1 18 | else: 19 | count -= 1 20 | print(count) 21 | -------------------------------------------------------------------------------- /2021/22b.py: -------------------------------------------------------------------------------- 1 | dat = [ 2 | (True,-6,41,-12,39,-10,42), (True,-33,13,-34,15,3,47), ... 3 | ] 4 | 5 | regions = set() 6 | def ol(min1,max1,min2,max2): 7 | if min1 < min2: 8 | yield (min1,min2-1) 9 | yield max(min1,min2), min(max1, max2) 10 | if max1 > max2: 11 | yield (max2+1, max1) 12 | def proc(s,xmin,xmax,ymin,ymax,zmin,zmax): 13 | #if xmax < -50 or ymax < -50 or zmax < -50 or xmin > 50 or ymin > 50 or zmin > 50: 14 | # return 15 | 16 | for xmin2, xmax2, ymin2, ymax2, zmin2, zmax2 in regions: 17 | if (xmin2, xmax2, ymin2, ymax2, zmin2, zmax2) == (xmin,xmax,ymin,ymax,zmin,zmax): 18 | break 19 | if xmin <= xmax2 and xmin2 <= xmax and ymin <= ymax2 and ymin2 <= ymax and zmin <= zmax2 and zmin2 <= zmax: 20 | regions.remove((xmin2,xmax2,ymin2,ymax2,zmin2,zmax2)) 21 | for xmin3, xmax3 in ol(xmin2, xmax2, xmin, xmax): 22 | for ymin3, ymax3 in ol(ymin2, ymax2, ymin, ymax): 23 | for zmin3, zmax3 in ol(zmin2, zmax2, zmin, zmax): 24 | regions.add((xmin3, xmax3, ymin3, ymax3, zmin3, zmax3)) 25 | for xmin3, xmax3 in ol(xmin, xmax, xmin2, xmax2): 26 | for ymin3, ymax3 in ol(ymin, ymax, ymin2, ymax2): 27 | for zmin3, zmax3 in ol(zmin, zmax, zmin2, zmax2): 28 | proc(s, xmin3, xmax3, ymin3, ymax3, zmin3, zmax3) 29 | return 30 | 31 | if s: 32 | regions.add((xmin,xmax,ymin,ymax,zmin,zmax)) 33 | elif (xmin,xmax,ymin,ymax,zmin,zmax) in regions: 34 | regions.remove((xmin,xmax,ymin,ymax,zmin,zmax)) 35 | for i,x in enumerate(dat[:2]): 36 | print(f" * {i}: {len(regions)}") 37 | proc(*x) 38 | 39 | import pprint 40 | pprint.pprint(regions) 41 | count = 0 42 | for xmin, xmax, ymin, ymax, zmin, zmax in regions: 43 | count += (xmax-xmin+1) * (ymax-ymin+1) * (zmax-zmin+1) 44 | print(count) 45 | -------------------------------------------------------------------------------- /2021/24.py: -------------------------------------------------------------------------------- 1 | dat = [ 2 | ("inp", 'w',),("mul", 'x', 0),("add", 'x', 'z'),("mod", 'x', 26), ... 3 | ] 4 | 5 | def dm(a,b): 6 | d = a // b 7 | m = a % b 8 | if d < 0 and m != 0: 9 | d += 1 10 | m -= b 11 | return d,m 12 | 13 | def run(input): 14 | vars = {k:0 for k in "wxyz"} 15 | def p(x): 16 | if isinstance(x, int): 17 | return x 18 | else: 19 | return vars[x] 20 | for opcode, *params in dat: 21 | if opcode == "inp": 22 | vars[params[0]] = input.pop(0) 23 | elif opcode == "add": 24 | vars[params[0]] += p(params[1]) 25 | elif opcode == "mul": 26 | vars[params[0]] *= p(params[1]) 27 | elif opcode == "div": 28 | if p(params[1]) == 0: 29 | raise "div by zero" 30 | vars[params[0]] = dm(p(params[0]), p(params[1]))[0] 31 | elif opcode == "mod": 32 | if p(params[1]) <= 0 or p(params[0]) < 0: 33 | raise "mod by invalid args" 34 | vars[params[0]] = dm(p(params[0]), p(params[1]))[1] 35 | elif opcode == "eql": 36 | vars[params[0]] = 1 if p(params[0]) == p(params[1]) else 0 37 | return vars['z'] == 0 38 | 39 | #from itertools import product 40 | #for inp in product("987654321", repeat=14): 41 | # if run([int(i) for i in inp]): 42 | # print(''.join(inp)) 43 | # break 44 | print(run([int(i) for i in "59996912981939"])) 45 | print(run([int(i) for i in "17241911811915"])) 46 | -------------------------------------------------------------------------------- /2021/25.md: -------------------------------------------------------------------------------- 1 | # 25 – We made it 2 | Ah, and as is tradition, a nice calm puzzle for the 25th to round it all out. 3 | 4 | Not a lot to talk about with this puzzle... we maintain a `Map` where the keys are positions of the cucumbers on the grid, and the values are which direction they are moving, to both make it easy to iterate over all the cucumbers each step, and also to easily check whether a given location is empty (and can be moved into). Then it's just a case of moving them all each step, and repeating until no changes are made to the grid. 5 | 6 | [20/16] 7 | -------------------------------------------------------------------------------- /2021/25.py: -------------------------------------------------------------------------------- 1 | dat = """.>.>.......v.v.....vvv>..v...>>>.>.v.>.v....>.v.vv.>..>.>>>.....v...>..>v..v.>>.>v>vv.>v.v>v>.>>>.>>>vvv.v>.v>>>.........vv.v.v>>.v>v>v..v. 2 | vv.....vvv.>.vv.vv.v..v..v>.>..v..v>>>..v.v.v>.>vvvv.>....v...>>.>..>>v.v>>v>>.>..v..>>>.>>.>.>.>v...vv.vv.>.>>>..v..>v>v>>...>v...v>>v>..v 3 | [...]""".split("\n") 4 | CY = len(dat) 5 | CX = len(dat[0]) 6 | 7 | cukes = {} 8 | for y,row in enumerate(dat): 9 | for x,c in enumerate(row): 10 | if c == '>': 11 | cukes[x,y] = True 12 | elif c == 'v': 13 | cukes[x,y] = False 14 | 15 | def step(x,y,dir): 16 | if dir: 17 | return (x+1)%CX,y 18 | else: 19 | return x,(y+1)%CY 20 | def dostep(dir): 21 | tomove = [] 22 | for x,y in cukes: 23 | if cukes[x,y] == dir: 24 | x2,y2 = step(x,y,dir) 25 | if (x2,y2) not in cukes: 26 | tomove.append((x,y,x2,y2)) 27 | for x,y,x2,y2 in tomove: 28 | del cukes[x,y] 29 | cukes[x2,y2] = dir 30 | return len(tomove) 31 | 32 | a = 1 33 | i = 0 34 | while a: 35 | a = dostep(True) 36 | a += dostep(False) 37 | i += 1 38 | print(i) 39 | -------------------------------------------------------------------------------- /2021/Makefile: -------------------------------------------------------------------------------- 1 | all: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 25 2 | 3 | clean: 4 | rm -f [0-9][0-9] *.hi *.o ../*.hi ../*.o 5 | 6 | .PHONY: all clean 7 | 8 | %: %.hs 9 | ghc -O2 -threaded -with-rtsopts="-N" -o $@ $^ 10 | 11 | 01: 01.hs 12 | 02: 02.hs 13 | 03: 03.hs ../Utils.hs 14 | 04: 04.hs ../Utils.hs 15 | 05: 05.hs ../Utils.hs 16 | 06: 06.hs ../Utils.hs 17 | 07: 07.hs ../Utils.hs 18 | 08: 08.hs ../Utils.hs 19 | 09: 09.hs ../Utils.hs 20 | 10: 10.hs ../Utils.hs 21 | 11: 11.hs ../Utils.hs 22 | 12: 12.hs ../Utils.hs 23 | 13: 13.hs ../Utils.hs 24 | 14: 14.hs ../Utils.hs 25 | 15: 15.hs ../Utils.hs ../Dijkstra.hs ../Direction.hs 26 | 16: 16.hs ../Utils.hs 27 | 17: 17.hs ../Utils.hs 28 | 18: 18.hs ../Utils.hs 29 | 19: 19.hs ../Utils.hs ../Vector.hs 30 | 20: 20.hs ../Utils.hs 31 | 21: 21.hs ../Utils.hs 32 | 22: 22.hs ../Utils.hs 33 | 23: 23.hs ../Utils.hs ../Dijkstra.hs ../Direction.hs 34 | # no 24 35 | 25: 25.hs ../Utils.hs ../Direction.hs 36 | -------------------------------------------------------------------------------- /2021/README.md: -------------------------------------------------------------------------------- 1 | # Advent of Code 2021 2 | 3 | My solutions for the [Advent of Code 2021](https://adventofcode.com/2021) 4 | 5 | I use these types of challenges as an excuse to learn (or brush up on) programming languages I'm not particularly familiar with. I'm continuing my trend from the past couple of years and continuing to practice with Haskell, a language I'm only somewhat familiar with. 6 | 7 | So don't expect this code to be particularly efficient, clean, or idiomatic. 8 | 9 | But, it works. 10 | 11 | For each puzzle, there are several files: 12 | * `01.hs` is my code to solve the puzzle. 13 | * `01.txt` is my version of the data file from AOC. 14 | * `01.md` is my diary of comments on the puzzle, and my thoughts on the solution. 15 | * `01.py` is the code I wrote while racing for the leaderboard, unpolished (not available for every puzzle). 16 | 17 | Some common code between the puzzles has been collected in `../Utils.hs`. 18 | 19 | The diary pages also include my leaderboard positions for the puzzles for which I was able to be around when the puzzle opened, to race for the charts. I'm not able to do this every day, as in my timezone they open during business hours and sometimes work just gets in the way. But I enjoy it when they do. When I'm racing for the chart, often I'll do my first attempt in Python, as it's a language I'm much more familiar with (and can code in quickly), and only after I've submitted my solution, I'll go back and re-solve the puzzle in Haskell. 20 | -------------------------------------------------------------------------------- /2022/01.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.List 3 | import Data.List.Split (splitOn) 4 | import Control.Exception 5 | 6 | getInput :: IO [[Integer]] 7 | getInput = do 8 | dat <- readFile "01.txt" 9 | return $ parseInput dat 10 | 11 | parseInput :: String -> [[Integer]] 12 | parseInput dat = map (map read . lines) $ splitOn "\n\n" dat 13 | 14 | sortElves :: [[Integer]] -> [[Integer]] 15 | sortElves = reverse . sortOn sum 16 | 17 | findTop :: Integer -> [[Integer]] -> Integer 18 | findTop n = sum . map sum . genericTake n . sortElves 19 | 20 | tests :: IO () 21 | tests = do 22 | check $ findTop 1 testData == 24000 23 | check $ findTop 3 testData == 45000 24 | where 25 | testData = parseInput "1000\n2000\n3000\n\n4000\n\n5000\n6000\n\n7000\n8000\n9000\n\n10000" 26 | check True = return () 27 | check False = throwIO $ AssertionFailed "test failed" 28 | 29 | main :: IO () 30 | main = do 31 | tests 32 | dat <- getInput 33 | print $ findTop 1 dat 34 | print $ findTop 3 dat 35 | -------------------------------------------------------------------------------- /2022/01.md: -------------------------------------------------------------------------------- 1 | # 1 – Getting sorted 2 | Ah, new year, new advent calendar, time to stretch these code-racing muscles again. What fun. 3 | 4 | While I've done AoC for a few years now, this is the first year where I've actually remembered in time to be here for the start of the event, so it's the first time I've actually attempted the leaderboard race on day 1. And... I probably _should_ have realised that the day-1 problem statement starts with several paragraphs of flavour text about the puzzles, and the stars, and whatnot, that I should quickly skip past when racing, I did not in fact realise this ahead of time... so I spent longer than necessary skimming through several paragraphs looking for the parts that are important to the puzzle and getting worried that I wasn't seeing anything. And when even 100th place on the leaderboard is finished in 2 minutes flat, that's not time you can waste. 5 | 6 | Regardless, it was a fun experience and I'll be better prepared next year. 7 | 8 | I was also a bit rusty and tried to write file-parsing code in my Python race submission, forgetting that my usual strategy is to just copy-paste my input value into the text editor (and possibly use some text-editor tools to pre-process it into a useful form) so I hit some unexpected hurdles with the blank lines and had to debug that (ended up just throwing a `if the line isn't blank` clause in the parser). 9 | 10 | Ready and looking forward to what the rest of the month brings! 11 | 12 | [386/299] 13 | -------------------------------------------------------------------------------- /2022/01.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | a = open("01.txt").read() 3 | a = [[int(j) for j in i.split("\n") if j] for i in a.split("\n\n")] 4 | print(max(sum(i) for i in a)) 5 | a.sort(key=sum) 6 | print(sum(j for i in a[-3:] for j in i)) 7 | -------------------------------------------------------------------------------- /2022/02.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.List 3 | import Data.List.Split (splitOn) 4 | import Control.Exception 5 | 6 | data Throw = Rock | Paper | Scissors deriving (Eq, Ord, Enum, Show, Read) 7 | data Result = Lose | Draw | Win deriving (Eq, Ord, Enum, Show, Read) 8 | 9 | getInput :: IO [(Throw, Throw)] 10 | getInput = do 11 | dat <- readFile "02.txt" 12 | return $ map parseInput $ lines dat 13 | 14 | parseInput :: String -> (Throw, Throw) 15 | parseInput [a, ' ', b] = (toThrow a 'A', toThrow b 'X') 16 | where toThrow x n = toEnum (fromEnum x - fromEnum n) 17 | 18 | toResult :: Throw -> Throw -> Result 19 | toResult you me = toEnum $ (fromEnum me - fromEnum you + 1) `mod` 3 20 | 21 | fromResult :: Throw -> Result -> Throw 22 | fromResult you res = toEnum $ (fromEnum you + fromEnum res - 1) `mod` 3 23 | 24 | reinterpret :: Throw -> Result 25 | reinterpret = toEnum . fromEnum 26 | 27 | score :: Throw -> Result -> Integer 28 | score t r = toInteger $ 1 + fromEnum t + 3 * fromEnum r 29 | 30 | scoreA :: Throw -> Throw -> Integer 31 | scoreA you me = score me res 32 | where res = toResult you me 33 | 34 | scoreB :: Throw -> Throw -> Integer 35 | scoreB you resT = score me res 36 | where res = reinterpret resT; me = fromResult you res 37 | 38 | tests :: IO () 39 | tests = do 40 | check $ (sum $ map (uncurry scoreA) testData) == 15 41 | check $ (sum $ map (uncurry scoreB) testData) == 12 42 | where 43 | testData = map parseInput ["A Y", "B X", "C Z"] 44 | check True = return () 45 | check False = throwIO $ AssertionFailed "test failed" 46 | 47 | main :: IO () 48 | main = do 49 | tests 50 | dat <- getInput 51 | print $ sum $ map (uncurry scoreA) dat 52 | print $ sum $ map (uncurry scoreB) dat 53 | -------------------------------------------------------------------------------- /2022/02.md: -------------------------------------------------------------------------------- 1 | # 2 – The King's game 2 | Ah, RPS, a classic. What I quickly realised is that I could use mod-3 arithmetic to quickly figure out who the winner was. What I was less immediately clever about was how to turn the input letters into numbers to do those calculations. 3 | 4 | Like, by thought process was pretty clearly "OK, I need the values to end up as `0`, `1` or `2`, so I'll type in those numbers, and then I'll pick it based on what letters show up"... except then the resulting code looked like 5 | ```py 6 | l = [0,1,2][ord(l) - ord('A')] 7 | r = [0,1,2][ord(r) - ord('X')] 8 | ``` 9 | which... like... it works? But come on, brain, I thought you were better than this. 10 | 11 | For part 2, I managed to get it done very quickly, with one extra line of code... but unfortunately, when typing that code, I made a typo (I put a `-` sign instead of a `%` in the formula) and it got me the wrong answer. So I hit the "you have to wait a minute before guessing again" page on the website. I'd submitted at the 0:05:21 mark, so if I hadn't made that mistake, I believe I would have come 38th for part 2 (and gotten 63 points). But alas. 12 | 13 | For the Haskell version, I'm using enums for everything, because type-safety... but in the function bodies I'm still dropping back to ints and using modular arithmetic, because it's a lot shorter to type in than doing it case-by-case with lookup tables. 14 | 15 | [224/111] 16 | -------------------------------------------------------------------------------- /2022/02.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | a = [ 4 | ("C","Y"),("B","Y"),[...snip...] 5 | ] 6 | #a = [('A','Y'),('B','X'),('C','Z')] 7 | s = 0 8 | for l, r in a: 9 | l = [0,1,2][ord(l) - ord('A')] 10 | r = [0,1,2][ord(r) - ord('X')] 11 | #r = (l + r - 1) % 3 # (uncomment this line for part 2) 12 | res = (r - l) % 3 13 | s += 1 + r + [3,6,0][res] 14 | #print(s) 15 | print(s) 16 | -------------------------------------------------------------------------------- /2022/03.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.List 3 | import Control.Exception 4 | import Utils 5 | 6 | getInput :: IO [String] 7 | getInput = do 8 | dat <- readFile "03.txt" 9 | return $ lines dat 10 | 11 | rateLetter :: Char -> Integer 12 | rateLetter c 13 | | 'a' <= c && c <= 'z' = toInteger $ fromEnum c - fromEnum 'a' + 1 14 | | 'A' <= c && c <= 'Z' = toInteger $ fromEnum c - fromEnum 'A' + 27 15 | 16 | partA :: [String] -> Integer 17 | partA = sum . map rate 18 | where 19 | rate s = rateLetter c 20 | where 21 | n = genericLength s `div` 2 22 | l = genericTake n s 23 | r = genericDrop n s 24 | [c] = nub $ l `intersect` r 25 | 26 | partB :: [String] -> Integer 27 | partB = sum . map rate . chunk 3 28 | where 29 | rate [x,y,z] = rateLetter c 30 | where [c] = nub $ x `intersect` y `intersect` z 31 | 32 | tests :: IO () 33 | tests = do 34 | check $ partA testData == 157 35 | check $ partB testData == 70 36 | where 37 | testData = ["vJrwpWtwJgWrhcsFMMfFFhFp","jqHRNqRjqzjGDLGLrsFMfFZSrLrFZsSL","PmmdzqPrVvPwwTWBwg","wMqvLMZHhHMvwLHjbvcjnnSBnvTQFn","ttgJtRGJQctTZtZT","CrZsJsPPZsGzwwsLwLmpwMDw"] 38 | check True = return () 39 | check False = throwIO $ AssertionFailed "test failed" 40 | 41 | main :: IO () 42 | main = do 43 | tests 44 | dat <- getInput 45 | print $ partA dat 46 | print $ partB dat 47 | -------------------------------------------------------------------------------- /2022/03.md: -------------------------------------------------------------------------------- 1 | # 3 – Set theory 2 | This one went real smooth during the race, using `set` operations. Reimplementing it Haskell using the builtin `intersect` gave me a little trouble at first, because I didn't realise that the one letter that was shared between the two strings could be in them multiple times, so `intersect` would return it multiple times (since it's an operation on lists, rather than on sets)... but once I figured that out, adding a call to `nub` fixed it right up. 3 | 4 | [79/35] 5 | -------------------------------------------------------------------------------- /2022/03.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | a = """ZNNvFWHqLNPZHHqPTHHnTGBhrrpjvmwfMmpfpjBjwpmw 3 | sbdzQgzgssgbglRtmjlwhjBlfrSrMt 4 | [...snip...]""".split("\n") 5 | b = [] 6 | for x in a: 7 | l = set(x[:len(x)//2]) 8 | r = set(x[len(x)//2:]) 9 | i, = l & r 10 | i = ord(i) 11 | if i > 0x60: 12 | i -= 0x60 13 | else: 14 | i -= 0x40 15 | i += 26 16 | b.append(i) 17 | print(sum(b)) 18 | 19 | b=[] 20 | for x in range(0, len(a), 3): 21 | l = set(a[x]) 22 | m = set(a[x+1]) 23 | r = set(a[x+2]) 24 | i, = l & m & r 25 | i = ord(i) 26 | if i > 0x60: 27 | i -= 0x60 28 | else: 29 | i -= 0x40 30 | i += 26 31 | b.append(i) 32 | print(sum(b)) 33 | -------------------------------------------------------------------------------- /2022/04.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.List 3 | import Control.Exception 4 | import qualified Text.ParserCombinators.ReadP as P 5 | import Utils 6 | 7 | type Range = (Integer, Integer) 8 | type Row = (Range, Range) 9 | 10 | getInput :: IO [Row] 11 | getInput = do 12 | dat <- readFile "04.txt" 13 | return $ map parseLine $ lines dat 14 | 15 | parseLine :: String -> Row 16 | parseLine = runReadP readLine 17 | where 18 | readInt = P.readS_to_P reads :: P.ReadP Integer 19 | readRange = do 20 | a <- readInt 21 | P.char '-' 22 | b <- readInt 23 | return (a, b) 24 | readLine = do 25 | a <- readRange 26 | P.char ',' 27 | b <- readRange 28 | return (a, b) 29 | 30 | hasSubset :: Row -> Bool 31 | hasSubset ((a,b),(x,y)) = (a <= x && y <= b) || (x <= a && b <= y) 32 | hasIntersect :: Row -> Bool 33 | hasIntersect ((a,b),(x,y)) = a <= y && b >= x 34 | 35 | partA :: [Row] -> Integer 36 | partA = genericLength . filter hasSubset 37 | partB :: [Row] -> Integer 38 | partB = genericLength . filter hasIntersect 39 | 40 | tests :: IO () 41 | tests = do 42 | check $ partA testData == 2 43 | check $ partB testData == 4 44 | where 45 | testData = map parseLine ["2-4,6-8","2-3,4-5","5-7,7-9","2-8,3-7","6-6,4-6","2-6,4-8"] 46 | check True = return () 47 | check False = throwIO $ AssertionFailed "test failed" 48 | 49 | main :: IO () 50 | main = do 51 | tests 52 | dat <- getInput 53 | print $ partA dat 54 | print $ partB dat 55 | -------------------------------------------------------------------------------- /2022/04.md: -------------------------------------------------------------------------------- 1 | # 4 – Out of bounds 2 | Another classic puzzle here, of intersecting ranges. The trick, for those unaware, is that if you have two ranges (call them `a-b` and `x-y`), the only way they can _not_ intersect is to have one entirely to one side of the other, ie `b < x || a > y`. So the intersection condition is the reverse of this: `b >= x && a <= y`. Much simpler than testing for different combinations of the various ways that two ranges can intersect. 3 | 4 | Unfortunately, when I was coding for the race, I didn't call those ranges `a-b` and `x-y`, but rather one of the first lines of code I wrote was 5 | ```py 6 | for w,x,y,z in dat: 7 | ``` 8 | which... didn't help, I absolutely could not keep all those letters straight, and that lead to me having a typo where I used the wrong variable in one condition and got the wrong answer out. Another minute in sad baby jail for me, and another miss at the leaderboards, entirely due to my own over-hasty coding. Live and learn. 9 | 10 | [278/157] 11 | -------------------------------------------------------------------------------- /2022/04.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | a = [ 3 | (37,87,36,87),(3,98,3,84),[...snip...] 4 | ] 5 | #a = [(2,4,6,8),(2,3,4,5),(5,7,7,9),(2,8,3,7),(6,6,4,6),(2,6,4,8)] 6 | t = 0 7 | for w,x,y,z in a: 8 | if (w <= y and z <= x) or (y <= w and x <= z): 9 | t += 1 10 | print(t) 11 | t = 0 12 | for w,x,y,z in a: 13 | if w <= z and x >= y: 14 | t += 1 15 | print(t) 16 | -------------------------------------------------------------------------------- /2022/05.md: -------------------------------------------------------------------------------- 1 | # 5 – Towers of Hanoi 2 | Lots of shuffling and list slicing here, but nothing surprising as yet. 3 | 4 | One thing I did find interesting is that my solution here is working with lists and pulling off each move in one go using list slices... which means that to get the solution for part B, all I had to do was _remove_ the part of the code that reverses the sublist being moved between the two piles. Meanwhile, I see solutions from other people that are using some specific Stack datastructure, and doing the moves as explicit push/pop operations, and they had to _add_ code for part B to unreverse the list that is already being naturally reversed by that strategy. It's interesting that these two strategies disagree over which part of the puzzle is "simpler". 5 | 6 | The hard part for this puzzle, it seems universally agreed, is actually reading the input data... specifically the initial state of the stacks. For the race, I went half-measures by using text-editor tricks to remove all the square brackets and spacing, to be left with just a simple grid of letters and spaces, and then used actual code to transpose that into a list of stacks. Meanwhile for the more careful Haskell solution I actually parse it fully, using fancy parser-combinator tricks, I'm actually quite happy with how simple the parser ended up looking. 7 | 8 | [11/10] 9 | -------------------------------------------------------------------------------- /2022/05.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | a = """ M V L 3 | G VCG D 4 | [...snip...]""".split("\n") 5 | a = [[j[i] for j in a if j[i] != ' '] for i in range(9)] 6 | dat = [ 7 | (1,5,2),(7,7,1),[...snip...] 8 | ] 9 | for n, f, t in dat: 10 | m = a[f-1][:n] 11 | a[f-1][:n] = [] 12 | a[t-1][:0] = m[::-1] # remove the [::-1] for part B 13 | for i in a: 14 | print(i[0], end='') 15 | print() 16 | -------------------------------------------------------------------------------- /2022/06.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.List 3 | import Control.Exception 4 | import Utils 5 | 6 | type Stacks = [[Char]] 7 | type Move = (Integer, Integer, Integer) 8 | 9 | getInput :: IO String 10 | getInput = readFile "06.txt" 11 | 12 | findSingleton :: Integer -> String -> Integer 13 | findSingleton n s = (+n) $ genericLength $ takeWhile ((/=n).genericLength.nub) $ window n s 14 | 15 | tests :: IO () 16 | tests = do 17 | check $ map (findSingleton 4) testData == [7, 5, 6, 10, 11] 18 | check $ map (findSingleton 14) testData == [19, 23, 23, 29, 26] 19 | where 20 | testData = [ 21 | "mjqjpqmgbljsphdztnvjfqwrcgsmlb", 22 | "bvwbjplbgvbhsrlpgdmjqwftvncz", 23 | "nppdvjthqldpwncqszvftbrmjlhg", 24 | "nznrnfrfntjfmvfwmzdfjlvtqnbhcprsg", 25 | "zcfzfwzzqfrljwzlrfnpqdbhtmscgvjw"] 26 | check True = return () 27 | check False = throwIO $ AssertionFailed "test failed" 28 | 29 | main :: IO () 30 | main = do 31 | tests 32 | dat <- getInput 33 | print $ findSingleton 4 dat 34 | print $ findSingleton 14 dat 35 | -------------------------------------------------------------------------------- /2022/06.md: -------------------------------------------------------------------------------- 1 | # 6 – Sliding Doors 2 | Pretty simple puzzle today... asks you to do a thing, so you just do the thing. 3 | 4 | In theory, the algorithm here could be improved... if this puzzle was showing up later in the event, I'd expect that the second part would need you to scan for a _much_ longer substring (to the point that the O(n²) behaviour of the basic solution is too much)... and you could do it (keep track of a multiset of what letters are in your sliding window, and then you only need to update it for the one letter being added and the one letter being removed each step). But that wasn't needed here, as the 14-letter case is still solved by the naive solution in a matter of milliseconds. 5 | 6 | For the race, I did have one hiccup as while I was skim-reading the requirements, I missed the part that explained exactly what number you need to enter at the end... like, if you have to skip the first three letters, and then the following four letters are your token, then is the answer 3? 4? 7? Turns out it's 7, but trying to figure that out from just the examples while in a half-panic took longer than it probably should have. 7 | 8 | [173/123] 9 | -------------------------------------------------------------------------------- /2022/06.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | a = "qfmfhmhjmjggw[...snip...]" 3 | for i in range(len(a)): 4 | if len(set(a[i:i+4])) == 4: 5 | break 6 | print(i+4) 7 | for i in range(len(a)): 8 | if len(set(a[i:i+14])) == 14: 9 | break 10 | print(i+14) 11 | -------------------------------------------------------------------------------- /2022/07.md: -------------------------------------------------------------------------------- 1 | # 7 – Now where did I save that report... 2 | A quite interesting puzzle for this early in the event... having to parse this unusual input and tease out the information that was needed. 3 | 4 | The first trick, though, is to recognise that although this is a directory tree, we absolutely do not actually want to parse it into some sort of tree-like data structure. We don't care about the tree structure, we only care about each directory and the files in each. A flattened structure that's just a list of all the directories (at any depth in the tree) in one list is all we need. That might seem like it could cause a problem when we go to determine the total size of a directory including all its subdirectories, but that leads to the second trick: when we read that a file exists, say `a/b/c/d.txt`, we immediately add its size to the size counters for all of `a`, `a/b` and `a/b/c` at once, so the size counters for each directory already contain all the subdirectories. 5 | 6 | Then, the tasks posed by both part A and part B are easy just reading off from that list. 7 | 8 | During the race, while I was coding part A, I was pretty sure that part B was going to involve identifying individual files rather than entire directories, so my race solution also builds a listing of every file (with its full pathname), on the assumption that it would be needed (and it would be easier to build that in from the start than to retrofit it during part B). But it turns out that wasn't actually needed once part B was revealed, so that was unnecessary. Still, didn't hurt much to add, and I think it was a reasonable guess. 9 | 10 | [10/6] 11 | -------------------------------------------------------------------------------- /2022/07.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from collections import defaultdict 3 | a = [ 4 | "$ cd /","$ ls","187585 dgflmqwt.srm","dir gnpd",[...snip...] 5 | ] 6 | curpath = [] 7 | files = {} 8 | dirs = defaultdict(int) 9 | in_ls = False 10 | for line in a: 11 | if line.startswith("$"): 12 | in_ls = False 13 | line = line[1:].strip().split() 14 | if line[0] == "cd": 15 | if line[1] == "..": 16 | curpath[-1:] = [] 17 | elif line[1] == "/": 18 | curpath = [] 19 | else: 20 | curpath.append(line[1]) 21 | elif line[0] == "ls": 22 | in_ls = True 23 | else: 24 | assert in_ls 25 | size, filename = line.strip().split() 26 | if size == "dir": 27 | continue 28 | size = int(size) 29 | files['/'.join(curpath + [filename])] = size 30 | for i in range(len(curpath) + 1): 31 | dirs['/'.join(curpath[:i])] += size 32 | 33 | print(sum(i for i in dirs.values() if i <= 100000)) 34 | target = 30000000 - (70000000 - dirs['']) 35 | print(min(i for i in dirs.values() if i >= target)) 36 | -------------------------------------------------------------------------------- /2022/08.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.Array 3 | import Data.Char 4 | import Data.List 5 | import Control.Exception 6 | import Utils 7 | import Direction 8 | 9 | type Grid = Array (Integer,Integer) Int 10 | 11 | getInput :: IO Grid 12 | getInput = parseInput <$> readFile "08.txt" 13 | 14 | parseInput :: String -> Grid 15 | parseInput = listArrayLen2 . map (map digitToInt) . lines 16 | 17 | visibility :: Grid -> (Integer, Integer) -> [(Integer, Bool)] 18 | visibility g p = map checkVis directions 19 | where 20 | target = g ! p 21 | checkVis d = walk d (step d p) 0 22 | walk d p' n 23 | | not $ inBounds g p' = (n, True) 24 | | (g ! p') >= target = (n + 1, False) 25 | | otherwise = walk d (step d p') (n + 1) 26 | 27 | partA :: Grid -> Integer 28 | partA g = genericLength $ filter isVisible $ indices g 29 | where 30 | isVisible (x,y) = any snd $ visibility g (x,y) 31 | 32 | partB :: Grid -> Integer 33 | partB g = maximum $ map (product.map fst.visibility g) $ indices g 34 | 35 | tests :: IO () 36 | tests = do 37 | check $ partA testData == 21 38 | check $ partB testData == 8 39 | where 40 | testData = parseInput "30373\n25512\n65332\n33549\n35390" 41 | check True = return () 42 | check False = throwIO $ AssertionFailed "test failed" 43 | 44 | main :: IO () 45 | main = do 46 | tests 47 | dat <- getInput 48 | print $ partA dat 49 | print $ partB dat 50 | -------------------------------------------------------------------------------- /2022/08.md: -------------------------------------------------------------------------------- 1 | # 8 – Towers 2 | Doing this puzzle for the race, I got trapped by puzzles like [Towers](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/towers.html) that count the number of visible towers from each cardinal direction along each line, in exactly this way... so naturally that's how I thought about approaching it. But doing it this way is quite fiddly... I have a lot of copy/pasted code for each of the four directions, and also need to keep track of which trees I have already counted (as a tree that's visible in multiple directions should not be double-counted). 3 | 4 | This then leads into the second part of the puzzle, which is completely different... I was confused for a bit that it wasn't doing the "visible if it's the tallest we've seen so far" thing when figuring out visibility from the targetted tree, and when I did manage to figure out that wasn't happening I ended up having to build something that looked completely unrelated to the work in the first part. 5 | 6 | On the other hand, when revisiting this for the Haskell solution, since I knew what was coming, I could approach it from that direction both times – for each tree, check the visibility range in each direction, and then for the first part we check if any of those visibility ranges reaches the edge of the map, and for the second part we take the product and look for the maximum. This strategy is almost certainly less efficient, but it's a lot cleaner code-wise as it's working from the point-of-view where the two halves of the puzzle are related. 7 | 8 | [140/111] 9 | -------------------------------------------------------------------------------- /2022/08.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | a = [ 3 | "110120112111001131321041300301301303441234124551121322251330313143000402010402222102132100122022010", 4 | "102022211221313002214040003422200133214443443344513144525425414312301403121303234303302002320112112", 5 | [...snip...] 6 | ] 7 | #a = ["30373","25512","65332","33549","35390"] 8 | a = [[int(j) for j in i] for i in a] 9 | cx = len(a[0]) 10 | cy = len(a) 11 | b = [[False for x in range(cx)] for y in range(cy)] 12 | count = 0 13 | for y in range(cy): 14 | cur = -1 15 | for x in range(cx): 16 | i = a[y][x] 17 | if i > cur: 18 | cur = i 19 | if not b[y][x]: 20 | b[y][x] = True 21 | count += 1 22 | cur = -1 23 | for x in range(cx-1,-1,-1): 24 | i = a[y][x] 25 | if i > cur: 26 | cur = i 27 | if not b[y][x]: 28 | b[y][x] = True 29 | count += 1 30 | for x in range(cx): 31 | cur = -1 32 | for y in range(cy): 33 | i = a[y][x] 34 | if i > cur: 35 | cur = i 36 | if not b[y][x]: 37 | b[y][x] = True 38 | count += 1 39 | cur = -1 40 | for y in range(cy-1,-1,-1): 41 | i = a[y][x] 42 | if i > cur: 43 | cur = i 44 | if not b[y][x]: 45 | b[y][x] = True 46 | count += 1 47 | print(count) 48 | 49 | 50 | def val(x,y): 51 | target = a[y][x] 52 | up = 0 53 | for y0 in range(y-1,-1,-1): 54 | up += 1 55 | if a[y0][x] >= target: 56 | break 57 | down = 0 58 | for y0 in range(y+1,cy): 59 | down += 1 60 | if a[y0][x] >= target: 61 | break 62 | left = 0 63 | for x0 in range(x-1,-1,-1): 64 | left += 1 65 | if a[y][x0] >= target: 66 | break 67 | right = 0 68 | for x0 in range(x+1,cx): 69 | right += 1 70 | if a[y][x0] >= target: 71 | break 72 | return up * down * left * right 73 | print(max(val(x,y) for x in range(1,cx-1) for y in range(1,cy-1))) 74 | -------------------------------------------------------------------------------- /2022/09.md: -------------------------------------------------------------------------------- 1 | # 9 – Nibbles.bas 2 | My race code for this one is an absolute unreadable mess... for [part 1](09a.py), I wrote the loop to process the inputs and move the head of the rope, and then had an update function to update the tail of the rope in step, and keep track of the set of points that are visited. This worked, but was kinda hard to read. Then along comes [part 2](09b.py), and suddenly I need to rewrite a whole chunk of the code to be able to run on multiple different "tails", where previously it had been hardcoded to point to specific "head" and "tail" variables. The end result is horrible, but it works. 3 | 4 | For the rewrite in Haskell, it is a lot clearer due to two main improvements: firstly, making it support an arbitrarily-length rope from the beginning, and also taking the time to actually think about the rules for how the tail follows the head, and realising it can be boiled down to just one stay-still condition and one movement condition (rather than having five separate movement conditions, in the Python code). Also, I do some fanciness with `scanl` to update all the tail positions in one step, nice and compact. There's also some minor benefit that I've already built a [directions helper](../Direction.hs) which genericises the movement a little, and also Haskell has a `signum` function which Python seems to lack. 5 | 6 | Also, just for fun, I rendered out [an animation of how the rope moves](https://cdn.discordapp.com/attachments/1050635291402190888/1050668633052942446/SPOILER_out.mp4). It's quite wibbly. 7 | 8 | [66/32] 9 | -------------------------------------------------------------------------------- /2022/09a.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | dat = [ 3 | ('U', 2),('D', 2),('L', 2),('R', 2),[...snip...] 4 | ] 5 | hx = hy = tx = ty = 0 6 | visited = {(0,0)} 7 | def updtail(): 8 | global tx, ty 9 | if tx == hx and ty < hy - 1: 10 | ty = hy - 1 11 | elif tx == hx and ty > hy + 1: 12 | ty = hy + 1 13 | elif ty == hy and tx < hx - 1: 14 | tx = hx - 1 15 | elif ty == hy and tx > hx + 1: 16 | tx = hx + 1 17 | elif tx != hx and ty != hy: 18 | dx = hx - tx 19 | dy = hy - ty 20 | adx, ady = abs(dx), abs(dy) 21 | sdx, sdy = dx // abs(dx), dy // abs(dy) 22 | if adx >= 2 or ady >= 2: 23 | tx += sdx 24 | ty += sdy 25 | visited.add((tx, ty)) 26 | for d, n in dat: 27 | if d == 'U': 28 | for i in range(n): 29 | hy -= 1 30 | updtail() 31 | elif d == 'D': 32 | for i in range(n): 33 | hy += 1 34 | updtail() 35 | elif d == 'L': 36 | for i in range(n): 37 | hx -= 1 38 | updtail() 39 | elif d == 'R': 40 | for i in range(n): 41 | hx += 1 42 | updtail() 43 | else: 44 | 1/0 45 | print(len(visited)) 46 | -------------------------------------------------------------------------------- /2022/09b.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | dat = [ 3 | ('U', 2),('D', 2),('L', 2),('R', 2),[...snip...] 4 | ] 5 | N = 10 6 | p = [[0,0] for i in range(N)] 7 | visited = {(0,0)} 8 | def updtail(): 9 | for i in range(1, N): 10 | updstep(i) 11 | visited.add(tuple(p[-1])) 12 | def updstep(i): 13 | if p[i][0] == p[i-1][0] and p[i][1] < p[i-1][1] - 1: 14 | p[i][1] = p[i-1][1] - 1 15 | elif p[i][0] == p[i-1][0] and p[i][1] > p[i-1][1] + 1: 16 | p[i][1] = p[i-1][1] + 1 17 | elif p[i][1] == p[i-1][1] and p[i][0] < p[i-1][0] - 1: 18 | p[i][0] = p[i-1][0] - 1 19 | elif p[i][1] == p[i-1][1] and p[i][0] > p[i-1][0] + 1: 20 | p[i][0] = p[i-1][0] + 1 21 | elif p[i][0] != p[i-1][0] and p[i][1] != p[i-1][1]: 22 | dx = p[i-1][0] - p[i][0] 23 | dy = p[i-1][1] - p[i][1] 24 | adx, ady = abs(dx), abs(dy) 25 | sdx, sdy = dx // abs(dx), dy // abs(dy) 26 | if adx >= 2 or ady >= 2: 27 | p[i][0] += sdx 28 | p[i][1] += sdy 29 | for d, n in dat: 30 | if d == 'U': 31 | for i in range(n): 32 | p[0][1] -= 1 33 | updtail() 34 | elif d == 'D': 35 | for i in range(n): 36 | p[0][1] += 1 37 | updtail() 38 | elif d == 'L': 39 | for i in range(n): 40 | p[0][0] -= 1 41 | updtail() 42 | elif d == 'R': 43 | for i in range(n): 44 | p[0][0] += 1 45 | updtail() 46 | else: 47 | 1/0 48 | print(len(visited)) 49 | -------------------------------------------------------------------------------- /2022/10.md: -------------------------------------------------------------------------------- 1 | # 10 – Not-tari 2600 2 | I'm glad they linked the [Racing the Beam video](https://www.youtube.com/watch?v=sJFnWZH5FXc) in the prose for this one, because if not I would definitely have been speculating that they had watched this video and based the puzzle around it. 3 | 4 | Decided pretty early on in this one that I wanted to flatten the operations to a full list of what is in the register at each clock cycle... so each `noop` would add one value to this list, and each `addx` would add two values. Make it much easier to keep track of what is happening at any given time. This definitely turned out to be the right call, after reading the second part. One judicious application of `scanl` and this list fell out right how we wanted it. 5 | 6 | There was definitely some confusion about indexing... the first part wants us to index the timestamps starting at 1, while the second part indexes the screen pixels starting at 0... but after re-reading it several times to ensure that, yes, that is actually what they want, the rest just mostly fell out as hoped. 7 | 8 | Unfortunately, I wasn't able to participate in the race for this one. 9 | -------------------------------------------------------------------------------- /2022/11.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from copy import deepcopy 3 | from math import lcm 4 | from functools import reduce 5 | dat = [ 6 | ([56, 52, 58, 96, 70, 75, 72], 7 | '*', 17, 8 | 11, 9 | 2, 10 | 3,), 11 | [...snip...] 12 | ] 13 | #dat = [([79, 98], '*', 19, 23, 2, 3,), ([54, 65, 75, 74], '+', 6, 19, 2, 0,), ([79, 60, 97], '^', 2, 13, 1, 3,), ([74], '+', 3, 17, 0, 1,)] 14 | modulo = reduce(lcm, [i[3] for i in dat]) 15 | origdat = deepcopy(dat) 16 | 17 | count = [0] * len(dat) 18 | 19 | def turn(part1): 20 | for n, m in enumerate(dat): 21 | for i in m[0]: 22 | if m[1] == '+': 23 | i += m[2] 24 | elif m[1] == '*': 25 | i *= m[2] 26 | else: 27 | i *= i 28 | if part1: 29 | i //= 3 30 | else: 31 | i %= modulo 32 | target = m[4] if i % m[3] == 0 else m[5] 33 | dat[target][0].append(i) 34 | count[n] += 1 35 | m[0][:] = [] 36 | for i in range(20): 37 | turn(True) 38 | count.sort() 39 | print(count[-1] * count[-2]) 40 | 41 | dat = origdat 42 | count = [0] * len(dat) 43 | for i in range(10000): 44 | turn(False) 45 | count.sort() 46 | print(count[-1] * count[-2]) 47 | -------------------------------------------------------------------------------- /2022/12.md: -------------------------------------------------------------------------------- 1 | # 12 – Gradient ascent 2 | Ah yes, can't get this far into an AoC without a classic shortest-path puzzle. Such a classic that by this point I can just rattle off a dodgy poorly-optimised implementation of Dijkstra in my sleep. Because, to be sure, the implementation in my Python race code is dodgy and poorly-optimised, but it works, and that's all that matters. 3 | 4 | Naturally, for part 1, I ran Dijkstra directly, starting at the start location and running until we find the end. So, of course, part 2 involves fixing the _end_ location and checking a whole bunch of _start_ locations. Which Dijkstra can do just fine, you only have to fix one endpoint and it finds the minimum distances to every other point on the graph if you let it run to completion. But that did require some quick swapping of variables and negating of conditions to make it run backwards from the end instead of forwards from the start. There was also a slight hiccup that the graph isn't fully connected... it's not possible to reach the endpoint from every candidate startpoint, which means the Dijkstra loop needs to bail before it's completely filled out the distance map. In lieu of an actually good way to handle this, I just catch the exception that's raised when it runs out of candidates and bail at that point. 5 | 6 | For the Haskell re-implementation, it was even easier, since I have this handy Dijkstra module that I've built up from previous AoCs, which conveniently even has a function for calculating a full distance map from a given point. Was just a case of plugging everything into the appropriate places. 7 | 8 | [40/64] 9 | -------------------------------------------------------------------------------- /2022/12.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | dat = [ 3 | "abaacccccccccccccaaaaaaaccccccccccccccccccccccccccccccccccaaaaaa", 4 | "abaaccccccccccccccaaaaaaaaaaccccccccccccccccccccccccccccccccaaaa", 5 | [...snip...] 6 | ] 7 | dat = [[ord(c) - ord('a') for c in row] for row in dat] 8 | sx, sy = 0, 20 9 | ex, ey = 40, 20 10 | cx, cy = len(dat[0]), len(dat) 11 | dat[sy][sx] = 0 12 | dat[ey][ex] = 25 13 | 14 | distmap = [[None for x in range(cx)] for y in range(cy)] 15 | distmap[ey][ex] = 0 16 | unseen = cx * cy - 1 17 | 18 | while unseen > 0: 19 | try: 20 | tx, ty, n = min(( 21 | (x, y, distmap[y + dy][x + dx] + 1) 22 | for y in range(cy) 23 | for x in range(cx) 24 | if distmap[y][x] is None 25 | for dy, dx in [(-1,0),(1,0),(0,-1),(0,1)] 26 | if 0 <= x + dx < cx and 0 <= y + dy < cy 27 | and distmap[y + dy][x + dx] is not None 28 | and dat[y][x] >= dat[y + dy][x + dx] - 1 29 | ), key=lambda x: x[2] 30 | ) 31 | except ValueError: 32 | break 33 | else: 34 | distmap[ty][tx] = n 35 | unseen -= 1 36 | 37 | print(distmap[sy][sx]) 38 | print(min(distmap[y][x] for y in range(cy) for x in range(cx) if dat[y][x] == 0 and distmap[y][x] is not None)) 39 | -------------------------------------------------------------------------------- /2022/13.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-tabs #-} 2 | import Data.List 3 | import Control.Exception 4 | import Utils 5 | 6 | data Packet = ListVal [Packet] | IntVal Integer deriving Eq 7 | 8 | getInput :: IO [Packet] 9 | getInput = map read <$> filter (not.null) <$> lines <$> readFile "13.txt" 10 | 11 | instance (Read Packet) where 12 | readsPrec d r = [(ListVal x, rest) | (x, rest) <- readsPrec d r] ++ [(IntVal x, rest) | (x, rest) <- readsPrec d r] 13 | 14 | instance (Show Packet) where 15 | showsPrec d (ListVal x) = showsPrec d x 16 | showsPrec d (IntVal x) = showsPrec d x 17 | 18 | instance (Ord Packet) where 19 | compare (IntVal a) (IntVal b) = compare a b 20 | compare (ListVal a) (ListVal b) = compare a b 21 | compare a@(IntVal _) (ListVal b) = compare [a] b 22 | compare (ListVal a) b@(IntVal _) = compare a [b] 23 | 24 | partA ps = sum [i | (i, [a, b]) <- zip [1..] $ chunk 2 ps, a < b] 25 | partB ps = (index1 + 1) * (index2 + 1) 26 | where 27 | sentinel1 = ListVal [ListVal [IntVal 2]] 28 | sentinel2 = ListVal [ListVal [IntVal 6]] 29 | sorted = sort $ [sentinel1, sentinel2] ++ ps 30 | Just index1 = elemIndex sentinel1 sorted 31 | Just index2 = elemIndex sentinel2 sorted 32 | 33 | tests :: IO () 34 | tests = do 35 | check $ partA testData == 13 36 | check $ partB testData == 140 37 | where 38 | testData = map read ["[1,1,3,1,1]", "[1,1,5,1,1]", "[[1],[2,3,4]]", "[[1],4]", "[9]", "[[8,7,6]]", "[[4,4],4,4]", "[[4,4],4,4,4]", "[7,7,7,7]", "[7,7,7]", "[]", "[3]", "[[[]]]", "[[]]", "[1,[2,[3,[4,[5,6,7]]]],8,9]", "[1,[2,[3,[4,[5,6,0]]]],8,9]"] 39 | check True = return () 40 | check False = throwIO $ AssertionFailed "test failed" 41 | 42 | main :: IO () 43 | main = do 44 | tests 45 | dat <- getInput 46 | print $ partA dat 47 | print $ partB dat 48 | -------------------------------------------------------------------------------- /2022/13.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from functools import cmp_to_key 3 | dat = [ 4 | ([[1,[0,3,5,[2,1,3,3,5]],4,[[],5]],[],[0,[7,[5],7,7]]],[[[],[[],[5,2,8,9,7],1,5],[3,[]]]]),([[[[1,3,6],[7,9,2,7],[5,0,5,8,4]]],[[8,2,6,[]],[],1,6]],[[5,10,4],[6,9,8],[5,[4,[3,8,1,5,1],[4],6,[3]],[[0]],[10],[[5,7],[5,1,1,4,7],[]]],[[[9,7,5,7],[5,1,7,3,1],[8,4,2]],5,[[8,9,3,4,4],4,[2,4,2],3,[6,10,5,7]],9],[[1]]]),[...snip...] 5 | ] 6 | def compare(a,b): 7 | if isinstance(a, int) and isinstance(b, int): 8 | return a - b 9 | elif isinstance(a, list) and isinstance(b, list): 10 | for x, y in zip(a, b): 11 | ret = compare(x, y) 12 | if ret != 0: 13 | return ret 14 | return len(a) - len(b) 15 | elif isinstance(a, list): 16 | return compare(a, [b]) 17 | elif isinstance(b, list): 18 | return compare([a], b) 19 | 20 | print(sum(i for i, (a, b) in enumerate(dat, 1) if compare(a, b) < 0)) 21 | 22 | packets = [ [[2]], [[6]] ] 23 | for a, b in dat: 24 | packets.append(a) 25 | packets.append(b) 26 | packets.sort(key=cmp_to_key(compare)) 27 | a = packets.index([[2]]) + 1 28 | b = packets.index([[6]]) + 1 29 | print(a * b) 30 | -------------------------------------------------------------------------------- /2022/14.md: -------------------------------------------------------------------------------- 1 | # 14 – Falling sand game 2 | I lost a bit of time here at the beginning by changing my mind about what data structure to use... at first I started building the code around storing the state of the grid in a big 2D array, but then ultimately realised that the pile of sand could, and indeed probably would, stretch somewhat above the top of the initial bounds of the array... and I really didn't want to dynamically resize the list (and have to maintain all the bounds details). So I rewrote it to use a `dict` keyed by an x,y pair instead. I could've just made it a `set` of points, but I kinda had this thought that maybe the second part of the puzzle would involve knowing the difference between a wall and a grain of sand, so I kept track of that in the `dict`. That didn't end up being the case, but it was simple enough to do (and would have been painful enough to retrofit if I didn't do it) that I still think it was the right play. For the Haskell re-implementation, with the power of foresight knowing what the second part is going to be, I implemented it using a `Set` from the beginning. 3 | 4 | Other than that hiccup though, this one was pretty smooth sailing, just directly implementing the path of the sand and simulating each one. Even for part 2. It really does feel like we're getting close to the point in the month where part 2's will be going from "just do it more" to "do it so much more that you need a more efficient solution"... but we're not there yet. 5 | 6 | Also, because I was curious, I did draw some images of how the final sand piles look for each part: 7 | 8 | ![The final sand pile for my input, for Part 1](14a.png) ![The final sand pile for my input, for Part 2](14b.png) 9 | 10 | [134/94] 11 | -------------------------------------------------------------------------------- /2022/14.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from collections import defaultdict 3 | from copy import deepcopy 4 | dat = [ 5 | [(490,23),(490,17),(490,23),(492,23),(492,14),(492,23),(494,23),(494,20),(494,23),(496,23),(496,18),(496,23),(498,23),(498,19),(498,23),(500,23),(500,20),(500,23),(502,23),(502,17),(502,23),(504,23),(504,14),(504,23),(506,23),(506,18),(506,23),(508,23),(508,15),(508,23)],[...snip...] 6 | ] 7 | grid = defaultdict(int) 8 | points = [p for path in dat for p in path] 9 | minx = min(x for x,y in points) 10 | maxx = max(x for x,y in points) 11 | miny = min(y for x,y in points) 12 | maxy = max(y for x,y in points) 13 | grid = defaultdict(int) 14 | for path in dat: 15 | x1, y1 = path[0] 16 | for x2, y2 in path[1:]: 17 | if x1 == x2: 18 | for y in range(min(y1,y2), max(y1,y2)+1): 19 | grid[x1, y] = 1 20 | elif y1 == y2: 21 | for x in range(min(x1,x2), max(x1,x2)+1): 22 | grid[x, y1] = 1 23 | else: 24 | raise ValueError("Non-rect path") 25 | x1, y1 = x2, y2 26 | initgrid = deepcopy(grid) 27 | 28 | def dropsand(x, y, partB): 29 | global miny 30 | while True: 31 | if y >= maxy and not partB: 32 | return False 33 | elif y >= maxy + 1 and partB: 34 | grid[x, y] = 2 35 | return True 36 | elif grid[x, y+1] == 0: 37 | y += 1 38 | continue 39 | elif grid[x-1, y+1] == 0: 40 | y += 1 41 | x -= 1 42 | continue 43 | elif grid[x+1, y+1] == 0: 44 | y += 1 45 | x += 1 46 | continue 47 | else: 48 | if y < miny: 49 | miny = y 50 | grid[x, y] = 2 51 | return True 52 | 53 | i = 0 54 | while dropsand(500, miny - 1, False): 55 | i += 1 56 | print(i) 57 | 58 | grid = initgrid 59 | 60 | i = 0 61 | while grid[500, 0] == 0: 62 | dropsand(500, 0, True) 63 | i += 1 64 | print(i) 65 | -------------------------------------------------------------------------------- /2022/14a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/e3fe0fbd8a26e4e0941ed72a032202416f1cc20c/2022/14a.png -------------------------------------------------------------------------------- /2022/14b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/e3fe0fbd8a26e4e0941ed72a032202416f1cc20c/2022/14b.png -------------------------------------------------------------------------------- /2022/15.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | dat = [ 3 | (2483411, 3902983, 2289579, 3633785),(3429446, 303715, 2876111, -261280),[...snip...] 4 | ] 5 | #dat = [(2, 18, -2, 15),(9, 16, 10, 16),(13, 2, 15, 3),(12, 14, 10, 16),(10, 20, 10, 16),(14, 17, 10, 16),(8, 7, 2, 10),(2, 0, 2, 10),(0, 11, 2, 10),(20, 14, 25, 17),(17, 20, 21, 22),(16, 7, 15, 3),(14, 3, 15, 3),(20, 1, 15, 3)] 6 | 7 | def find_at(ty, minx=None, maxx=None): 8 | ranges = [] 9 | for sx, sy, bx, by in dat: 10 | dx = abs(sx - bx) 11 | dy = abs(sy - by) 12 | dist = dx + dy 13 | remaining_dist = dist - abs(ty - sy) 14 | if remaining_dist >= 0: 15 | ranges.append((sx - remaining_dist, sx + remaining_dist + 1)) 16 | ranges.sort() 17 | lastb = ranges[0][0] - 1 18 | flatranges = [] 19 | for a, b in ranges: 20 | if minx is not None: 21 | a = max(a, minx) 22 | b = max(b, minx) 23 | a = min(a, maxx) 24 | b = min(b, maxx) 25 | if a >= b: 26 | continue 27 | if lastb >= b: 28 | continue 29 | if a <= lastb: 30 | flatranges[-1][1] = b 31 | else: 32 | flatranges.append([a, b]) 33 | lastb = b 34 | return sum(b-a for a,b in flatranges), flatranges 35 | 36 | targety = 2000000 37 | #targety = 10 38 | t, _ = find_at(targety) 39 | t -= len(set((bx,by) for _,_,bx,by in dat if by == targety)) 40 | print(t) 41 | 42 | limit = 4000000 43 | #limit = 20 44 | for y in range(limit + 1): 45 | #if y % 1000 == 0: 46 | # print(f"[{y}]") 47 | t, ranges = find_at(y, 0, limit + 1) 48 | if t != limit + 1: 49 | break 50 | x = ranges[0][1] 51 | print(x * 4000000 + y) 52 | -------------------------------------------------------------------------------- /2022/16a.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | dat = [ 3 | ("NQ",0,["SU", "XD"]),("AB",0,["XD", "TE"]),[...snip...] 4 | ] 5 | #dat = [("AA",0,["DD", "II", "BB"]),("BB",13,["CC", "AA"]),("CC",2,["DD", "BB"]),("DD",20,["CC", "AA", "EE"]),("EE",3,["FF", "DD"]),("FF",0,["EE", "GG"]),("GG",0,["FF", "HH"]),("HH",22,["GG"]),("II",0,["AA", "JJ"]),("JJ",21,["II"])] 6 | valves = [a for a, b, c in dat] 7 | flows = {a: b for a, b, c in dat} 8 | neighbours = {a: c for a, b, c in dat} 9 | 10 | distmap = {} 11 | for f in valves: 12 | d = {k:None for k in valves} 13 | d[f] = 0 14 | while True: 15 | c = [(d[k]+1,n) for k in valves if d[k] is not None for n in neighbours[k] if d[n] is None] 16 | if not c: 17 | break 18 | c.sort() 19 | c = [i for i in c if i[0] == c[0][0]] 20 | for n, k in c: 21 | d[k] = n 22 | for k, n in d.items(): 23 | distmap[f, k] = n 24 | 25 | def consider(node, range, score, enabled): 26 | foundany = False 27 | for k in valves: 28 | if k != node and flows[k] > 0 and k not in enabled and distmap[node, k] <= range: 29 | yield from consider(k, range-distmap[node, k]-1, score+flows[k]*(range-distmap[node,k]-1), enabled+[k]) 30 | foundany = True 31 | 32 | if not foundany: 33 | yield score, enabled 34 | 35 | print(max(consider("AA", 30, 0, ["AA"]))) 36 | 37 | def consider2(node, range, score, enabled): 38 | #foundany = False 39 | for i, k in enumerate(valves): 40 | if range == 26: 41 | print(i) 42 | if k != node and flows[k] > 0 and k not in enabled and distmap[node, k] <= range - 1: 43 | yield from consider2(k, range-distmap[node, k]-1, score+flows[k]*(range-distmap[node,k]-1), enabled+[k]) 44 | #foundany = True 45 | 46 | #if not foundany: 47 | yield from consider("AA", 26, score, enabled) 48 | 49 | print(max(consider2("AA", 26, 0, ["AA"]))) 50 | -------------------------------------------------------------------------------- /2022/16b.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | dat = [ 3 | ("NQ",0,["SU", "XD"]),("AB",0,["XD", "TE"]),[...snip...] 4 | ] 5 | #dat = [("AA",0,["DD", "II", "BB"]),("BB",13,["CC", "AA"]),("CC",2,["DD", "BB"]),("DD",20,["CC", "AA", "EE"]),("EE",3,["FF", "DD"]),("FF",0,["EE", "GG"]),("GG",0,["FF", "HH"]),("HH",22,["GG"]),("II",0,["AA", "JJ"]),("JJ",21,["II"])] 6 | valves = [a for a, b, c in dat] 7 | flows = {a: b for a, b, c in dat} 8 | neighbours = {a: c for a, b, c in dat} 9 | 10 | distmap = {} 11 | for f in valves: 12 | d = {k:None for k in valves} 13 | d[f] = 0 14 | while True: 15 | c = [(d[k]+1,n) for k in valves if d[k] is not None for n in neighbours[k] if d[n] is None] 16 | if not c: 17 | break 18 | c.sort() 19 | c = [i for i in c if i[0] == c[0][0]] 20 | for n, k in c: 21 | d[k] = n 22 | for k, n in d.items(): 23 | distmap[f, k] = n 24 | 25 | def consider(node, range, score, enabled): 26 | #foundany = False 27 | for k in valves: 28 | if k != node and flows[k] > 0 and k not in enabled and distmap[node, k] <= range: 29 | yield from consider(k, range-distmap[node, k]-1, score+flows[k]*(range-distmap[node,k]-1), enabled+[k]) 30 | #foundany = True 31 | 32 | #if not foundany: 33 | yield score, enabled 34 | 35 | res = [(n, set(e)) for n, e in consider("AA", 26, 0, [])] 36 | #print(max(n1 + n2 for n1, e1 in res for n2, e2 in res if not (e1 & e2))) 37 | def gen(): 38 | for i, (n1, e1) in enumerate(res): 39 | if i % 100 == 0: 40 | print(' ',i,len(res)) 41 | for n2, e2 in res: 42 | if not e1 & e2: 43 | yield n1 + n2 44 | print(max(gen())) 45 | -------------------------------------------------------------------------------- /2022/18.md: -------------------------------------------------------------------------------- 1 | # 18 – Minecraft at home 2 | A welcome reprieve from the last couple of days... everyone was expecting a hard one here, day 18 has been a difficulty spike in the past, and we were on a weekend... but this was a nice break. 3 | 4 | First part is pretty simple... for each cell in the input, check each of its 6 sides, and if there isn't another cell in that direction, count them up. 5 | 6 | Second part is quite more involved, we need to know which cells outside our shape are path-connected to the exterior. The strategy I went with was to box in an bounding box around our shape, with a buffer on all sides, and then run a flood-fill algorithm to mark all the cells on the exterior, and then we can tell which faces of the surface are on the exterior and which are not. 7 | 8 | The flood-fill in my Python race code is a bit janky... I wasn't paying the closest attention so my todo list is actually being used as a stack, rather than a queue... which means the flood fill is running as a depth-first search, rather than a breadth-first search, which is not the most efficient. Still works just fine, but is slower. Meanwhile for the Haskell version I looked into a new module I hadn't worked with before, to get a proper FIFO queue going (since bare lists are not ideal for that sort of thing). 9 | 10 | [12/3] 11 | -------------------------------------------------------------------------------- /2022/18.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | dat = { 3 | (4,9,7),(11,4,7),(8,18,14),[...snip...] 4 | } 5 | 6 | n = 0 7 | for x,y,z in dat: 8 | for dx,dy,dz in [(-1,0,0),(1,0,0),(0,-1,0),(0,1,0),(0,0,-1),(0,0,1)]: 9 | if (x+dx,y+dy,z+dz) not in dat: 10 | n += 1 11 | print(n) 12 | 13 | minx = min(i[0] for i in dat) - 1 14 | maxx = max(i[0] for i in dat) + 2 15 | miny = min(i[1] for i in dat) - 1 16 | maxy = max(i[1] for i in dat) + 2 17 | minz = min(i[2] for i in dat) - 1 18 | maxz = max(i[2] for i in dat) + 2 19 | 20 | water = set() 21 | tofill = [(minx, miny, minz)] 22 | while tofill: 23 | wx, wy, wz = tofill.pop() 24 | if (wx, wy, wz) in water or (wx, wy, wz) in dat: 25 | continue 26 | water.add((wx, wy, wz)) 27 | for dx,dy,dz in [(-1,0,0),(1,0,0),(0,-1,0),(0,1,0),(0,0,-1),(0,0,1)]: 28 | tx, ty, tz = wx+dx,wy+dy,wz+dz 29 | if minx <= tx < maxx and miny <= ty < maxy and minz <= tz < maxz: 30 | tofill.append((tx, ty, tz)) 31 | 32 | n = 0 33 | for x,y,z in dat: 34 | for dx,dy,dz in [(-1,0,0),(1,0,0),(0,-1,0),(0,1,0),(0,0,-1),(0,0,1)]: 35 | if (x+dx,y+dy,z+dz) in water: 36 | n += 1 37 | print(n) 38 | 39 | -------------------------------------------------------------------------------- /2022/20.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | class A: 3 | def __init__(self, val): 4 | self.val = val 5 | 6 | dat = [ 7 | 9038,7675,-2761,[...snip...] 8 | ] 9 | #dat = [1, 2, -3, 3, -2, 0, 4] 10 | dat = [i * 811589153 for i in dat] # remove for part 1 11 | dat = [A(i) for i in dat] 12 | 13 | seq = dat[:] 14 | zero, = [i for i in dat if i.val == 0] 15 | 16 | def mix(): 17 | for i in dat: 18 | ix = seq.index(i) 19 | newix = (ix + i.val) % (len(dat) - 1) 20 | del seq[ix] 21 | seq.insert(newix, i) 22 | 23 | def grove(): 24 | ix = seq.index(zero) 25 | a = seq[(ix + 1000) % len(seq)].val 26 | b = seq[(ix + 2000) % len(seq)].val 27 | c = seq[(ix + 3000) % len(seq)].val 28 | return a + b + c 29 | 30 | for i in range(10): # remove loop for part 1 31 | mix() 32 | print(grove()) 33 | -------------------------------------------------------------------------------- /2022/22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/e3fe0fbd8a26e4e0941ed72a032202416f1cc20c/2022/22.png -------------------------------------------------------------------------------- /2022/22a.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | dat = """\ 3 | .#..#.....#.....................................................................#...........#....... 4 | ................#.....#......#...#................#..........#.#...........#........................ 5 | etc""".split("\n") 6 | dat2 = [ 7 | 7, 'R', 13, 'R', etc 8 | ] 9 | #dat = """ ...#\n .#..\n #...\n ....\n...#.......#\n........#...\n..#....#....\n..........#.\n ...#....\n .....#..\n .#......\n ......#.""".split("\n") 10 | #dat2 = [10,'R',5,'L',5,'R',10,'L',4,'R',5,'L',5] 11 | 12 | x = len([i for i in dat[0] if i == ' ']) 13 | y = 0 14 | d = 0 15 | def ob(): 16 | return y < 0 or y >= len(dat) or x < 0 or x >= len(dat[y]) or dat[y][x] == ' ' 17 | def step(): 18 | global x, y 19 | oldx, oldy = x, y 20 | if d == 0: # right 21 | x += 1 22 | if ob(): 23 | x = len([i for i in dat[y] if i == ' ']) 24 | elif d == 2: # left 25 | x -= 1 26 | if ob(): 27 | x = len(dat[y]) - 1 28 | elif d == 1: # down 29 | y += 1 30 | if ob(): 31 | y = 0 32 | while ob(): 33 | y += 1 34 | elif d == 3: # up 35 | y -= 1 36 | if ob(): 37 | y = len(dat) - 1 38 | while ob(): 39 | y -= 1 40 | if dat[y][x] == '#': 41 | x, y = oldx, oldy 42 | print(x,y,d) 43 | for i in range(0, len(dat2), 2): 44 | for j in range(dat2[i]): 45 | step() 46 | if i+1 < len(dat2): 47 | if dat2[i+1] == 'L': 48 | d = (d - 1) % 4 49 | else: 50 | d = (d + 1) % 4 51 | print(x,y,d) 52 | 53 | print(1000 * (y+1) + 4 * (x+1) + d) 54 | -------------------------------------------------------------------------------- /2022/23.md: -------------------------------------------------------------------------------- 1 | # 23 – It's about time 2 | Well, it was inevitable. It's not an Advent of Code event without some sort of cellular automaton. Which this... almost is... I guess it would be possible to define this as a cellular automaton with a large range of influence (I think each cell is determined by a 9x9 square centred around that square on the previous level)... and while it's not super useful to think about it directly in that way, some of the higher-level ideas still work (ie you have a grid at a particular generation, and from that you derive the grid at the next generation, all in one step). 3 | 4 | The hard part is figuring out how this complex process actually works... and in particular, not missing the whole "the elves' priority order for directions changes each round" part, which I definitely skimmed over several times while reading. The result being a horrifying for-case loop to do all the different options in the right order for each step. It's a mess, but it works. 5 | 6 | The rest, after that, is not too bad... we figure out where each elf wants to go, pass that to `collections.Counter` to check for duplicates, and then move all the elves that aren't duplicates. 7 | 8 | For part 2, we just do it in the most direct way – keep a copy of the previous generation, and just directly check if two consecutive generations are equal. In theory this might not actually be 100%, because of the priority-order thing, in theory we actually need to check that it stays static for 4 straight steps (or check that no elf neighbours another elf, which guarantees they'll all stay static)... but this gives me the right answer for my input at least. 9 | 10 | [55/56] 11 | -------------------------------------------------------------------------------- /2022/23.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from collections import Counter 3 | dat = """\ 4 | .##.####.#.#..###....##.....#.#.#.###.#####..#..#...#..#.##.##.#..#####.#. 5 | #.#..####..#...#.####..#.#..##.###..#.#...#.#..##..##.#...#..#####.#.####. 6 | [...snip...]"".split("\n") 7 | #dat = ".....\n..##.\n..#..\n.....\n..##.\n.....".split("\n") 8 | #dat = "....#..\n..###.#\n#...#.#\n.#...##\n#.###..\n##.#.##\n.#..#..".split("\n") 9 | elves = {(x, y) for y,row in enumerate(dat) for x,cell in enumerate(row) if cell == '#'} 10 | 11 | step = 0 12 | 13 | def propmove(x, y): 14 | if all((x+dx,y+dy) not in elves for dx in [-1,0,1] for dy in [-1,0,1] if dx or dy): 15 | return (x, y) 16 | for i in range(step, step+4): 17 | if i%4 == 0 and all((x+dx,y-1) not in elves for dx in [-1,0,1]): 18 | return (x,y-1) 19 | if i%4 == 1 and all((x+dx,y+1) not in elves for dx in [-1,0,1]): 20 | return (x,y+1) 21 | if i%4 == 2 and all((x-1,y+dy) not in elves for dy in [-1,0,1]): 22 | return (x-1,y) 23 | if i%4 == 3 and all((x+1,y+dy) not in elves for dy in [-1,0,1]): 24 | return (x+1,y) 25 | return (x, y) 26 | 27 | def move(): 28 | global elves, step 29 | props = [((x, y), propmove(x,y)) for (x,y) in elves] 30 | counts = Counter(i[1] for i in props) 31 | elves = {newp if counts[newp] <= 1 else oldp for oldp, newp in props} 32 | step += 1 33 | 34 | for i in range(10): 35 | move() 36 | minx = min(x for x,y in elves) 37 | maxx = max(x for x,y in elves) 38 | miny = min(y for x,y in elves) 39 | maxy = max(y for x,y in elves) 40 | print((maxx - minx + 1) * (maxy - miny + 1) - len(elves)) 41 | 42 | i = 10 43 | oldelves = None 44 | while oldelves != elves: 45 | oldelves = elves 46 | move() 47 | i += 1 48 | print(i) 49 | -------------------------------------------------------------------------------- /2022/25.md: -------------------------------------------------------------------------------- 1 | # 25 – Balanced Quinary 2 | Once more, another puzzle where I'm helped out by having looked into this sort of thing before... not this exact thing, but [balanced ternary](https://en.wikipedia.org/wiki/Balanced_ternary) which is a similar idea. With that under my belt, this was all pretty simple to churn out. But ultimately, the day-25 puzzles usually aren't too complicated, let people get the puzzle out of the way and on with their christmas day. 3 | 4 | [11/9], which puts me with a total of 1976 points for the event. The highest score of anyone who got under 2000 points! Or, more usefully, overall rank 22. Which I'm very happy with, it's a super solid showing. 5 | -------------------------------------------------------------------------------- /2022/25.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | dat = [ 3 | "21211-122", 4 | "1=--02-10=-=00=-0", 5 | "1=-2", 6 | [...snip...] 7 | ] 8 | 9 | digits = {'0':0, '1':1, '2':2, '-':-1, '=':-2} 10 | digits_rev = {v:k for k,v in digits.items()} 11 | def from_num(s): 12 | d = 0 13 | for c in s: 14 | d = d * 5 + digits[c] 15 | return d 16 | def to_num(n): 17 | if not n: 18 | return '0' 19 | s = '' 20 | while n: 21 | n, d = divmod(n, 5) 22 | if d >= 3: 23 | d -= 5 24 | n += 1 25 | s = digits_rev[d] + s 26 | return s 27 | print(to_num(sum(from_num(i) for i in dat))) 28 | -------------------------------------------------------------------------------- /2022/Makefile: -------------------------------------------------------------------------------- 1 | all: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 17 18 21a 21b 2 | 3 | clean: 4 | rm -f [0-9][0-9] 21a 21b *.hi *.o ../*.hi ../*.o 5 | 6 | .PHONY: all clean 7 | 8 | %: %.hs 9 | ghc -O2 -threaded -with-rtsopts="-N" -o $@ $^ 10 | 11 | 01: 01.hs 12 | 02: 02.hs 13 | 03: 03.hs ../Utils.hs 14 | 04: 04.hs ../Utils.hs 15 | 05: 05.hs ../Utils.hs 16 | 06: 06.hs ../Utils.hs 17 | 07: 07.hs 18 | 08: 08.hs ../Utils.hs ../Direction.hs 19 | 09: 09.hs ../Direction.hs 20 | 10: 10.hs ../Utils.hs 21 | 11: 11.hs ../Utils.hs 22 | 12: 12.hs ../Utils.hs ../Dijkstra.hs ../Direction.hs 23 | 13: 13.hs ../Utils.hs 24 | 14: 14.hs ../Utils.hs 25 | 15: 15.hs ../Utils.hs ../Range.hs 26 | # Not gonna do 16 in Haskell 27 | 17: 17.hs ../Utils.hs ../Direction.hs 28 | 18: 18.hs 29 | 21a: 21a.hs 30 | 21b: 21b.hs 31 | -------------------------------------------------------------------------------- /2022/README.md: -------------------------------------------------------------------------------- 1 | # Advent of Code 2022 2 | 3 | My solutions for the [Advent of Code 2022](https://adventofcode.com/2022) 4 | 5 | I use these types of challenges as an excuse to learn (or brush up on) programming languages I'm not particularly familiar with. I'm continuing my trend from the past couple of years and continuing to practice with Haskell, a language I'm only somewhat familiar with. 6 | 7 | So don't expect this code to be particularly efficient, clean, or idiomatic. 8 | 9 | But, it works. 10 | 11 | For each puzzle, there are several files: 12 | * `01.hs` is my code to solve the puzzle. 13 | * `01.txt` is my version of the data file from AOC. 14 | * `01.md` is my diary of comments on the puzzle, and my thoughts on the solution. 15 | * `01.py` is the code I wrote while racing for the leaderboard, unpolished. 16 | 17 | Some common code between the puzzles has been collected in `../Utils.hs`. 18 | 19 | The diary pages also include my leaderboard positions for the puzzles for which I was able to be around when the puzzle opened, to race for the charts. I'm not able to do this every day, as in my timezone they open during business hours and sometimes work just gets in the way. But I enjoy it when they do. When I'm racing for the chart, often I'll do my first attempt in Python, as it's a language I'm much more familiar with (and can code in quickly), and only after I've submitted my solution, I'll go back and re-solve the puzzle in Haskell. 20 | -------------------------------------------------------------------------------- /2023/01.md: -------------------------------------------------------------------------------- 1 | # 1 – Numbers can be letters? 2 | That was... somewhat more challenging than previous day-1 challenges. Still entirely doable, for sure, but more than I was expecting for day 1. 3 | 4 | Part 1 was fine, no issue there, but part 2 had a pretty subtle trap (which is definitely there on purpose), and once I'd fallen into it, it was surprisingly difficult to get out of it. 5 | 6 | Namely, that the words can overlap. So if you, as I first did, try to replace the words with numbers like: 7 | ```py 8 | i = i.replace("one", "1") 9 | i = i.replace("two", "2") 10 | ... etc 11 | ``` 12 | then when it gets a string like the sample `eightwothree`, it'll replace the `two` with `2`, then it won't be able to see the `eight` because the `t` has been replaced. 13 | 14 | My first thought was to use regular expressions, since a regex like `/[0-9]|one|two|three|...etc/` could easily be applied to match the first digit, no matter what it potentially overlapped with. But it couldn't be easily applied to match the _last_ digit. 15 | 16 | Alternatively, I considered checking it manually, by iterating over each index in the string, and checking if the substring starting at that index matched any of the number words... but for some reason I decided that would take too long? I'm not sure why, thinking about that now it seems like it would be fine, but in the heat of the moment I dismissed this idea. 17 | 18 | Instead, I build a second regex like `/[0-9]|eno|owt|eerht|...etc/` and match it against the reversed line... then take the first match and re-reverse it, which gives me the final match from the string. Which finally gets me the answer I was after. But it took me a bit to get there. 19 | 20 | [71/286] 21 | -------------------------------------------------------------------------------- /2023/01.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import re 3 | 4 | dat = [ 5 | "twovgtprdzcjjzkq3ffsbcblnpq", 6 | "two8sixbmrmqzrrb1seven", 7 | "9964pfxmmr474", 8 | [...snip...] 9 | ] 10 | 11 | calibs = [] 12 | for i in dat: 13 | i = [c for c in i if c in '0123456789'] 14 | calibs.append(int(i[0] + i[-1])) 15 | print(sum(calibs)) 16 | 17 | #dat = ["two1nine","eightwothree","abcone2threexyz","xtwone3four","4nineeightseven2","zoneight234","7pqrstsixteen"] 18 | 19 | lookup = {j:str(i) for i,j in enumerate(["one","two","three","four","five","six","seven","eight","nine"], 1)} 20 | for i in "0123456789": 21 | lookup[i] = i 22 | re_num = re.compile("(%s)" % "|".join(lookup.keys())) 23 | re_rev = re.compile("(%s)" % "|".join(i[::-1] for i in lookup.keys())) 24 | 25 | calibs = [] 26 | for i in dat: 27 | first = re_num.search(i).group(1) 28 | first = lookup[first] 29 | last = re_rev.search(i[::-1]).group(1) 30 | last = lookup[last[::-1]] 31 | calibs.append(int(first + last)) 32 | #print(calibs) 33 | print(sum(calibs)) 34 | 35 | -------------------------------------------------------------------------------- /2023/02.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | dat = [ 3 | [(2 ,"blue", 3 ,"red"),( 3 ,"green", 3 ,"blue", 6 ,"red"),( 4 ,"blue", 6 ,"red"),( 2 ,"green", 2 ,"blue", 9 ,"red"),( 2 ,"red", 4 ,"blue")], 4 | [(4 ,"red", 1 ,"green"),( 3 ,"red"),( 13 ,"green", 5 ,"red", 3 ,"blue"),( 3 ,"green", 2 ,"red"),( 3 ,"blue", 5 ,"red", 3 ,"green"),( 2 ,"red", 3 ,"blue", 12 ,"green")], 5 | [...snip...] 6 | ] 7 | 8 | #dat = [[(3 ,"blue", 4 ,"red"),( 1 ,"red", 2 ,"green", 6 ,"blue"),( 2 ,"green")], [(1 ,"blue", 2 ,"green"),( 3 ,"green", 4 ,"blue", 1 ,"red"),( 1 ,"green", 1 ,"blue")], [(8 ,"green", 6 ,"blue", 20 ,"red"),( 5 ,"blue", 4 ,"red", 13 ,"green"),( 5 ,"green", 1 ,"red")], [(1 ,"green", 3 ,"red", 6 ,"blue"),( 3 ,"green", 6 ,"red"),( 3 ,"green", 15 ,"blue", 14 ,"red")], [(6 ,"red", 1 ,"blue", 3 ,"green"),( 2 ,"blue", 1 ,"red", 2 ,"green")], ] 9 | 10 | cap = {"red":12,"green":13,"blue":14} 11 | def possible(game): 12 | for step in game: 13 | for i in range(0,len(step),2): 14 | if step[i] > cap.get(step[i+1],0): 15 | return False 16 | return True 17 | 18 | print(sum(i for i, g in enumerate(dat, 1) if possible(g))) 19 | 20 | def power(game): 21 | req = {"red":0,"green":0,"blue":0} 22 | for step in game: 23 | for i in range(0,len(step),2): 24 | if step[i] > req[step[i+1]]: 25 | req[step[i+1]] = step[i] 26 | return req["red"] * req["green"] * req["blue"] 27 | print(sum(power(g) for g in dat)) 28 | -------------------------------------------------------------------------------- /2023/03.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from collections import defaultdict 3 | 4 | dat = [ 5 | "...........441.................367................296........................................567..47.....45.................947.............", 6 | "...606..........888.....................508..........*892................+..=138.381..967...............*....%......926...........218.......", 7 | [...snip...] 8 | ] 9 | #dat=["467..114..","...*......","..35..633.","......#...","617*......",".....+.58.","..592.....","......755.","...$.*....",".664.598.."] 10 | 11 | CY = len(dat) 12 | CX = len(dat[0]) 13 | 14 | symbols = {} 15 | numbers = {} 16 | for y in range(CY): 17 | for x in range(CX): 18 | if dat[y][x] not in ".0123456789": 19 | symbols[x,y] = dat[y][x] 20 | if dat[y][x] in "0123456789": 21 | if x >= 1 and dat[y][x-1] in "0123456789": 22 | continue 23 | for x2 in range(x, CX): 24 | if dat[y][x2] not in "0123456789": 25 | x2 -= 1 26 | break 27 | numbers[x,y] = (dat[y][x:x2+1], x2+1-x) 28 | 29 | def neighbours(x, y, l): 30 | for y2 in range(y-1,y+2): 31 | for x2 in range(x-1,x+l+1): 32 | if not (y2 == y and x <= x2 < x+l): 33 | if 0 <= x2 < CX and 0 <= y2 < CY: 34 | yield x2, y2 35 | 36 | parts = [] 37 | for (x, y), (n, l) in numbers.items(): 38 | if any(p in symbols for p in neighbours(x,y,l)): 39 | parts.append(int(n)) 40 | print(sum(parts)) 41 | 42 | g = defaultdict(list) 43 | for (x, y), (n, l) in numbers.items(): 44 | for p in neighbours(x, y, l): 45 | if p in symbols and symbols[p] == '*': 46 | g[p].append(int(n)) 47 | g = [n[0] * n[1] for p,n in g.items() if len(n) == 2] 48 | print(sum(g)) 49 | -------------------------------------------------------------------------------- /2023/04.md: -------------------------------------------------------------------------------- 1 | # 4 – Remember Kids, Gambling is Cool! 2 | Not a lot of interest to talk here about the puzzle, it was remarkably straightforward, in a language that has a `set` type with an intersect method built-in. The second half tried to pull the trap of counting copy of every card individually, instead of doing each card once and multiplying it by the number of copies, but it was so transparent that I didn't even consider doing it that way, and the easy answer fell right out. 3 | 4 | The good thing I can say is that I don't think there was any of that "subtle trap missing from the worked example" stuff that I was talking about yesterday... though, the puzzle description was pretty straightforward as it is, and I don't know that there was much room to a trap like that. If they really wanted to, they could have made it that, say, one of the last cards in the list had enough matches that you would run off the bottom of the list looking for cards to copy... but no, they specifically clarify in the puzzle description that this will never happen. So that's nice. 5 | 6 | Unfortunately, I missed the leaderboard race for this one. I wasn't, like, doing anything, or busy at all, I just ADHDed my way straight through until about an hour after release. And normally when I do that, I'll time myself and compare to the leaderboard, but I fat-fingered the stopwatch app and I don't have a proper time, either. All I can say is I took somewhere in the 6-7 minute range, so I'm pretty sure I would have missed the leaderboard for part 1, and probably got some small number of points for part 2, but I can't be more precise than that. A shame to miss them, but I'll live. 7 | -------------------------------------------------------------------------------- /2023/04.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | dat = [ 3 | [(4,33,89,61,95,36,5,30,26,55),(15,33,28,36,93,57,26,13,95,4,18,79,6,87,60,66,69,67,19,42,22,61,78,5,58)], 4 | [(9,16,48,75,82,61,56,91,3,27),(4,12,96,20,22,13,6,86,61,94,95,30,9,75,56,38,26,28,7,16,42,55,2,34,8)], 5 | [...snip...] 6 | ] 7 | 8 | #dat=[[(41,48,83,86,17),(83,86,6,31,17,9,48,53)],[(13,32,20,16,61),(61,30,68,82,17,32,24,19)],[(1,21,53,59,44),(69,82,63,72,16,21,14,1)],[(41,92,73,84,69),(59,84,76,51,58,5,54,83)],[(87,83,26,28,32),(88,30,70,12,93,22,82,36)],[(31,18,13,56,72),(74,77,10,23,35,67,36,11)],] 9 | 10 | s = [] 11 | for w,n in dat: 12 | m = set(w) & set(n) 13 | if m: 14 | s.append(1 << (len(m) - 1)) 15 | #print(s) 16 | print(sum(s)) 17 | 18 | a = [1] * len(dat) 19 | for i,(w,n) in enumerate(dat): 20 | m = set(w) & set(n) 21 | for j in range(i+1,i+1+len(m)): 22 | if j < len(dat): 23 | a[j] += a[i] 24 | #print(a) 25 | print(sum(a)) 26 | -------------------------------------------------------------------------------- /2023/05.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | seeds = [91926764,235794528,3279509610,[...snip...]] 3 | maps = [ 4 | [ 5 | (2076625497,3385713231,258448094), 6 | (933162806,1124446801,128749435), 7 | (744984268,625015359,188178538), 8 | [...snip...] 9 | ],[ 10 | [...snip...] 11 | ] 12 | ] 13 | 14 | #seeds = [79,14,55,13] 15 | #maps = [[(50,98,2), (52,50,48), ],[(0,15,37), (37,52,2), (39,0,15), ],[(49,53,8), (0,11,42), (42,0,7), (57,7,4), ],[(88,18,7), (18,25,70), ],[(45,77,23), (81,45,19), (68,64,13), ],[(0,69,1), (1,0,69), ],[(60,56,37), (56,93,4), ]] 16 | 17 | 18 | def domap(m, v): 19 | for dst, src, n in m: 20 | if src <= v < src + n: 21 | return v - src + dst 22 | return v 23 | s = list(seeds) 24 | for m in maps: 25 | s = [domap(m, i) for i in s] 26 | print(min(s)) 27 | 28 | def maprange(m, a, b): 29 | for dst, src, n in m: 30 | if src < b and a < src + n: 31 | ra = max(src, a) 32 | rb = min(src+n, b) 33 | yield (ra - src + dst, rb - src + dst) 34 | if a < src: 35 | yield from maprange(m, a, src) 36 | if b > src + n: 37 | yield from maprange(m, src+n, b) 38 | return 39 | yield (a, b) 40 | return 41 | def flatten(r): 42 | new = [] 43 | r.sort() 44 | for a, b in r: 45 | if new and a <= new[-1][1]: 46 | new[-1][1] = max(new[-1][1], b) 47 | else: 48 | new.append([a,b]) 49 | return new 50 | def mapranges(m, s): 51 | new = [] 52 | for r in s: 53 | new.extend(maprange(m, *r)) 54 | return flatten(new) 55 | s = [(seeds[i], seeds[i] + seeds[i+1]) for i in range(0, len(seeds), 2)] 56 | for m in maps: 57 | s = mapranges(m, s) 58 | print(s[0][0]) 59 | -------------------------------------------------------------------------------- /2023/06.bas: -------------------------------------------------------------------------------- 1 | 5 REM Solution for the Casio fx-5200P programmable calculator 2 | 10 R=1 3 | 20 INPUT "TIME=",T 4 | 30 INPUT "DIST=",D 5 | 40 A= SQR(T*T-4*D) 6 | 50 B=(T-A)/2 7 | 60 C=(T+A)/2 8 | 70 B= RND(B+0.5,-1) 9 | 80 C= RND(C+0.4999,-1) 10 | 90 R=R*(C-B) 11 | 100 PRINT R 12 | 110 GOTO 20 13 | -------------------------------------------------------------------------------- /2023/06.md: -------------------------------------------------------------------------------- 1 | # 6 – The Boat That Couldn't Slow Down 2 | Got trapped by my own smarts on this one... 3 | 4 | So, when you strip away all of the fluff, the puzzle is: given values `T` and `D`, what is the range of values `x` such that `x(T-x) > D`. And there are two clear ways to solve this: blindly loop over all the integers between 0 and T, and count them. Or solve the quadratic. And I, who have done many AoC puzzles before, assumed that part 2 was going to be a lot bigger so that the brute-force-loop strategy would be infeasible, and the maths solution would get there. 5 | 6 | So, I buckled down, and solved the quadratic. I briefly considered whether floating point was going to be an issue, and whether I'd need to find some integer sqrt function, but decided I was probably fine and would worry about it if it became a problem (which it didn't, happily). And, I was right, part 2 _did_ dramatically increase the size of the numbers involved, so that the brute-force loop would take a lot longer to run. 7 | 8 | Unfortunately... not by enough. The brute-force loop solution for part 2 still only takes, like, 4 seconds to run. That's a lot slower than part 1, but still not really very long in the grand scheme of things. Meanwhile I'd spent a good couple minutes analytically solving the puzzle and cracking open the quadratic. Ultimately the dataset just wasn't big enough for that time to have been worth it. 9 | 10 | I still think it was the right way to go, and given AoC's of the past, it's the strategy that will work more often than not. But unfortunately this time, I lost the race to people rushing into the no-thinking solution and getting lucky. 11 | 12 | [989/392] 13 | -------------------------------------------------------------------------------- /2023/06.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from math import floor, ceil, sqrt 3 | 4 | times = (45, 98, 83, 73) 5 | distances = (295, 1734, 1278, 1210) 6 | 7 | def solve(t, d): 8 | disc = sqrt(t*t - 4*d) 9 | x1 = (t - disc)/2 10 | x2 = (t + disc)/2 11 | return int(floor(x1)) + 1, int(ceil(x2)) 12 | 13 | res = 1 14 | for t, d in zip(times, distances): 15 | a, b = solve(t, d) 16 | res *= b - a 17 | print(res) 18 | 19 | t = int(''.join(map(str, times))) 20 | d = int(''.join(map(str, distances))) 21 | a, b = solve(t, d) 22 | print(b - a) 23 | -------------------------------------------------------------------------------- /2023/06b.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Alternate solution using the brute-force strategy, so I could compare speeds 3 | 4 | times = (45, 98, 83, 73) 5 | distances = (295, 1734, 1278, 1210) 6 | 7 | def solve(t, d): 8 | n = 0 9 | for i in range(t + 1): 10 | if i * (t - i) > d: 11 | n += 1 12 | return n 13 | 14 | res = 1 15 | for t, d in zip(times, distances): 16 | res *= solve(t, d) 17 | print(res) 18 | 19 | t = int(''.join(map(str, times))) 20 | d = int(''.join(map(str, distances))) 21 | print(solve(t, d)) 22 | -------------------------------------------------------------------------------- /2023/07.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from collections import Counter 3 | 4 | dat = [ 5 | ("398KA", 456), 6 | ("2J299", 282), 7 | ("8939K", 547), 8 | [...snip...] 9 | ] 10 | 11 | #dat = [("32T3K", 765),("T55J5", 684),("KK677", 28),("KTJJT", 220),("QQQJA", 483),] 12 | 13 | cardval = {c:i for i,c in enumerate("23456789TJQKA")} 14 | joker = cardval['J'] 15 | 16 | def rank(hand, jokers=False): 17 | hand = [cardval[i] for i in hand] 18 | h = Counter(hand) 19 | if jokers: 20 | hand = [-1 if i == joker else i for i in hand] 21 | numjokers = h.pop(joker, 0) 22 | h = [n for c,n in h.items()] 23 | h.sort() 24 | if jokers: 25 | if h: 26 | h[-1] += numjokers 27 | else: 28 | h.append(numjokers) 29 | 30 | match h: 31 | case [5]: 32 | return (7, *hand) 33 | case [1, 4]: 34 | return (6, *hand) 35 | case [2, 3]: 36 | return (5, *hand) 37 | case [1, 1, 3]: 38 | return (4, *hand) 39 | case [1, 2, 2]: 40 | return (3, *hand) 41 | case [1, 1, 1, 2]: 42 | return (2, *hand) 43 | case [1, 1, 1, 1, 1]: 44 | return (1, *hand) 45 | 46 | d = list(dat) 47 | d.sort(key=lambda x:rank(x[0])) 48 | print(sum(i*b for i, (_, b) in enumerate(d, 1))) 49 | 50 | d = list(dat) 51 | d.sort(key=lambda x:rank(x[0], True)) 52 | print(sum(i*b for i, (_, b) in enumerate(d, 1))) 53 | -------------------------------------------------------------------------------- /2023/08.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from math import lcm 3 | dat = [ 4 | ("RGT", ("HDG", "QJV")), 5 | ("QDM", ("GPB", "SXG")), 6 | ("DJN", ("TQD", "BQN")), 7 | [...snip...] 8 | ] 9 | m = "LRRLRRRLRRLLLRLLRRLR[...snip...]" 10 | 11 | #dat = [("AAA",("BBB", "CCC")), ("BBB",("DDD", "EEE")), ("CCC",("ZZZ", "GGG")), ("DDD",("DDD", "DDD")), ("EEE",("EEE", "EEE")), ("GGG",("GGG", "GGG")), ("ZZZ",("ZZZ", "ZZZ")),] 12 | #m = "RL" 13 | 14 | dd = dict(dat) 15 | 16 | p = "AAA" 17 | i = 0 18 | while p != "ZZZ": 19 | d = m[i % len(m)] 20 | p = dd[p][d == "R"] 21 | i += 1 22 | print(i) 23 | 24 | p = list(i for i in dd if i[-1] == "A") 25 | l = [[] for i in range(len(p))] 26 | i = 0 27 | while not all(len(i) == 2 for i in l): 28 | d = m[i % len(m)] 29 | p = [dd[j][d == "R"] for j in p] 30 | for j, x in enumerate(p): 31 | if len(l[j]) < 2 and x[-1] == "Z": 32 | l[j].append(i) 33 | i += 1 34 | l2 = [b-a for a,b in l] 35 | print(lcm(*l2)) 36 | -------------------------------------------------------------------------------- /2023/09.md: -------------------------------------------------------------------------------- 1 | # 9 – This puzzle seems derivative 2 | I don't have a lot to say about this puzzle, honestly. Both parts of the puzzle were just, write code to do exactly what the puzzle said to do, in exactly the way the puzzle said to do it. No particular need for any analysis or any fancy optimisations. 3 | 4 | There were some things you could do. Like, for instance, you could solve part 2 by simply reversing the input lists and then passing them to the code from part 1, rather than modifying your code to do the new thing. But even that really wasn't necessary as the changes weren't that severe. 5 | 6 | You could even go the full maths route, and notice that what they're getting you to do is find a polynomial fit through all of the provided points, and then evaluating that at the next point. So you could do that directly in another way. But this too, was overkill for the problem at hand. 7 | 8 | [38/58] 9 | -------------------------------------------------------------------------------- /2023/09.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | dat = [ 3 | (4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44), 4 | (8,10,21,62,176,448,1042,2259,4634,9116,17421,32738,61151,114506,216170,412530,795822,1549262,3035999,5973950,11778148), 5 | (16,30,62,134,281,559,1056,1909,3332,5662,9448,15671,26340,46040,85632,170374,356396,761786,1621354,3376772,6814703), 6 | [...snip...] 7 | ] 8 | 9 | #dat = [(0,3,6,9,12,15), (1,3,6,10,15,21), (10,13,16,21,30,45), ] 10 | 11 | def extrap(n): 12 | a = [n] 13 | while not all(i == 0 for i in n): 14 | n = [b-a for a,b in zip(n[:-1],n[1:])] 15 | a.append(n) 16 | return sum(i[-1] for i in a) 17 | print(sum(extrap(i) for i in dat)) 18 | 19 | def extrap2(n): 20 | a = [n] 21 | while not all(i == 0 for i in n): 22 | n = [b-a for a,b in zip(n[:-1],n[1:])] 23 | a.append(n) 24 | x = 0 25 | for i in reversed(a): 26 | x = i[0] - x 27 | return x 28 | #print([extrap2(i) for i in dat]) 29 | print(sum(extrap2(i) for i in dat)) 30 | -------------------------------------------------------------------------------- /2023/10.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | dat = """\ 3 | F--F7F7|7FJ..JF-7F|77FL-L7.L.L7LFFJ-77|7.L777LF7L|FJJ-7-F|J7-7FF-7..FFF7JF--L77FL--77-7-77-|7FJ.LL.|7L7-|-FJJ.-J.F-77|F---|7.F--|--.F-7-7.F7 4 | FJ-|J|JJLF7F77|||FJ7J-|J|LF7.L||LJ|L77LL-JL--F--.LLJ|-..7|FJ..FJ7F.J.|7L-L7.|--F.7J||L|J.JJLFJF77|-LJ-|-J-FJ-F|J|-7|-L|JJ.L7.|-F77-FFJLLF.L- 5 | [...snip...]""".split("\n") 6 | 7 | CY = len(dat) 8 | CX = len(dat[0]) 9 | 10 | for y in range(CY): 11 | for x in range(CX): 12 | if dat[y][x] == "S": 13 | dat[y] = dat[y][:x] + "F" + dat[y][x+1:] 14 | sx, sy = x, y 15 | break 16 | 17 | up = (0,-1) 18 | down = (0,1) 19 | left = (-1,0) 20 | right = (1,0) 21 | neighbours = { 22 | '|': (up, down), 23 | '-': (left, right), 24 | 'L': (up, right), 25 | 'J': (up, left), 26 | '7': (down, left), 27 | 'F': (down, right), 28 | '.': (), 29 | } 30 | 31 | dists = {} 32 | todo = {(sx,sy):0} 33 | while todo: 34 | x, y = min(todo.keys(), key=todo.__getitem__) 35 | d = todo.pop((x, y)) 36 | dists[x,y] = d 37 | for dx, dy in neighbours[dat[y][x]]: 38 | if (x+dx, y+dy) not in dists: 39 | if (x+dx, y+dy) not in todo or todo[(x+dx, y+dy)] > d+1: 40 | todo[(x+dx, y+dy)] = d + 1 41 | # print(max(dists.values())) ??? 42 | print(len(dists) // 2) 43 | 44 | 45 | n = 0 46 | for y in range(CY): 47 | for x in range(CX): 48 | if (x, y) in dists: 49 | #print('@', end='') 50 | continue 51 | inside = False 52 | for x2 in range(x, CY): 53 | if (x2, y) in dists and up in neighbours[dat[y][x2]]: 54 | inside = not inside 55 | if inside: 56 | n += 1 57 | #print('.' if inside else ' ', end='') 58 | #print() 59 | print(n) 60 | -------------------------------------------------------------------------------- /2023/11.md: -------------------------------------------------------------------------------- 1 | # 11 – Elfie In the Sky with Galaxies 2 | Whenever these puzzles present me with a big massive grid like this, with a bunch of info scattershot across it, my first instict is to extract all of the relevant information into a flat list. In this case, a list of all the gridpoints that contain a galaxy. In practise it seems that having the information in that form so often turns out to be easier to work with for these types of puzzles. 3 | 4 | In this case, it meant that the step of "expanding space" just involved adding a correction to the coordinates of all of these galaxies... as opposed to what I suspect is the trap solution, which is to actually add additional blank rows to the grid. 5 | 6 | This meant that for part 2, it was a simple case of adding a multiplying factor to that correction, to make it expand by more (though it did take me a few moments of thought to realise that this multiplying factor needed to be `999999`, not `1000000`). As opposed to adding millions of blank rows and columns to this grid. 7 | 8 | Realising that the "shortest path" search you had to do was just basic taxicab distance, and you weren't expected to do any sort of pathfinding, also helped. 9 | 10 | [47/15] 11 | -------------------------------------------------------------------------------- /2023/11.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | dat = """\ 3 | ...................#.............#...........................................#.............................................................. 4 | .......................................#.........#..................................................................#.........#........#.... 5 | [...snip...]""".split("\n") 6 | 7 | CY = len(dat) 8 | CX = len(dat[0]) 9 | 10 | blankrows = set() 11 | blankcols = set() 12 | 13 | #GAPSIZE = 2 14 | GAPSIZE = 1_000_000 15 | 16 | for y in range(CY): 17 | if all(dat[y][x] == '.' for x in range(CX)): 18 | blankrows.add(y) 19 | for x in range(CX): 20 | if all(dat[y][x] == '.' for y in range(CY)): 21 | blankcols.add(x) 22 | 23 | galaxies = [] 24 | for y in range(CY): 25 | for x in range(CX): 26 | if dat[y][x] == '#': 27 | x2 = x + len([i for i in blankcols if i < x]) * (GAPSIZE-1) 28 | y2 = y + len([i for i in blankrows if i < y]) * (GAPSIZE-1) 29 | galaxies.append((x2,y2)) 30 | 31 | l = 0 32 | for i in range(len(galaxies)): 33 | for j in range(i+1, len(galaxies)): 34 | x1, y1 = galaxies[i] 35 | x2, y2 = galaxies[j] 36 | l += abs(x1-x2) + abs(y1-y2) 37 | print(l) 38 | -------------------------------------------------------------------------------- /2023/12a.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -u 2 | dat = [ 3 | ("??????#??#??", (1,1,5,1)), 4 | ("?#?#??##?#?", (2,5,1)), 5 | [...snip...] 6 | ] 7 | 8 | #dat = [("???.###",(1,1,3)),(".??..??...?##.",(1,1,3)),("?#?#?#?#?#?#?#?",(1,3,1,6)),("????.#...#...",(4,1,1)),("????.######..#####.",(1,6,5)),("?###????????",(3,2,1)),] 9 | 10 | def gen(val): 11 | return tuple(len(i) for i in val.split(".") if i) 12 | 13 | def possibilities(val): 14 | if '?' in val: 15 | ix = val.index('?') 16 | yield from possibilities(val[:ix] + '.' + val[ix+1:]) 17 | yield from possibilities(val[:ix] + '#' + val[ix+1:]) 18 | else: 19 | yield val 20 | 21 | def count(val, criteria): 22 | return sum(1 for i in possibilities(val) if gen(i) == criteria) 23 | 24 | print(sum(count(a,b) for a,b in dat)) 25 | -------------------------------------------------------------------------------- /2023/12b.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -u 2 | dat = [ 3 | ("??????#??#??", (1,1,5,1)), 4 | ("?#?#??##?#?", (2,5,1)), 5 | [...snip...] 6 | ] 7 | 8 | #dat = [("???.###",(1,1,3)),(".??..??...?##.",(1,1,3)),("?#?#?#?#?#?#?#?",(1,3,1,6)),("????.#...#...",(4,1,1)),("????.######..#####.",(1,6,5)),("?###????????",(3,2,1)),] 9 | 10 | def possibilities2(val, criteria, startix=0, startcrit=0): 11 | if startcrit >= len(criteria): 12 | if startix >= len(val) or all(i in '.?' for i in val[startix:]): 13 | yield 1 14 | return 15 | 16 | for i in range(startix, len(val) - criteria[startcrit] + 1): 17 | if all(i in '#?' for i in val[i:i+criteria[startcrit]]) and (i+criteria[startcrit] >= len(val) or val[i+criteria[startcrit]] in '.?'): 18 | yield from possibilities2(val, criteria, i+criteria[startcrit]+1, startcrit+1) 19 | if val[i] == '#': 20 | break 21 | 22 | def count2(val, criteria): 23 | return sum(possibilities2(val, criteria)) 24 | 25 | print(sum(count2(a,b) for a,b in dat)) 26 | print(sum(count2("?".join([a]*5),b*5) for a,b in dat)) 27 | -------------------------------------------------------------------------------- /2023/12d.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -u 2 | dat = [ 3 | ("??????#??#??", (1,1,5,1)), 4 | ("?#?#??##?#?", (2,5,1)), 5 | [...snip...] 6 | ] 7 | 8 | #dat = [("???.###",(1,1,3)),(".??..??...?##.",(1,1,3)),("?#?#?#?#?#?#?#?",(1,3,1,6)),("????.#...#...",(4,1,1)),("????.######..#####.",(1,6,5)),("?###????????",(3,2,1)),] 9 | 10 | def possibilities4(v, c): 11 | def worker(val, criteria, dotsleft, hashesleft): 12 | #print(val, criteria, dotsleft) 13 | if not criteria: 14 | if any(i == '#' for i in val): 15 | #print("=>0") 16 | return 0 17 | else: 18 | #print("=>1") 19 | return 1 20 | 21 | val = val.lstrip('.') 22 | if not val: 23 | #print("bad end") 24 | return 0 25 | if val[0] == '#': 26 | if len(val) >= criteria[0] and not any(i == '.' for i in val[:criteria[0]]): 27 | if (criteria[0] == len(val) or val[criteria[0]] != '#'): 28 | rmhash = val[:criteria[0]].count('?') 29 | rmdot = (criteria[0] < len(val) and val[criteria[0]] == '?') 30 | if rmhash <= hashesleft and rmdot <= dotsleft: 31 | return worker(val[criteria[0]+1:], criteria[1:], dotsleft - rmdot, hashesleft - rmhash) 32 | #print("bad end 2") 33 | return 0 34 | else: 35 | assert val[0] == '?' 36 | res = 0 37 | if hashesleft: 38 | res = worker('#' + val[1:], criteria, dotsleft, hashesleft - 1) 39 | if dotsleft: 40 | res += worker(val[1:], criteria, dotsleft - 1, hashesleft) 41 | #print("==>", res) 42 | return res 43 | numhashes = sum(c) 44 | numdots = len(v) - sum(c) 45 | existhashes = v.count('#') 46 | existdots = v.count('.') 47 | return worker(v, c, numdots - existdots, numhashes - existhashes) 48 | 49 | print(sum(possibilities4(a,b) for a,b in dat)) 50 | print(sum(possibilities4("?".join([a]*5),b*5) for a,b in dat)) 51 | -------------------------------------------------------------------------------- /2023/13.md: -------------------------------------------------------------------------------- 1 | # 13 – Mirror, mirror, in the maze... 2 | I think I got a bit too clever for my own good here. 3 | 4 | I tried to get real fancy with the comparisons, I thought I'd save dev time by using string comparisons rather than writing the nested loops to check every cell individually. So like, instead of doing: 5 | ```py 6 | for y in range(...): 7 | for x in range(...): 8 | x2, y2 = reflected point 9 | if grid[y2][x2] != grid[y][x]: 10 | do whatever 11 | ``` 12 | I thought I could do something roughly like: 13 | ```py 14 | for y in range(...): 15 | y2 = reflected coordinate 16 | if grid[y2] != grid[y]: 17 | do whatever 18 | # or 19 | for y in range(...): 20 | if grid[y][:x] != grid[y][x:][::-1]: 21 | do whatever 22 | ``` 23 | except keeping track of all the intricacies of extracting the correct substrings, and iterating over the correct ranges, all got a bit much, and ironing out all the bugs took a lot longer than it really should have. While the direct loop would likely have been much simpler (and been easier to adapt to part 2). 24 | 25 | Ah well, live and learn. 26 | 27 | [167/205] 28 | -------------------------------------------------------------------------------- /2023/14.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | dat = """\ 3 | #..O...O...O....##O#O#O..O.#...O..O..#O.#O.#.O....##.OOOO....O.O.OO..O##..O..O....#O#....#..O.#.#.O. 4 | OO.....O.O...##..#.#.......#...#.O.#.#..#...#...#....##O......#O#........#...O......O.#......#O.O... 5 | [...snip...]""".split("\n") 6 | 7 | #dat = "O....#....\nO.OO#....#\n.....##...\nOO.#O....O\n.O.....O#.\nO.#..O.#.#\n..O..#O..O\n.......O..\n#....###..\n#OO..#....".split("\n") 8 | 9 | CY = len(dat) 10 | CX = len(dat[0]) 11 | 12 | cubes = set() 13 | balls = set() 14 | for y in range(CY): 15 | for x in range(CX): 16 | if dat[y][x] == '#': 17 | cubes.add((x,y)) 18 | elif dat[y][x] == 'O': 19 | balls.add((x,y)) 20 | 21 | def roll(balls, dx, dy): 22 | rolledballs = set() 23 | for x,y in sorted(balls, key=lambda i: (i[0]*dx+i[1]*dy), reverse=True): 24 | while 0 <= x+dx < CX and 0 <= y+dy < CY and (x+dx,y+dy) not in cubes and (x+dx,y+dy) not in rolledballs: 25 | x += dx 26 | y += dy 27 | rolledballs.add((x,y)) 28 | return rolledballs 29 | 30 | rolledballs = roll(balls, 0, -1) 31 | score = sum(CY-y for x,y in rolledballs) 32 | print(score) 33 | 34 | rolledballs = balls 35 | n = 0 36 | seen = {} 37 | hist = [] 38 | key = tuple(sorted(rolledballs)) 39 | while n < 1000000000 and key not in seen: 40 | seen[key] = n 41 | hist.append(rolledballs) 42 | rolledballs = roll(rolledballs, 0, -1) 43 | rolledballs = roll(rolledballs, -1, 0) 44 | rolledballs = roll(rolledballs, 0, 1) 45 | rolledballs = roll(rolledballs, 1, 0) 46 | key = tuple(sorted(rolledballs)) 47 | n += 1 48 | #print(n, seen[key]) 49 | step = n - seen[key] 50 | ofs = (1000000000 - seen[key]) % step 51 | res = hist[seen[key] + ofs] 52 | score = sum(CY-y for x,y in res) 53 | print(score) 54 | -------------------------------------------------------------------------------- /2023/15.md: -------------------------------------------------------------------------------- 1 | # 15 – Stay focused 2 | Have I sung the praises of the Python `collections` module yet this event? I'm pretty sure I have. But still, and again, all hail the Python `collections` module. 3 | 4 | Because, while part 1 of this was pretty straightforward, just implement the hash function as described... part 2 had a _lot_ of words spent on detailing the mechanics of how these hash buckets need to work... how they're ordered, with new items going on the end, but items can also be modified in-place without reordering, and items can be removed, which pulls them out of the ordering... 5 | 6 | And that just so happens to be _exactly_ the semantics of `collections.OrderedDict`, across the board. 7 | 8 | The hardest part of the whole thing was just keeping track of which parts of the puzzle used zero-indexed lists and which parts used one-indexed, because of course the puzzle used both. For the same list. 9 | 10 | [305/48] 11 | -------------------------------------------------------------------------------- /2023/15.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from collections import OrderedDict 3 | 4 | dat = "vpq=6,fbf-,pft-,[...snip...]".split(",") 5 | 6 | #dat = "rn=1,cm-,qp=3,cm=2,qp-,pc=4,ot=9,ab=5,pc-,pc=6,ot=7".split(",") 7 | 8 | def gethash(s): 9 | n = 0 10 | for i in s: 11 | n += ord(i) 12 | n = (n*17)%256 13 | return n 14 | print(sum(gethash(i) for i in dat)) 15 | 16 | boxes=[OrderedDict() for i in range(256)] 17 | for i in dat: 18 | if '-' in i: 19 | l, r = i.split('-') 20 | box = gethash(l) 21 | boxes[box].pop(l, None) 22 | else: 23 | l, r = i.split('=') 24 | box = gethash(l) 25 | r = int(r) 26 | boxes[box][l] = r 27 | #print([(i,v) for i,v in enumerate(boxes) if v]) 28 | print(sum(ix*slot*r for ix,box in enumerate(boxes,1) for slot, r in enumerate(box.values(), 1))) 29 | -------------------------------------------------------------------------------- /2023/16.md: -------------------------------------------------------------------------------- 1 | # 16 – Flashbacks of Aargon 2 | Another pretty straightforward one. Not that I'm complaining when the weekend ones are easier, it makes my scheduling a lot easier. 3 | 4 | Part 1 was just a case of following the directions as given, and tracing the path of the beam through the grid. The puzzle was vague about what needed to be the "end state" of the simulation... the obvious first guess is that it ends when all the beams exit the grid, but the existence of the "splitter" tiles (which aren't reversible) means that there is no guarantee that this process will actually end. Like, this simple setup has a beam bouncing around in a loop forever: 5 | ``` 6 | |- 7 | -| 8 | ``` 9 | However, it is all deterministic, so if we've already processed a beam at a particular location in a particular direction, then a later beam at the same location in the same direction isn't going to cause any _additional_ tiles to be energised, so we can ignore it. Essentially a flood-fill algorithm, keep track of everywhere we've been, and break the loop if we revisit anywhere. 10 | 11 | For part 2, I didn't immediately see any particular opportunities to speed it up... there was probably some amount of memoisation that could be done, but I didn't see anything immediate. So I just threw together the obvious, just testing each entry point individually and compare them. And I let that run while I tried to think of a faster way to do it... and it finished pretty quickly, before I came up with anything. 12 | 13 | [80/42] 14 | -------------------------------------------------------------------------------- /2023/17b.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | dat = """\ 3 | 314412134515235343233533333536633455653353226524463545367577644655564375735567774443767545555476326253354625332434443364224465621314212153555 4 | 541151354452244522426632553456444465355436252633745744545656765664467373344353475666347765767443443552326263264235346622346265464551153535535 5 | [...snip...]""" 6 | 7 | #dat = "2413432311323\n3215453535623\n3255245654254\n3446585845452\n4546657867536\n1438598798454\n4457876987766\n3637877979653\n4654967986887\n4564679986453\n1224686865563\n2546548887735\n4322674655533" 8 | 9 | dat = [[int(j) for j in i] for i in dat.split("\n")] 10 | 11 | CY = len(dat) 12 | CX = len(dat[0]) 13 | 14 | def solve(minl, maxl): 15 | costs = {(0,0,0,0):0} 16 | solved = set() 17 | todo = {(0,0,0,0)} 18 | while todo: 19 | x, y, prevdx, prevdy = min(todo, key=costs.__getitem__) 20 | todo.remove((x,y,prevdx, prevdy)) 21 | solved.add((x,y,prevdx, prevdy)) 22 | for dx, dy in [[-1,0],[1,0],[0,-1],[0,1]]: 23 | if dx == prevdx and dy == prevdy: 24 | continue 25 | if dx == -prevdx and dy == -prevdy: 26 | continue 27 | for l in range(minl, maxl): 28 | dx2 = dx * l 29 | dy2 = dy * l 30 | if not (0 <= x+dx2 < CX and 0 <= y+dy2 < CY): 31 | continue 32 | if (x+dx2, y+dy2, dx, dy) in solved: 33 | continue 34 | newcost = costs[x,y,prevdx,prevdy] 35 | for i in range(1, l+1): 36 | newcost += dat[y+dy*i][x+dx*i] 37 | if (x+dx2, y+dy2, dx, dy) not in costs or costs[(x+dx2, y+dy2, dx, dy)] > newcost: 38 | costs[(x+dx2, y+dy2, dx, dy)] = newcost 39 | todo.add((x+dx2, y+dy2, dx, dy)) 40 | return min(v for (x,y,_,_),v in costs.items() if x == CX-1 and y == CY-1) 41 | #print(solve(1,4)) 42 | print(solve(4,11)) 43 | -------------------------------------------------------------------------------- /2023/20.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | dat = { 3 | "jx": ('%',("rt", "rs")), 4 | "cc": ('&',("cd", "fc", "qr", "nl", "gk", "zr")), 5 | "qs": ('%',("cl", "rs")), 6 | [...snip...] 7 | } 8 | 9 | #dat = {"broadcaster":("b",('a','b','c')),'a':('%',('b',)),'b':('%',('c',)),'c':('%',('inv',)),'inv':('&',('a',)),} 10 | 11 | dat['rx'] = ('*', ()) 12 | 13 | inc = {k:[] for k in dat.keys()} 14 | for k,v in dat.items(): 15 | for i in v[1]: 16 | inc[i].append(k) 17 | 18 | flipflops = {} 19 | ands = {} 20 | def reset(): 21 | global flipflops, ands 22 | flipflops = {k:False for k,v in dat.items() if v[0] == '%'} 23 | ands = {k:{i:False for i in inc[k]} for k,v in dat.items() if v[0] == '&'} 24 | reset() 25 | 26 | lowpulses = 0 27 | highpulses = 0 28 | def run(stepnum): 29 | global lowpulses, highpulses 30 | rxseen = False 31 | todo = [('broadcaster', False, '')] 32 | while todo: 33 | node, level, prevnode = todo.pop(0) 34 | if level: 35 | highpulses += 1 36 | else: 37 | lowpulses += 1 38 | mode, links = dat[node] 39 | if mode == 'b': 40 | todo.extend([(n, level, node) for n in links]) 41 | elif mode == '%': 42 | if not level: 43 | flipflops[node] = not flipflops[node] 44 | todo.extend([(n, flipflops[node], node) for n in links]) 45 | elif mode == '&': 46 | ands[node][prevnode] = level 47 | if node == 'dn' and level: 48 | print(prevnode, stepnum) 49 | v = not all(ands[node].values()) 50 | todo.extend([(n, v, node) for n in links]) 51 | elif mode == '*': 52 | if not level: 53 | rxseen = True 54 | return rxseen 55 | 56 | for i in range(1000): 57 | run(i) 58 | print(lowpulses * highpulses) 59 | 60 | reset() 61 | i = 1 62 | while not run(i): 63 | i += 1 64 | print(i) 65 | -------------------------------------------------------------------------------- /2023/20.reordered.txt: -------------------------------------------------------------------------------- 1 | broadcaster -> bit0, xg, cd, sg 2 | 3 | %bit0 -> acc, bit1 4 | %bit1 -> acc, bit2 5 | %bit2 -> bit3 6 | %bit3 -> bit4, acc 7 | %bit4 -> acc, bit5 8 | %bit5 -> bit6, acc 9 | %bit6 -> bit7 10 | %bit7 -> acc, bit8 11 | %bit8 -> bit9, acc 12 | %bit9 -> bit10, acc 13 | %bit10 -> bit11, acc 14 | %bit11 -> acc 15 | &acc -> bit6, bit0, out, bit2 16 | &out -> finalacc 17 | 18 | %xg -> cf, pm 19 | %cf -> gj, pm 20 | %gj -> zd, pm 21 | %zd -> jv, pm 22 | %jv -> sp 23 | %sp -> pc 24 | %pc -> kt, pm 25 | %kt -> lt 26 | %lt -> pm, mx 27 | %mx -> nr, pm 28 | %nr -> vf, pm 29 | %vf -> pm 30 | &pm -> kt, xg, xp, jv, sp 31 | &xp -> finalacc 32 | 33 | %cd -> cc, nl 34 | %nl -> pj 35 | %pj -> cc, mj 36 | %mj -> qr, cc 37 | %qr -> gk 38 | %gk -> ln 39 | %ln -> zr, cc 40 | %zr -> cq 41 | %cq -> cj, cc 42 | %cj -> cc, nt 43 | %nt -> mn, cc 44 | %mn -> cc 45 | &cc -> cd, fc, qr, nl, gk, zr 46 | &fc -> finalacc 47 | 48 | %sg -> rs, rh 49 | %rh -> nb, rs 50 | %nb -> sl 51 | %sl -> kx 52 | %kx -> jx 53 | %jx -> rt, rs 54 | %rt -> qq 55 | %qq -> rs, hd 56 | %hd -> qs, rs 57 | %qs -> cl, rs 58 | %cl -> zx, rs 59 | %zx -> rs 60 | &rs -> sg, dd, sl, kx, nb, rt 61 | &dd -> finalacc 62 | 63 | &finalacc -> rx 64 | -------------------------------------------------------------------------------- /2023/21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/e3fe0fbd8a26e4e0941ed72a032202416f1cc20c/2023/21.png -------------------------------------------------------------------------------- /2023/22.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | dat = [ 3 | ((7,6,107),(7,8,107)), 4 | ((4,6,229),(4,6,231)), 5 | ((6,4,110),(8,4,110)), 6 | [...snip...] 7 | ] 8 | 9 | #dat = [((1,0,1),(1,2,1)), ((0,0,2),(2,0,2)), ((0,2,3),(2,2,3)), ((0,0,4),(0,2,4)), ((2,0,5),(2,2,5)), ((0,1,6),(2,1,6)), ((1,1,8),(1,1,9)),] 10 | 11 | bricks = list(dat) 12 | bricks.sort(key=lambda x:min(x[0][2], x[1][2])) 13 | offsets = [None] * len(dat) 14 | supported = [None] * len(dat) 15 | covered = {} 16 | for i, ((x1,y1,z1),(x2,y2,z2)) in enumerate(bricks): 17 | def isok(zofs): 18 | touching = set() 19 | if z1 - zofs <= 0 or z2 - zofs <= 0: 20 | touching.add(-1) 21 | for x in range(x1,x2+1): 22 | for y in range(y1,y2+1): 23 | for z in range(z1-zofs,z2-zofs+1): 24 | if (x,y,z) in covered: 25 | touching.add(covered[x,y,z]) 26 | return touching 27 | zofs = 0 28 | while True: 29 | touch = isok(zofs + 1) 30 | if touch: 31 | break 32 | zofs = zofs + 1 33 | offsets[i] = zofs 34 | supported[i] = touch 35 | for x in range(x1,x2+1): 36 | for y in range(y1,y2+1): 37 | for z in range(z1-zofs,z2-zofs+1): 38 | covered[x,y,z] = i 39 | 40 | #print(supported) 41 | unremovable = set.union(*(i for i in supported if len(i) == 1)) 42 | unremovable.remove(-1) 43 | print(len(bricks) - len(unremovable)) 44 | 45 | cascade = [None] * len(dat) 46 | for i in range(len(dat)): 47 | fallen = {i} 48 | for j in range(i+1, len(dat)): 49 | if not supported[j] - fallen: 50 | fallen.add(j) 51 | cascade[i] = len(fallen) - 1 52 | print(sum(cascade)) 53 | -------------------------------------------------------------------------------- /2023/README.md: -------------------------------------------------------------------------------- 1 | # Advent of Code 2023 2 | 3 | My solutions for the [Advent of Code 2023](https://adventofcode.com/2023) 4 | 5 | After previous years, I'm taking a break from using AoC to try to learn another language, as the pattern of solving the problem in a language I know for the leaderboard race, and then solving it again in the language I'm trying to learn, just wasn't working for me. I can only really do one or the other, and I'm choosing to do the leaderboard race. So only garbage thrown-together Python implementations here this year. 6 | -------------------------------------------------------------------------------- /2024/01.md: -------------------------------------------------------------------------------- 1 | # 1 – Location, location, location 2 | Ah, Advent of Code time again, one of my favourite times of year. 3 | 4 | Easing it off with a pretty simple puzzle, as usual. The code I put together here is pretty directly taking the requirements from the puzzle as written, and turning them directly into code. 5 | 6 | The result isn't the most efficient... for part 2, I think it might be cleaner to run `Counter`s over _both_ input lists, and iterate over the intersection... the actual calculation you're doing is, in fact, symmetric between the two lists, it's just not described that way in the puzzle statement. So what I wrote is as written, where you iterate through one list, and count how many matching entries there are in the second list. 7 | 8 | I did lose a little bit of time on part 2 from re-reading the puzzle multiple times, I was sure I was missing some detail, but no, I read it right the first time. 9 | 10 | I also lost a little thinking about how `Counter` works... specifically, whether `counter[x]` returns `0` if there are no `x`s in the count, or if it raises `KeyError`... I guessed it _probably_ returned `0`, but I wasn't completely certain. But I quickly decided that the fastest option was just to use `counter.get(x, 0)` anyway, even though it was a little bit of extra typing, it was faster than spending the time to _check_ how it worked. Turns out, it wasn't necessary, as it does return `0`, but I feel good about making that choice in the moment. 11 | 12 | [34/73] 13 | -------------------------------------------------------------------------------- /2024/01.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import re 3 | 4 | dat = [ 5 | (15244, 50562), 6 | (81245, 49036), 7 | # ... 8 | ] 9 | 10 | a = [i[0] for i in dat] 11 | b = [i[1] for i in dat] 12 | a.sort() 13 | b.sort() 14 | n = sum(abs(i-j) for i,j in zip(a,b)) 15 | print(n) 16 | 17 | from collections import Counter 18 | b = Counter(b) 19 | n = sum(b.get(i, 0)*i for i in a) 20 | print(n) 21 | -------------------------------------------------------------------------------- /2024/02.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import re 3 | 4 | dat = [ 5 | (1,3,5,6,8,9,12,9), 6 | (66,67,70,72,73,74,75,75), 7 | (18,20,22,25,28,31,35), 8 | # ... 9 | ] 10 | 11 | def safe(s): 12 | s = list(s) 13 | s_ = list(sorted(s)) 14 | if s == s_ or s == s_[::-1]: 15 | if all(i != j and j-3 <= i <= j+3 for i,j in zip(s,s[1:])): 16 | return True 17 | return False 18 | print(sum(1 for i in dat if safe(i))) 19 | 20 | def safe2(s): 21 | return safe(s) or any(safe(s[:i]+s[i+1:]) for i in range(len(s))) 22 | print(sum(1 for i in dat if safe2(i))) 23 | -------------------------------------------------------------------------------- /2024/03.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from collections import defaultdict, Counter 3 | from fractions import Fraction 4 | from copy import deepcopy 5 | from pprint import pprint 6 | import re, math 7 | 8 | dat = """what()who(){from(),'mul(28,510)?<,>where()why()mul(...etc...""" 9 | 10 | n = 0 11 | for a, b in re.findall(r"mul\((\d+),(\d+)\)", dat): 12 | n += int(a) * int(b) 13 | print(n) 14 | 15 | n = 0 16 | enabled = True 17 | for a, b, c in re.findall(r"mul\((\d+),(\d+)\)|(do(?:n't)?)\(\)", dat): 18 | if c == "do": 19 | enabled = True 20 | elif c == "don't": 21 | enabled = False 22 | elif enabled: 23 | n += int(a) * int(b) 24 | print(n) 25 | -------------------------------------------------------------------------------- /2024/04.md: -------------------------------------------------------------------------------- 1 | # 4 – Do (not) find the Xmas 2 | 3 | I thought had a bit of a head-start on this one, as I already had a handy function for unrolling a wordsearch, from a previous project doing some statistical analysis of the "Do not find the fox" game played on YouTube by [AlexCheddarUK](https://www.youtube.com/@AlexCheddarUK/shorts). Unfortunately, it took me a minute to track it down, as I had a couple of different versions of the thing, as I'd made some changes for different parts of that project, and I had to find the one that had the features I needed (works on a 2D grid, not a flattened list, and used a generic `SIZE` constant instead of having it hard-coded). But I still think this was faster than trying to re-write it from scratch. 4 | 5 | This function pulls out all of the rows, columns, and both diagonals, as a single long string, which can then be searched through to find all of the relevant words. Have to check for `XMAS` and `SAMX` separately, of course, since it's only pulling out each row/col/etc once, not in both directions. 6 | 7 | For part 2, I throw all of that away and do a scan through for the particular shape we're after. I search through the grid looking for `A`s, and then use `set` magic to check that both of the corners on each diagonal contain both an `M` and an `S` (in either order). This strategy came together pretty quickly. 8 | 9 | [245/45] 10 | -------------------------------------------------------------------------------- /2024/04.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from collections import defaultdict, Counter 3 | from fractions import Fraction 4 | from copy import deepcopy 5 | from pprint import pprint 6 | import re, math 7 | 8 | dat = """SSSMXMMSMMXXXS[...]""".split("\n") 9 | 10 | assert len(dat) == len(dat[0]) 11 | SIZE = len(dat) 12 | 13 | def unroll(board): 14 | rows = ["".join(row) for row in board] 15 | cols = ["".join(col) for col in zip(*board)] 16 | diags = ["".join(board[j][j-i] for j in range(max(0,i), SIZE+min(0, i))) for i in range(-SIZE+1,SIZE)] 17 | rev_diags = ["".join(board[SIZE-j-1][j-i] for j in range(max(0,i), SIZE+min(0, i))) for i in range(-SIZE+1,SIZE)] 18 | return "-".join(rows + cols + diags + rev_diags) 19 | 20 | a = unroll(dat) 21 | print(a.count("XMAS") + a.count("SAMX")) 22 | 23 | 24 | n = 0 25 | for y in range(1,SIZE-1): 26 | for x in range(1,SIZE-1): 27 | if dat[y][x] != "A": 28 | continue 29 | if {dat[y-1][x-1], dat[y+1][x+1]} != {"M", "S"}: 30 | continue 31 | if {dat[y-1][x+1], dat[y+1][x-1]} != {"M", "S"}: 32 | continue 33 | n += 1 34 | print(n) 35 | -------------------------------------------------------------------------------- /2024/05.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from collections import defaultdict, Counter 3 | from fractions import Fraction 4 | from copy import deepcopy 5 | from pprint import pprint 6 | import re, math 7 | 8 | dat = [ 9 | (69,26), 10 | (93,46), 11 | (93,43), 12 | (46,53), 13 | # ... 14 | ] 15 | 16 | dat2 = [ 17 | (57,47,82,32,18), 18 | (74,56,86,81,84,44,53,92,12,36,15,66,95,26,71), 19 | (26,68,47,42,73,41,52,44,78,64,24,76,29,82,38), 20 | (98,78,59,22,91), 21 | # ... 22 | ] 23 | 24 | pairs = set(dat) 25 | 26 | n = 0 27 | for row in dat2: 28 | if all((row[j], row[i]) not in pairs for i in range(len(row)-1) for j in range(i+1,len(row))): 29 | n += row[len(row)//2] 30 | print(n) 31 | 32 | from functools import cmp_to_key 33 | def cmp(a, b): 34 | if (a,b) in pairs: 35 | return 1 36 | elif (b,a) in pairs: 37 | return -1 38 | else: 39 | 1/0 40 | key = cmp_to_key(cmp) 41 | n = 0 42 | for row in dat2: 43 | if all((row[j], row[i]) not in pairs for i in range(len(row)-1) for j in range(i+1,len(row))): 44 | continue 45 | row = list(row) 46 | row.sort(key=key) 47 | n += row[len(row)//2] 48 | print(n) 49 | -------------------------------------------------------------------------------- /2024/06.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from collections import defaultdict, Counter 3 | from fractions import Fraction 4 | from copy import deepcopy 5 | from pprint import pprint 6 | import re, math 7 | 8 | dat = """..................#.....[...]""".split("\n") 9 | 10 | CX = len(dat) 11 | CY = len(dat[0]) 12 | 13 | sx = sy = None 14 | for y, row in enumerate(dat): 15 | for x, c in enumerate(row): 16 | if c == "^": 17 | sx, sy = x, y 18 | break 19 | dat = [[c == "#" for c in row] for row in dat] 20 | 21 | def go(): 22 | dx, dy = 0, -1 23 | px, py = sx, sy 24 | seen = {(px, py)} 25 | seen_dir = set() 26 | while True: 27 | if not (0 <= px+dx < CX and 0 <= py+dy < CY): 28 | return len(seen) 29 | if (px, py, dx, dy) in seen_dir: 30 | return None 31 | seen_dir.add((px, py, dx, dy)) 32 | if dat[py+dy][px+dx]: 33 | dx, dy = -dy, dx 34 | else: 35 | px += dx 36 | py += dy 37 | seen.add((px, py)) 38 | 39 | print(go()) 40 | 41 | n = 0 42 | for y in range(CY): 43 | #print(y) 44 | for x in range(CX): 45 | if dat[y][x] or (x == sx and y == sy): 46 | continue 47 | dat[y][x] = True 48 | if go() is None: 49 | n += 1 50 | dat[y][x] = False 51 | print(n) 52 | -------------------------------------------------------------------------------- /2024/07.md: -------------------------------------------------------------------------------- 1 | # 7 – 2 | 3 | Second day in a row where brute force still works, but on the borderline, with runtimes in the tens of seconds. 4 | 5 | Just directly throwing it all together, using `itertools.product` to generate a list of possible operator combinations, and then evaluating each one to see if it works. Quite fast for the part 1, but slow enough to be a concern for part 2. 6 | 7 | There were certainly optimisations to be had... on one hand, if evaluating an expression didn't work, and then all that's changed is the last operator, then really it should only need to recalculate that last step, but I have it recalculating the entire expression from the start every time. On the other hand, all three of the operators can only increase the value, so if the accumulated value is ever larger than the target, it could in theory bail out early and not bother evaluating any of the operator options after that point. 8 | 9 | Even better would be to try to solve it from the outside in... like, if the target value isn't a multiple of the last operand, then the last operator can't be `*`. And if the target value doesn't end with the same digits as the last operand, then the last operator can't be `||`. So that limits your options for that last operator, iterate through _those_ and work backwards to figure out a new target value for the second-last operator, and recurse. 10 | 11 | But in the moment, didn't do any of that, and we're still at the point in the event where I can get away with that. 12 | 13 | [315/226] 14 | -------------------------------------------------------------------------------- /2024/07.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from collections import defaultdict, Counter 3 | from fractions import Fraction 4 | from copy import deepcopy 5 | from pprint import pprint 6 | import re, math 7 | import itertools 8 | 9 | dat = [ 10 | (2382106471,(2,8,175,1,17,3,5,9,4,51,5)), 11 | (864708004,(278,22,259,2,12,3)), 12 | [...] 13 | ] 14 | 15 | #dat = [(190,(10,19)),(3267,(81,40,27)),(83,(17,5)),(156,(15,6)),(7290,(6,8,6,15)),(161011,(16,10,13)),(192,(17,8,14)),(21037,(9,7,18,13)),(292,(11,6,16,20))] 16 | 17 | def solvable(vals, res, ops="+*"): 18 | for i in itertools.product(ops, repeat=len(vals)-1): 19 | if calc(vals, i) == res: 20 | return True 21 | return False 22 | 23 | def calc(vals, ops): 24 | i = vals[0] 25 | for op, v in zip(ops, vals[1:]): 26 | if op == "+": 27 | i += v 28 | elif op == "*": 29 | i *= v 30 | elif op == "|": 31 | i = int(f"{i}{v}") 32 | else: 33 | 1/0 34 | return i 35 | 36 | n = 0 37 | for res, vals in dat: 38 | if solvable(vals, res): 39 | n += res 40 | print(n) 41 | 42 | n = 0 43 | for res, vals in dat: 44 | if solvable(vals, res, "+*|"): 45 | n += res 46 | print(n) 47 | -------------------------------------------------------------------------------- /2024/08.md: -------------------------------------------------------------------------------- 1 | # 8 – RF diffraction 2 | 3 | A lot of discussion on this one to do with some ambiguity in the puzzle description... in that the puzzle for part 2 says "any grid position exactly in line with two antennas of the same frequency", but the worked description implies you should just be looking at integer multiples of the vector between the antennas. In theory, if there are, for example, two antennas that are `(2,4)` apart, then it should result in antinodes separated by `(1,2)`, as all of those points are "in line" with those two antennas. Including the point directly _between_ the two antennas. 4 | 5 | However, as it turns out, this ambiguity doesn't come up, for the puzzle input, no two antennas of the same frequency are separated by a vector where the two components share a common factor... they're all already in least terms, when treated as a fraction (or a gradient). So either interpretation will get you the right answer. 6 | 7 | For me in particular, I didn't think about it too hard... I kinda realised the ambiguity was there, in the back of my mind, but since it wasn't called out in the worked example I decided to ignore it and hope everything would be fine, and it was. If it had given me the wrong answer, then I would have looked into it then. 8 | 9 | So I just go with the basic solution... I group all the antennae together by their frequency, and then go through all the pairs and, for part 1, mark the two points on either side, and for part 2, iterate in each direction until I leave the range of the grid, marking each step. And use a `set` to automatically dedupe the results. 10 | 11 | [128/82] 12 | -------------------------------------------------------------------------------- /2024/08.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from collections import defaultdict, Counter 3 | from fractions import Fraction 4 | from copy import deepcopy 5 | from pprint import pprint 6 | import re, math 7 | import itertools 8 | 9 | dat = """..........W......8..............................4. 10 | ............W...................F............1.L.. 11 | [...]""".split("\n") 12 | 13 | #dat = "............\n........0...\n.....0......\n.......0....\n....0.......\n......A.....\n............\n............\n........A...\n.........A..\n............\n............".split("\n") 14 | 15 | CX = len(dat[0]) 16 | CY = len(dat) 17 | 18 | ants = [(x,y,c) for y,row in enumerate(dat) for x, c in enumerate(row) if c != "."] 19 | grouped = defaultdict(list) 20 | for x,y,c in ants: 21 | grouped[c].append((x, y)) 22 | 23 | antis = set() 24 | for grp in grouped.values(): 25 | for i in range(len(grp)-1): 26 | for j in range(i+1,len(grp)): 27 | x1,y1 = grp[i] 28 | x2,y2 = grp[j] 29 | xa = x1 - (x2 - x1) 30 | ya = y1 - (y2 - y1) 31 | xb = x2 - (x1 - x2) 32 | yb = y2 - (y1 - y2) 33 | if 0 <= xa < CX and 0 <= ya < CY: 34 | antis.add((xa, ya)) 35 | if 0 <= xb < CX and 0 <= yb < CY: 36 | antis.add((xb, yb)) 37 | print(len(antis)) 38 | 39 | antis = set() 40 | for grp in grouped.values(): 41 | for i in range(len(grp)-1): 42 | for j in range(i+1,len(grp)): 43 | x1,y1 = grp[i] 44 | x2,y2 = grp[j] 45 | dx = x2 - x1 46 | dy = y2 - y1 47 | n = 0 48 | while 0 <= x1 + n*dx < CX and 0 <= y1 + n*dy < CY: 49 | antis.add((x1+n*dx,y1+n*dy)) 50 | n += 1 51 | n = 0 52 | while 0 <= x1 + n*dx < CX and 0 <= y1 + n*dy < CY: 53 | antis.add((x1+n*dx,y1+n*dy)) 54 | n -= 1 55 | print(len(antis)) 56 | -------------------------------------------------------------------------------- /2024/10.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from collections import defaultdict, Counter 3 | from fractions import Fraction 4 | from copy import deepcopy 5 | from pprint import pprint 6 | import re, math 7 | import itertools 8 | 9 | dat = """123454078104569871014321021[...]""".split("\n") 10 | 11 | #dat = "89010123\n78121874\n87430965\n96549874\n45678903\n32019012\n01329801\n10456732".split("\n") 12 | 13 | CY = len(dat) 14 | CX = len(dat[0]) 15 | 16 | dat = [[int(i) for i in row] for row in dat] 17 | 18 | def eval(x,y): 19 | seen = set() 20 | todo = [(x,y)] 21 | n = 0 22 | while todo: 23 | x,y = todo.pop() 24 | if (x,y) in seen: 25 | continue 26 | seen.add((x,y)) 27 | c = dat[y][x] 28 | if c == 9: 29 | n += 1 30 | continue 31 | for nx,ny in [(x-1,y),(x+1,y),(x,y-1),(x,y+1)]: 32 | if 0 <= nx < CX and 0 <= ny < CY and dat[ny][nx] == c + 1: 33 | todo.append((nx, ny)) 34 | return n 35 | 36 | n = 0 37 | for y, row in enumerate(dat): 38 | for x, c in enumerate(row): 39 | if c == 0: 40 | n += eval(x,y) 41 | print(n) 42 | 43 | def eval2(x,y): 44 | seen = {} 45 | todo = [(x,y,None,None)] 46 | tot = 0 47 | while todo: 48 | x,y,fx,fy = todo.pop(0) 49 | if fx is None: 50 | n = 1 51 | else: 52 | n = seen[fx,fy] 53 | c = dat[y][x] 54 | if c == 9: 55 | tot += n 56 | continue 57 | if (x,y) in seen: 58 | seen[x,y] += n 59 | continue 60 | seen[x,y] = n 61 | for nx,ny in [(x-1,y),(x+1,y),(x,y-1),(x,y+1)]: 62 | if 0 <= nx < CX and 0 <= ny < CY and dat[ny][nx] == c + 1: 63 | todo.append((nx, ny, x, y)) 64 | return tot 65 | 66 | n = 0 67 | for y, row in enumerate(dat): 68 | for x, c in enumerate(row): 69 | if c == 0: 70 | n += eval2(x,y) 71 | print(n) 72 | -------------------------------------------------------------------------------- /2024/11.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from collections import defaultdict, Counter 3 | from fractions import Fraction 4 | from copy import deepcopy 5 | from pprint import pprint 6 | import re, math 7 | import itertools 8 | 9 | dat = (7568, 155731, ...) 10 | 11 | #dat = (125, 17) 12 | 13 | #def step(line): 14 | # def gen(): 15 | # for i in line: 16 | # if i == 0: 17 | # yield 1 18 | # else: 19 | # s = str(i) 20 | # if len(s) % 2 == 0: 21 | # yield int(s[:len(s)//2]) 22 | # yield int(s[len(s)//2:]) 23 | # else: 24 | # yield i*2024 25 | # return tuple(gen()) 26 | # 27 | #a = dat 28 | #for i in range(25): 29 | # a = step(a) 30 | #print(len(a)) 31 | 32 | CACHE = {} 33 | 34 | def score(n, levels): 35 | if (n, levels) not in CACHE: 36 | CACHE[n, levels] = calcscore(n, levels) 37 | return CACHE[n, levels] 38 | 39 | def calcscore(n, levels): 40 | if levels <= 0: 41 | return 1 42 | if n == 0: 43 | return score(1, levels-1) 44 | s = str(n) 45 | if len(s) % 2 == 0: 46 | return score(int(s[:len(s)//2]), levels-1) + score(int(s[len(s)//2:]), levels-1) 47 | else: 48 | return score(n*2024, levels-1) 49 | 50 | print(sum(score(i, 25) for i in dat)) 51 | print(sum(score(i, 75) for i in dat)) 52 | -------------------------------------------------------------------------------- /2024/13.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from collections import defaultdict, Counter 3 | from fractions import Fraction 4 | from copy import deepcopy 5 | from pprint import pprint 6 | import re, math 7 | from itertools import count, cycle, repeat, chain, groupby, product, permutations, combinations 8 | from functools import cache, cmp_to_key, partial, reduce 9 | 10 | dat = [ 11 | (+63, +14, +12, +37, 5921, 10432), 12 | [...] 13 | ] 14 | 15 | #dat = [(+94, +34, +22, +67, 8400, 5400), (+26, +66, +67, +21, 12748, 12176), (+17, +86, +84, +37, 7870, 6450), (+69, +23, +27, +71, 18641, 10279)] 16 | 17 | def solve(a,b,c,d,x,y): 18 | det = a*d-b*c 19 | if det == 0: 20 | raise ValueError("det == 0") 21 | det = Fraction(1,det) 22 | a, b, c, d = d*det, -b*det, -c*det, a*det 23 | ra, rb = a*x + c*y, b*x + d*y 24 | if ra == int(ra) and rb == int(rb): 25 | return int(ra), int(rb) 26 | else: 27 | return None, None 28 | 29 | n = 0 30 | for line in dat: 31 | a, b = solve(*line) 32 | if a is not None: 33 | n += a*3+b 34 | print(n) 35 | 36 | n = 0 37 | for line in dat: 38 | a, b = solve(*line[:4], line[4] + 10000000000000, line[5] + 10000000000000) 39 | if a is not None: 40 | n += a*3+b 41 | print(n) 42 | -------------------------------------------------------------------------------- /2024/14_0000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/e3fe0fbd8a26e4e0941ed72a032202416f1cc20c/2024/14_0000.png -------------------------------------------------------------------------------- /2024/14_0018.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/e3fe0fbd8a26e4e0941ed72a032202416f1cc20c/2024/14_0018.png -------------------------------------------------------------------------------- /2024/14_0076.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/e3fe0fbd8a26e4e0941ed72a032202416f1cc20c/2024/14_0076.png -------------------------------------------------------------------------------- /2024/14_7492.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/e3fe0fbd8a26e4e0941ed72a032202416f1cc20c/2024/14_7492.png -------------------------------------------------------------------------------- /2024/18.md: -------------------------------------------------------------------------------- 1 | # 18 – Pathfinding 101 2 | 3 | Well, that was unexpected. After the difficulty spike yesterday, I was expecting today's puzzle to go in a quite different, more difficult direction... I was sure part 2 was going to be something along the lines of "each step you take through the maze, an additional tile becomes corrupted, how quickly can you get through this ever-shifting maze?" They've done similar puzzles [before](../2022/24.md), after all. 4 | 5 | But no, part 2 still doesn't have us trying to pathfind through a changing maze, just repeatedly doing pathfind solves through different static mazes, to find which ones are solvable. 6 | 7 | For part 1, I yet again use a hastily thrown-together Dijkstra's solver. Though, given we're walking through a grid one step at a time, a simple BFS would have been sufficient. 8 | 9 | For part 2, we reuse the Dijkstra's solver to check if the maze is solvable (but now we don't care about how long it takes, so this is overkill, a simple floodfill algorithm would be more than enough) and do a binary search to find the cutoff where it goes from solveable to unsolveable. 10 | 11 | Bing bang bosh, and we're done in time for tea. 12 | 13 | [628/435] 14 | -------------------------------------------------------------------------------- /2024/18.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from aocimports import * 3 | 4 | dat = [ 5 | (27,6), 6 | (24,29), 7 | [...] 8 | ] 9 | 10 | CX = CY = 71 11 | N = 1024 12 | 13 | #dat = [(5,4),(4,2),(4,5),(3,0),(2,1),(6,3),(2,4),(1,5),(0,6),(3,3),(2,6),(5,1),(1,2),(5,5),(2,5),(6,5),(1,4),(0,4),(6,4),(1,1),(6,1),(1,0),(0,5),(1,6),(2,0),] 14 | #CX = CY = 7 15 | #N = 12 16 | 17 | def solve(nblocks): 18 | blocks = set(dat[:nblocks]) 19 | todo = {(0,0)} 20 | solved = set() 21 | best = {(0,0):0} 22 | while todo and (CX-1,CY-1) not in solved: 23 | x, y = min(todo, key=best.__getitem__) 24 | todo.remove((x, y)) 25 | if (x, y) in solved: 26 | continue 27 | solved.add((x, y)) 28 | d = best[x, y] 29 | for nx, ny in [(x-1,y), (x+1,y), (x,y-1), (x,y+1)]: 30 | if 0 <= nx < CX and 0 <= ny < CY and (nx, ny) not in solved and (nx, ny) not in blocks and ((nx, ny) not in best or best[nx,ny] > d + 1): 31 | best[nx, ny] = d + 1 32 | todo.add((nx, ny)) 33 | if (CX-1,CY-1) in solved: 34 | return best[CX-1,CY-1] 35 | else: 36 | return None 37 | 38 | print(solve(N)) 39 | 40 | nmin = 0 41 | nmax = len(dat) + 1 42 | while nmin < nmax - 1: 43 | nmid = (nmin + nmax) // 2 44 | if solve(nmid) is None: 45 | nmax = nmid 46 | else: 47 | nmin = nmid 48 | print(dat[nmin]) 49 | -------------------------------------------------------------------------------- /2024/19.md: -------------------------------------------------------------------------------- 1 | # 19 – Speedy backtracking 2 | 3 | Well that sure was a quick one for this late in the event. Done in under 4 minutes, and still didn't hit the leaderboard. 4 | 5 | For part 1, I made use of our good friend, the regular expression. Like, for the worked example, where our available list of patterns is `r, wr, b, g, bwu, rb, gb, br`, we build the regular expression `^(r|wr|b|g|bwu|rb|gb|br)*$`, which will match any of the designs that can be built form those patterns. Then just run that against them all, and count the matches. 6 | 7 | For part 2, what we would like is to be able to take that same regex, and ask "how many ways can this match?"... but I don't believe that's something that the Python `re` library is equipped to answer. So, instead, we build our own basic backtracker... we check which of the patterns can be used at the very beginning of the design, remove them from the string and recurse, counting up how many possibilities make it to the end cleanly. 8 | 9 | Now, as this goes it will likely end up at the same position in the string multiple times... like, for the worked example, all of `g`, `b` and `gb` are acceptable, so when a design starts with `gb...` then the backtracking solver will be trying to find solutions from the 3rd position in the string, multiple times, and it would get the same result each time. And this continues, with the number of potential duplicate calls growing exponentially as the string grows. So we slap a memoisation wrapper on there and hope that's good enough. Which, it turns out, it is. 10 | 11 | [138/137] 12 | -------------------------------------------------------------------------------- /2024/19.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from aocimports import * 3 | 4 | dat = "brbwuur, bwuw, [...]" 5 | dat2 = """buwugbgrgururgwrgrrugbwgrwurgbubrggruwugwgrwguuurwu 6 | bwbrurbwgurggbbwbr[...]""" 7 | 8 | #dat = "r, wr, b, g, bwu, rb, gb, br" 9 | #dat2 = "brwrr\nbggr\ngbbr\nrrbgbr\nubwu\nbwurrg\nbrgr\nbbrgwb" 10 | 11 | dat = dat.split(", ") 12 | dat2 = dat2.split("\n") 13 | 14 | a = re.compile("^(%s)*$" % "|".join(dat)) 15 | n = 0 16 | for i in dat2: 17 | if a.match(i): 18 | n += 1 19 | print(n) 20 | 21 | @cache 22 | def count(s): 23 | if not s: 24 | return 1 25 | n = 0 26 | for i in dat: 27 | if s.startswith(i): 28 | n += count(s[len(i):]) 29 | return n 30 | 31 | n = 0 32 | for i in dat2: 33 | n += count(i) 34 | print(n) 35 | -------------------------------------------------------------------------------- /2024/22.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from aocimports import * 3 | 4 | dat = [ 5 | 15996872, 6 | 620419, 7 | [...] 8 | ] 9 | 10 | #dat = [1,10,100,2024] 11 | 12 | def step(n): 13 | #n = prune(mix(n, n*64)) 14 | #n = prune(mix(n, n//32)) 15 | #n = prune(mix(n, n*2048)) 16 | n = mix(n, prune(n*64)) 17 | n = mix(n, n//32) 18 | n = mix(n, prune(n*2048)) 19 | return n 20 | def mix(a, b): 21 | return a^b 22 | def prune(n): 23 | return n % 16777216 24 | 25 | n = 0 26 | for i in dat: 27 | for j in range(2000): 28 | i = step(i) 29 | n += i 30 | print(n) 31 | 32 | def analyse(seed): 33 | res = {} 34 | last = seed % 10 35 | code = [] 36 | for i in range(4): 37 | seed = step(seed) 38 | dig = seed % 10 39 | code.append(dig - last) 40 | last = dig 41 | for i in range(1997): 42 | ctup = tuple(code) 43 | if ctup not in res: 44 | res[ctup] = last 45 | seed = step(seed) 46 | dig = seed % 10 47 | code.pop(0) 48 | code.append(dig - last) 49 | last = dig 50 | return res 51 | 52 | #dat = [1, 2, 3, 2024] 53 | 54 | res = defaultdict(int) 55 | for i in dat: 56 | for k, v in analyse(i).items(): 57 | res[k] += v 58 | print(max(res.values())) 59 | -------------------------------------------------------------------------------- /2024/23.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from aocimports import * 3 | 4 | dat = [ 5 | ("qs","sa"), 6 | ("sj","su"), 7 | [...] 8 | ] 9 | 10 | #dat = [("kh","tc"),("qp","kh"),("de","cg"),("ka","co"),("yn","aq"),("qp","ub"),("cg","tb"),("vc","aq"),("tb","ka"),("wh","tc"),("yn","cg"),("kh","ub"),("ta","co"),("de","co"),("tc","td"),("tb","wq"),("wh","td"),("ta","ka"),("td","qp"),("aq","cg"),("wq","ub"),("ub","vc"),("de","ta"),("wq","aq"),("wq","vc"),("wh","yn"),("ka","de"),("kh","ta"),("co","tc"),("wh","qp"),("tb","vc"),("td","yn")] 11 | 12 | conn = {(i,j) for i,j in dat} | {(j,i) for i,j in dat} 13 | neighbours = defaultdict(set) 14 | for i, j in dat: 15 | neighbours[i].add(j) 16 | neighbours[j].add(i) 17 | comps = sorted(neighbours.keys()) 18 | 19 | n = 0 20 | for i in comps: 21 | for j in neighbours[i]: 22 | if j > i: 23 | for k in neighbours[i] & neighbours[j]: 24 | if k > j: 25 | if i.startswith("t") or j.startswith("t") or k.startswith("t"): 26 | n += 1 27 | print(n) 28 | 29 | def cliques(incl, ix): 30 | if ix >= len(comps): 31 | yield incl 32 | return 33 | if all((i, comps[ix]) in conn for i in incl): 34 | yield from cliques(incl | {comps[ix]}, ix+1) 35 | yield from cliques(incl, ix+1) 36 | 37 | cl = max(cliques(set(), 0), key=len) 38 | print(",".join(sorted(cl))) 39 | -------------------------------------------------------------------------------- /2024/24_fulladder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/e3fe0fbd8a26e4e0941ed72a032202416f1cc20c/2024/24_fulladder.png -------------------------------------------------------------------------------- /2024/24_halfadder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/e3fe0fbd8a26e4e0941ed72a032202416f1cc20c/2024/24_halfadder.png -------------------------------------------------------------------------------- /2024/25.md: -------------------------------------------------------------------------------- 1 | # 25 – It can be opened with a Virtual Five-Pin Tumbler Lock 2 | 3 | As always, a nice and easy simple puzzle for Christmas day. For my implementation, [at first](25.py) I did it all exactly as specified, I separated the inputs into locks and keys, I counted up the `#` marks in each column to get its numeric code, and then I looped through each pair of lock and key to see if the codes matched up. 4 | 5 | But I realised pretty quickly while doing it, that a lot of this was superfluous... all we really care about is if the bitmap for the lock and the key have any overlap, and that's pretty easy to test directly, without having to do all the intermediate steps. I ignored this and just did it as specified out of habit, because usually some part of it would turn out to be required in part 2. Of course, the day-25 puzzle doesn't _have_ a part 2, and I knew this, but it didn't stop me. 6 | 7 | Still, the more direct solution seemed straightforward enough that I [implemented it too](25a.py) after-the-fact, just to see if it really was that simple. I just convert each bitmap into a set, and check them pairwise to see if they intersect. I don't even bother separating the locks from the keys, since by how they're designed, all the locks will intersect with each other, as will all the keys. Ultimately the whole thing is solved in only a few short lines. 8 | 9 | Still, managed to scrape together a final handful of points from this one, and ended the whole event in position 84 on the leaderboard. Which is lower than I've been on the past couple of years, but I don't think the leaderboards are all that comparable year-over-year, especially with how many leaderboard positions on the early puzzles this year are filled with people just passing it off to genai. 10 | 11 | [124/96] 12 | -------------------------------------------------------------------------------- /2024/25.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from aocimports import * 3 | 4 | dat = """..... 5 | ..... 6 | [...]""" 7 | 8 | #dat = "#####\n.####\n.####\n.####\n.#.#.\n.#...\n.....\n\n#####\n##.##\n.#.##\n...##\n...#.\n...#.\n.....\n\n.....\n#....\n#....\n#...#\n#.#.#\n#.###\n#####\n\n.....\n.....\n#.#..\n###..\n###.#\n###.#\n#####\n\n.....\n.....\n.....\n#....\n#.#..\n#.#.#\n#####" 9 | 10 | dat = [i.split("\n") for i in dat.split("\n\n")] 11 | 12 | N = len(dat) 13 | CX = 5 14 | CY = 7 15 | 16 | locks = [] 17 | keys = [] 18 | for i in dat: 19 | if all(c == "#" for c in i[0]): 20 | target = locks 21 | elif all(c == "#" for c in i[-1]): 22 | target = keys 23 | else: 24 | 1/0 25 | newentry = [0] * CX 26 | for y, row in enumerate(i): 27 | for x, c in enumerate(row): 28 | if c == "#": 29 | newentry[x] += 1 30 | target.append(newentry) 31 | 32 | n = 0 33 | for l in locks: 34 | for k in keys: 35 | if all(i+j <= 7 for i,j in zip(l, k)): 36 | n += 1 37 | print(n) 38 | -------------------------------------------------------------------------------- /2024/25a.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from aocimports import * 3 | 4 | dat = """..... 5 | ..... 6 | [...]""" 7 | 8 | #dat = "#####\n.####\n.####\n.####\n.#.#.\n.#...\n.....\n\n#####\n##.##\n.#.##\n...##\n...#.\n...#.\n.....\n\n.....\n#....\n#....\n#...#\n#.#.#\n#.###\n#####\n\n.....\n.....\n#.#..\n###..\n###.#\n###.#\n#####\n\n.....\n.....\n.....\n#....\n#.#..\n#.#.#\n#####" 9 | 10 | dat = [i.split("\n") for i in dat.split("\n\n")] 11 | 12 | dat = [{(x,y) for y, row in enumerate(i) for x, c in enumerate(row) if c == "#"} for i in dat] 13 | n = 0 14 | for a, b in combinations(dat, 2): 15 | if not a&b: 16 | n += 1 17 | print(n) 18 | -------------------------------------------------------------------------------- /2024/README.md: -------------------------------------------------------------------------------- 1 | # Advent of Code 2024 2 | 3 | My solutions for the [Advent of Code 2024](https://adventofcode.com/2024) 4 | 5 | Just race solutions again, this year. Don't expect the code here to be any good, expect it to be very quickly thrown together and technically functional. 6 | -------------------------------------------------------------------------------- /2024/aocimports.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | sys.path.insert(1, os.path.join(os.path.dirname(__file__), "..", "utils")) 3 | from collections import defaultdict, Counter 4 | from fractions import Fraction 5 | from copy import deepcopy 6 | from pprint import pprint 7 | import re 8 | from math import factorial as fact, gcd, lcm, isqrt, perm as npr, comb as ncr, prod 9 | from math import sqrt, sin, cos, tan, atan, pi, ceil, floor 10 | from itertools import count, cycle, repeat, chain, groupby, product, permutations, combinations 11 | from functools import cache, cmp_to_key, partial, reduce 12 | 13 | from myutils import crt, gcdext, primes, factor 14 | from matrix import Matrix 15 | from vector import Vector 16 | from modular import Modular 17 | from prioqueue import PrioQueue 18 | from dijkstra import dijkstra, dijkstra_grid 19 | -------------------------------------------------------------------------------- /Direction.hs: -------------------------------------------------------------------------------- 1 | module Direction (Direction(..), directions, reverseDirection, flipDirX, flipDirY, rotDirLeft, rotDirRight, step) where 2 | 3 | data Direction = LeftDir | RightDir | UpDir | DownDir deriving (Eq, Enum, Ord, Show, Read) 4 | 5 | directions = [UpDir, LeftDir, DownDir, RightDir] 6 | 7 | reverseDirection UpDir = DownDir 8 | reverseDirection DownDir = UpDir 9 | reverseDirection LeftDir = RightDir 10 | reverseDirection RightDir = LeftDir 11 | 12 | flipDirX UpDir = UpDir 13 | flipDirX DownDir = DownDir 14 | flipDirX LeftDir = RightDir 15 | flipDirX RightDir = LeftDir 16 | 17 | flipDirY UpDir = DownDir 18 | flipDirY DownDir = UpDir 19 | flipDirY LeftDir = LeftDir 20 | flipDirY RightDir = RightDir 21 | 22 | rotDirLeft UpDir = LeftDir 23 | rotDirLeft LeftDir = DownDir 24 | rotDirLeft DownDir = RightDir 25 | rotDirLeft RightDir = UpDir 26 | 27 | rotDirRight UpDir = RightDir 28 | rotDirRight LeftDir = UpDir 29 | rotDirRight DownDir = LeftDir 30 | rotDirRight RightDir = DownDir 31 | 32 | step :: Direction -> (Integer,Integer) -> (Integer,Integer) 33 | step LeftDir (x, y) = (x-1, y) 34 | step RightDir (x, y) = (x+1, y) 35 | step UpDir (x, y) = (x, y-1) 36 | step DownDir (x, y) = (x, y+1) 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Advent of Code 2 | 3 | A repository for my solutions to [Advent of Code](https://adventofcode.com/). See the subdirectories for: 4 | * [2019](2019) 5 | * [2020](2020) 6 | * [2021](2021) 7 | * [2022](2022) 8 | * [2023](2023) 9 | * [2024](2024) 10 | -------------------------------------------------------------------------------- /utils/dijkstra.py: -------------------------------------------------------------------------------- 1 | __all__ = ["dijkstra", "dijkstra_grid"] 2 | 3 | from prioqueue import PrioQueue 4 | 5 | def dijkstra(start_pos, neighbours, target=None, heuristic=None): 6 | distmap = {} 7 | todo = PrioQueue() 8 | todo[start_pos] = 0, (0, None) 9 | if target is not None and not isinstance(target, set): 10 | target = {target} 11 | while todo and not (target and target <= distmap.keys()): 12 | node, _, (dist, prev) = todo.pop() 13 | distmap[node] = dist, prev 14 | for n, ndist in neighbours(node): 15 | if n in distmap: 16 | continue 17 | newscore = dist + ndist 18 | if heuristic: 19 | newscore += heuristic(n) 20 | if n not in todo or todo[n][0] > newscore: 21 | todo[n] = newscore, (dist + ndist, {node}) 22 | elif todo[n][0] == newscore: 23 | todo[n][1][1].add(node) 24 | return distmap 25 | 26 | def dijkstra_grid(start_pos, width, height, available, target=None, diags=False, heuristic=None): 27 | if diags: 28 | steps = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)] 29 | else: 30 | steps = [(-1, 0), (0, -1), (0, 1), (1, 0)] 31 | def neighbours(pos): 32 | x, y = pos 33 | for dx, dy in steps: 34 | if 0 <= x + dx < width and 0 <= y + dy < height and available(x + dx, y + dy): 35 | yield (x + dx, y + dy), 1 36 | return dijkstra(start_pos, neighbours, target, heuristic) 37 | --------------------------------------------------------------------------------