├── 2019 ├── 08.md ├── 25.png ├── 05.hs ├── 09.hs ├── 04.md ├── 06.md ├── 01.md ├── README.md ├── 19.md ├── 01.hs ├── 02.hs ├── 20.md ├── 03.md ├── 21.md ├── Makefile ├── 17.md ├── 05.md ├── 21.hs ├── 04.hs └── 09.md ├── 2020 ├── 03.md ├── 02.md ├── 01.md ├── 11.md ├── 18.sh ├── 12.md ├── 04.md ├── 05.hs ├── 09.md ├── Makefile ├── 24.md ├── 06.hs ├── 06.md ├── 01.hs ├── 08.md ├── 18.md ├── 16.md ├── 25.md ├── 03.hs ├── 10.hs ├── 17.md ├── 05.md ├── README.md ├── 13.hs └── 22.md ├── 2021 ├── 06_roots.png ├── 03.md ├── 01.md ├── 25.md ├── 22a.py ├── 10.py ├── 17.py ├── 02.md ├── 13.py ├── 12.md ├── 07.md ├── 09.py ├── 01.hs ├── 10.md ├── 07.hs ├── Makefile ├── 11.md ├── 25.py ├── 15.py ├── 08.md ├── 05.md ├── 21.py ├── 06.hs ├── 24.py ├── 09.md ├── README.md ├── 16.py ├── 02.hs ├── 13.md ├── 15.hs ├── 18.py └── 22b.py ├── 2022 ├── 14a.png ├── 14b.png ├── 22.png ├── 01.py ├── 06.py ├── 04.py ├── 02.py ├── 05.py ├── 03.md ├── 25.py ├── 03.py ├── 25.md ├── 20.py ├── Makefile ├── 01.hs ├── 04.md ├── 07.py ├── 09a.py ├── 06.hs ├── 11.py ├── 13.py ├── 18.py ├── 10.md ├── 06.md ├── 12.py ├── 18.md ├── 09b.py ├── 05.md ├── 03.hs ├── 08.hs ├── 02.md ├── README.md ├── 04.hs ├── 01.md ├── 09.md ├── 15.py ├── 08.md ├── 16b.py ├── 12.md ├── 07.md ├── 23.md ├── 22a.py ├── 23.py ├── 14.md └── 02.hs ├── 2023 ├── 21.png ├── 06.bas ├── 06b.py ├── 06.py ├── README.md ├── 12a.py ├── 15.py ├── 09.md ├── 04.py ├── 08.py ├── 15.md ├── 09.py ├── 01.py ├── 12b.py ├── 13.md ├── 11.py ├── 20.reordered.txt ├── 11.md ├── 07.py ├── 02.py ├── 16.md ├── 22.py ├── 05.py ├── 10.py ├── 04.md ├── 03.py ├── 06.md ├── 01.md ├── 14.py └── 20.py ├── 2024 ├── 14_0000.png ├── 14_0018.png ├── 14_0076.png ├── 14_7492.png ├── 24_fulladder.png ├── 24_halfadder.png ├── README.md ├── 01.py ├── 02.py ├── 25a.py ├── 03.py ├── 19.py ├── aocimports.py ├── 25.py ├── 04.py ├── 05.py ├── 07.py ├── 22.py ├── 06.py ├── 18.md ├── 13.py ├── 11.py ├── 18.py ├── 23.py ├── 04.md ├── 01.md ├── 07.md ├── 19.md ├── 08.md ├── 08.py └── 10.py ├── 2025 ├── 09.png ├── 09bad.png ├── README.md ├── 05a.py ├── 12.py ├── aocimports.py ├── 02.py ├── 01.py ├── 11b.py ├── 03.py ├── 04.py ├── 05.py ├── 06.py ├── 11a.py ├── 04.md ├── 11.py ├── 08.py ├── 07.py └── 09.py ├── .gitignore ├── README.md ├── Direction.hs └── utils └── dijkstra.py /2019/08.md: -------------------------------------------------------------------------------- 1 | # 8 – Pixel bashing 2 | What a weird image format. 3 | -------------------------------------------------------------------------------- /2019/25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/HEAD/2019/25.png -------------------------------------------------------------------------------- /2022/14a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/HEAD/2022/14a.png -------------------------------------------------------------------------------- /2022/14b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/HEAD/2022/14b.png -------------------------------------------------------------------------------- /2022/22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/HEAD/2022/22.png -------------------------------------------------------------------------------- /2023/21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/HEAD/2023/21.png -------------------------------------------------------------------------------- /2025/09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/HEAD/2025/09.png -------------------------------------------------------------------------------- /2025/09bad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/HEAD/2025/09bad.png -------------------------------------------------------------------------------- /2021/06_roots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/HEAD/2021/06_roots.png -------------------------------------------------------------------------------- /2024/14_0000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/HEAD/2024/14_0000.png -------------------------------------------------------------------------------- /2024/14_0018.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/HEAD/2024/14_0018.png -------------------------------------------------------------------------------- /2024/14_0076.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/HEAD/2024/14_0076.png -------------------------------------------------------------------------------- /2024/14_7492.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/HEAD/2024/14_7492.png -------------------------------------------------------------------------------- /2024/24_fulladder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/HEAD/2024/24_fulladder.png -------------------------------------------------------------------------------- /2024/24_halfadder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrphlip/aoc/HEAD/2024/24_halfadder.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | * [2025](2025) 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /2025/README.md: -------------------------------------------------------------------------------- 1 | # Advent of Code 2025 2 | 3 | My solutions for the [Advent of Code 2025](https://adventofcode.com/2025) 4 | 5 | The global leaderboard may be a thing of the past, but I still enjoy the feel of the race, so these are still going to be speed-written solutions (with all of that implies for, say, legibility). 6 | 7 | However, that means that this year I'll be recording my times for each puzzle, rather than my leaderboard rank. 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /2025/05a.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from aocimports import * 3 | 4 | dat = [ 5 | (169486974574545,170251643963353), 6 | (350457710225863,350888576149828), 7 | [...] 8 | ] 9 | dat2 = [ 10 | 166774327825644, 11 | 91047458369966, 12 | [...] 13 | ] 14 | 15 | #dat = [(3,5),(10,14),(16,20),(12,18)]; dat2 = [1,5,8,11,17,32] 16 | 17 | dat = [(i,j+1) for i,j in dat] 18 | 19 | ranges = Ranges(dat) 20 | print(len([i for i in dat2 if i in ranges])) 21 | print(len(ranges)) 22 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /2020/18.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ( 3 | cat < 18.hs 27 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /2025/12.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from aocimports import * 3 | 4 | dat = """\ 5 | ..# 6 | .## 7 | ##. 8 | 9 | [...]""" 10 | 11 | dat2 = [ 12 | ((39,43),(31,33,40,22,27,28)), 13 | ((40,37),(39,23,40,36,48,42)), 14 | [...] 15 | ] 16 | 17 | #dat = "###\n##.\n##.\n\n###\n##.\n.##\n\n.##\n###\n##.\n\n##.\n###\n##.\n\n###\n#..\n###\n\n###\n.#.\n###"; dat2 = [((4,4),(0,0,0,0,2,0)),((12,5),(1,0,1,0,2,2)),((12,5),(1,0,1,0,3,2)),] 18 | 19 | dat = [[[c == "#" for c in row] for row in i.split("\n")] for i in dat.split("\n\n")] 20 | piecesize = [sum(c for row in piece for c in row) for piece in dat] 21 | 22 | def solve(row): 23 | (w, h), counts = row 24 | 25 | totsize = sum(piecesize[ix] * count for ix, count in enumerate(counts)) 26 | 27 | return totsize <= w*h 28 | 29 | print(sum(solve(row) for row in dat2)) 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /2025/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, primes_to, 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 | from ranges import Ranges 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /2025/02.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from aocimports import * 3 | 4 | dat = """\ 5 | 52500467-52574194,[...]""" 6 | 7 | #dat = "11-22,95-115,998-1012,1188511880-1188511890,222220-222224,1698522-1698528,446443-446449,38593856-38593862,565653-565659,824824821-824824827,2121212118-2121212124" 8 | 9 | dat = [(int(j),int(k)+1) for i in dat.split(",") for j,k in [i.split('-')]] 10 | 11 | def iterbads(a, b, k=2): 12 | i = str(a) 13 | if len(i) % k == 0: 14 | n = int(i[:len(i)//k]) 15 | if int(str(n)*k) < a: 16 | n += 1 17 | else: 18 | n = 10 ** (len(i) // k) 19 | while int(str(n)*k) < b: 20 | yield int(str(n)*k) 21 | n += 1 22 | 23 | def iterbadsall(a, b): 24 | return {i for l in primes_to(len(str(b))+1) for i in iterbads(a, b, l)} 25 | 26 | print(sum(i for a, b in dat for i in iterbads(a, b))) 27 | print(sum(i for a, b in dat for i in iterbadsall(a, b))) 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /2025/01.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from aocimports import * 3 | 4 | dat = """\ 5 | R32 6 | R2 7 | R4 8 | [...] 9 | """ 10 | 11 | #dat = "L68 L30 R48 L5 R60 L55 L1 L99 R14 L82" 12 | 13 | dat = [(i[0], int(i[1:])) for i in dat.split()] 14 | 15 | 16 | pos = 50 17 | count = 0 18 | for dir, step in dat: 19 | if dir == "L": 20 | pos -= step 21 | elif dir == "R": 22 | pos += step 23 | else: 24 | 1/0 25 | pos = pos % 100 26 | if pos == 0: 27 | count += 1 28 | print(count) 29 | 30 | 31 | pos = 50 32 | count = 0 33 | for dir, step in dat: 34 | prevpos = pos 35 | if dir == "L": 36 | pos -= step 37 | if pos < 0 and prevpos == 0: 38 | count -= 1 39 | while pos < 0: 40 | count += 1 41 | pos += 100 42 | if pos == 0: 43 | count += 1 44 | elif dir == "R": 45 | pos += step 46 | while pos >= 100: 47 | count += 1 48 | pos -= 100 49 | else: 50 | 1/0 51 | print(count) 52 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /2025/11b.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from aocimports import * 3 | 4 | dat = { 5 | "lvy":{"oyl","fyu","wde","kxs"}, 6 | "ozc":{"ziu","zyw","omn","kzo"}, 7 | [...] 8 | } 9 | 10 | #dat = {"aaa":{"you","hhh"},"you":{"bbb","ccc"},"bbb":{"ddd","eee"},"ccc":{"ddd","eee","fff"},"ddd":{"ggg"},"eee":{"out"},"fff":{"out"},"ggg":{"out"},"hhh":{"ccc","fff","iii"},"iii":{"out"},} 11 | #dat = {"svr":{"aaa","bbb"},"aaa":{"fft"},"fft":{"ccc"},"bbb":{"tty"},"tty":{"ccc"},"ccc":{"ddd","eee"},"ddd":{"hub"},"hub":{"fff"},"eee":{"dac"},"dac":{"fff"},"fff":{"ggg","hhh"},"ggg":{"out"},"hhh":{"out"},} 12 | 13 | nodes = set(dat.keys()) | {"out"} 14 | 15 | print ("digraph {") 16 | for k in nodes: 17 | col = {"you": "blue", "svr": "blue", "fft": "red", "dac": "red", "out": "green"}.get(k) 18 | if col: 19 | print(f"{k} [color={col}];") 20 | else: 21 | print(k) 22 | for k, vs in dat.items(): 23 | for v in vs: 24 | print(f"{k} -> {v};") 25 | print("}") 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /2025/03.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from aocimports import * 3 | 4 | dat = """\ 5 | 5336553644444345344544134246423443634474453456455433543434354444344554344336446734443434424442135474 6 | 2231552222211222122232222222153222143972321313222122221132199121111212232232222223322324232211141222 7 | [...] 8 | """ 9 | 10 | #dat = "987654321111111 811111111111119 234234234234278 818181911112111" 11 | 12 | dat = [[int(c) for c in row] for row in dat.split()] 13 | 14 | #n = 0 15 | #for row in dat: 16 | # digit1 = max(row[:-1]) 17 | # digit1ix = row.index(digit1) 18 | # digit2 = max(row[digit1ix+1:]) 19 | # val = digit1 * 10 + digit2 20 | # n += val 21 | #print(n) 22 | 23 | 24 | def maximise(row, n): 25 | digits = [] 26 | ix = 0 27 | while n > 0: 28 | dig = max(row[ix:len(row)-n+1]) 29 | digits.append(dig) 30 | ix = row.index(dig, ix) + 1 31 | n -= 1 32 | return int(''.join(map(str, digits))) 33 | print(sum(maximise(row, 2) for row in dat)) 34 | print(sum(maximise(row, 12) for row in dat)) 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /2025/04.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from aocimports import * 3 | 4 | dat = """\ 5 | @@@@@@.@@..@@.@.@@.@@...@@.@@@.@.@.@.@@.@@@@@@@@@@@@.@.@@.@.@...@.@@@@@@@@..@@..@@@..@@@.@@@@..@.....@@@.@@.@@@@@@.@@.@@.@.@@@@@@.@@.@@.@ 6 | ..@@.@@@@.@.@.@.@.@@..@@.@@@@@@.@@@..@@.@@.@.@.@.@.....@@..@.@@@@...@@@.@.@@.@..@.@@@@@..@..@@@@.@@@@.@@@@@@@@@@@@@@.@@..@.@.@..@.@@@@@@@ 7 | [...]""" 8 | 9 | #dat = "..@@.@@@@. @@@.@.@.@@ @@@@@.@.@@ @.@@@@..@. @@.@@@@.@@ .@@@@@@@.@ .@.@.@.@@@ @.@@@.@@@@ .@@@@@@@@. @.@.@@@.@." 10 | 11 | dat = [[{"@":True,".":False}[c] for c in row] for row in dat.split()] 12 | rolls = {(x, y) for y, row in enumerate(dat) for x, c in enumerate(row) if c} 13 | 14 | def removable(rolls): 15 | return [(x,y) for x, y in rolls if len({(x+dx,y+dy) for dx in [-1,0,1] for dy in [-1,0,1]} & rolls) <= 4] 16 | print(len(removable(rolls))) 17 | 18 | remain = rolls 19 | while True: 20 | remove = set(removable(remain)) 21 | if not remove: 22 | break 23 | remain = remain - remove 24 | print(len(rolls) - len(remain)) 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /2025/05.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from aocimports import * 3 | 4 | dat = [ 5 | (169486974574545,170251643963353), 6 | (350457710225863,350888576149828), 7 | [...] 8 | ] 9 | dat2 = [ 10 | 166774327825644, 11 | 91047458369966, 12 | [...] 13 | ] 14 | 15 | #dat = [(3,5),(10,14),(16,20),(12,18)]; dat2 = [1,5,8,11,17,32] 16 | 17 | dat = [(i,j+1) for i,j in dat] 18 | 19 | dat.sort() 20 | 21 | flattened = [] 22 | for i,j in dat: 23 | if not flattened or i >= flattened[-1][1]: 24 | flattened.append((i, j)) 25 | else: 26 | flattened[-1] = (flattened[-1][0], max(flattened[-1][1], j)) 27 | 28 | def findrange(x): 29 | a = 0 30 | b = len(flattened) 31 | while a < b: 32 | c = (a + b) // 2 33 | if flattened[c][0] <= x < flattened[c][1]: 34 | return flattened[c] 35 | elif x < flattened[c][0]: 36 | b = c 37 | else: 38 | a = c + 1 39 | return None 40 | 41 | count = 0 42 | for i in dat2: 43 | if findrange(i) is not None: 44 | count += 1 45 | print(count) 46 | 47 | print(sum(j-i for i,j in flattened)) 48 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /2025/06.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from aocimports import * 3 | 4 | dat = """\ 5 | 563 334 22 38 2624 [...]""" 6 | dat2 = """\ 7 | * + * + + * [...]""" 8 | 9 | #dat = "123 328 51 64 \n 45 64 387 23 \n 6 98 215 314"; dat2 = "* + * + " 10 | 11 | cols = {i for i, c in enumerate(dat2) if c != " "} 12 | ops = [c for i, c in enumerate(dat2) if c != " "] 13 | vals = [[int(i) for i in row.split()] for row in dat.split("\n")] 14 | assert all(len(row) == len(ops) for row in vals) 15 | 16 | res = [sum(row[ix] for row in vals) if op == "+" else prod(row[ix] for row in vals) for ix, op in enumerate(ops)] 17 | print(sum(res)) 18 | 19 | dat = dat.split("\n") 20 | curr = None 21 | vals = [] 22 | for ix in range(len(dat2)): 23 | if ix in cols: 24 | curr = [] 25 | vals.append(curr) 26 | val = ''.join(row[ix] for row in dat).strip() 27 | if val: 28 | curr.append(int(val)) 29 | if not curr: 30 | del vals[-1] 31 | assert len(vals) == len(ops) 32 | 33 | res = [sum(vals[ix]) if op == "+" else prod(vals[ix]) for ix, op in enumerate(ops)] 34 | print(sum(res)) 35 | 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /2025/11a.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from aocimports import * 3 | 4 | dat = { 5 | "lvy":{"oyl","fyu","wde","kxs"}, 6 | "ozc":{"ziu","zyw","omn","kzo"}, 7 | [...] 8 | } 9 | 10 | #dat = {"aaa":{"you","hhh"},"you":{"bbb","ccc"},"bbb":{"ddd","eee"},"ccc":{"ddd","eee","fff"},"ddd":{"ggg"},"eee":{"out"},"fff":{"out"},"ggg":{"out"},"hhh":{"ccc","fff","iii"},"iii":{"out"},} 11 | #dat = {"svr":{"aaa","bbb"},"aaa":{"fft"},"fft":{"ccc"},"bbb":{"tty"},"tty":{"ccc"},"ccc":{"ddd","eee"},"ddd":{"hub"},"hub":{"fff"},"eee":{"dac"},"dac":{"fff"},"fff":{"ggg","hhh"},"ggg":{"out"},"hhh":{"out"},} 12 | 13 | nodes = set(dat.keys()) | {"out"} 14 | 15 | revdat = {k: set() for k in nodes} 16 | for k, v in dat.items(): 17 | for x in v: 18 | revdat[x].add(k) 19 | 20 | @cache 21 | def docount(target, node): 22 | if node == target: 23 | return 1 24 | else: 25 | return sum(docount(target, i) for i in revdat[node]) 26 | print(docount("you", "out")) 27 | print( 28 | docount("svr", "fft") * docount("fft", "dac") * docount("dac", "out") + 29 | docount("svr", "dac") * docount("dac", "fft") * docount("fft", "out")) 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /2025/04.md: -------------------------------------------------------------------------------- 1 | # 4 – They see me rollin' 2 | 3 | Another day, another simple one. All my time in previous AoCs helped out here, with plenty of practice working on these stuff-in-a-grid puzzles. 4 | 5 | The primary lesson being: don't store it as a big grid, as an array of `bool`s... instead, store it as a `set` of locations. Makes it easier to, eg, iterate over the objects, and check for neighbours without having to bounds-check against the size of the grid. 6 | 7 | So, for part 1, we just go through each roll, and use a set intersection to quickly extract all of its neighbours, and count how many there are. For part 2, we just directly work as described, repeatedly removing all the ones that can be removed, until we do a pass that doesn't find any removable, then count how many were removed in total. Nice and easy. 8 | 9 | Only real small optimisation is that, instead of checking that the _neighbours_ of any given roll are _less than_ 4, I instead check the entire 3x3 range around it (including itself) is _less than or equal to_ 4. Same result, but slightly simpler to do. 10 | 11 | [04:09/06:02] 12 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /2025/11.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from aocimports import * 3 | 4 | dat = { 5 | "lvy":{"oyl","fyu","wde","kxs"}, 6 | "ozc":{"ziu","zyw","omn","kzo"}, 7 | [...] 8 | } 9 | 10 | #dat = {"aaa":{"you","hhh"},"you":{"bbb","ccc"},"bbb":{"ddd","eee"},"ccc":{"ddd","eee","fff"},"ddd":{"ggg"},"eee":{"out"},"fff":{"out"},"ggg":{"out"},"hhh":{"ccc","fff","iii"},"iii":{"out"},} 11 | #dat = {"svr":{"aaa","bbb"},"aaa":{"fft"},"fft":{"ccc"},"bbb":{"tty"},"tty":{"ccc"},"ccc":{"ddd","eee"},"ddd":{"hub"},"hub":{"fff"},"eee":{"dac"},"dac":{"fff"},"fff":{"ggg","hhh"},"ggg":{"out"},"hhh":{"out"},} 12 | 13 | nodes = set(dat.keys()) | {"out"} 14 | 15 | revdat = {k: set() for k in nodes} 16 | for k, v in dat.items(): 17 | for x in v: 18 | revdat[x].add(k) 19 | 20 | @cache 21 | def part1(node): 22 | if node == "you": 23 | return 1 24 | else: 25 | return sum(part1(i) for i in revdat[node]) 26 | print(part1("out")) 27 | 28 | @cache 29 | def part2(node, seenfft, seendac): 30 | if node == "svr": 31 | return 1 if seenfft and seendac else 0 32 | else: 33 | return sum(part2(i, seenfft or node == "fft", seendac or node == "dac") for i in revdat[node]) 34 | print(part2("out", False, False)) 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /2025/08.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from aocimports import * 3 | 4 | dat = [ 5 | (57869,43825,4618), 6 | (33342,30491,49401), 7 | [...] 8 | ] 9 | N=1000 10 | 11 | #dat = (162,817,812),(57,618,57),(906,360,560),(592,479,940),(352,342,300),(466,668,158),(542,29,236),(431,825,988),(739,650,466),(52,470,668),(216,146,977),(819,987,18),(117,168,530),(805,96,715),(346,949,466),(970,615,88),(941,993,340),(862,61,35),(984,92,344),(425,690,689); N=10 12 | 13 | dists = [ 14 | (i, j, (x2-x1)**2+(y2-y1)**2+(z2-z1)**2) 15 | for i, (x1, y1, z1) in enumerate(dat) 16 | for j, (x2, y2, z2) in enumerate(dat) 17 | if i < j 18 | ] 19 | dists.sort(key=lambda x: x[2]) 20 | 21 | connected = {i: {i} for i in range(len(dat))} 22 | joins = [] 23 | for i, j, dist in dists[:N]: 24 | if i in connected[j]: 25 | continue 26 | joins.append((i, j)) 27 | conn = connected[i] | connected[j] 28 | for x in conn: 29 | connected[x] = conn 30 | sizes = [len(v) for k,v in connected.items() if k == min(v)] 31 | sizes.sort() 32 | print(prod(sizes[-3:])) 33 | 34 | for i, j, dist in dists[N:]: 35 | if i in connected[j]: 36 | continue 37 | joins.append((i, j)) 38 | conn = connected[i] | connected[j] 39 | for x in conn: 40 | connected[x] = conn 41 | if len(joins) >= len(dat) - 1: 42 | break 43 | i, j = joins[-1] 44 | print(dat[i][0] * dat[j][0]) 45 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /2025/07.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from aocimports import * 3 | 4 | dat = """\ 5 | ......................................................................S...................................................................... 6 | ............................................................................................................................................. 7 | ......................................................................^...................................................................... 8 | [...]""" 9 | 10 | #dat = ".......S....... ............... .......^....... ............... ......^.^...... ............... .....^.^.^..... ............... ....^.^...^.... ............... ...^.^...^.^... ............... ..^...^.....^.. ............... .^.^.^.^.^...^. ..............." 11 | 12 | dat = dat.split() 13 | CY = len(dat) 14 | CX = len(dat[0]) 15 | 16 | grid = set() 17 | for y, row in enumerate(dat): 18 | for x, c in enumerate(row): 19 | if c == "S": 20 | SX, SY = x, y 21 | elif c == "^": 22 | grid.add((x, y)) 23 | 24 | xs = {SX: 1} 25 | count = 0 26 | for y in range(SY, CY): 27 | newxs = defaultdict(int) 28 | for x, v in xs.items(): 29 | if (x, y) in grid: 30 | newxs[x-1] += v 31 | newxs[x+1] += v 32 | count += 1 33 | else: 34 | newxs[x] += v 35 | xs = newxs 36 | print(count) 37 | print(sum(xs.values())) 38 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /2025/09.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from aocimports import * 3 | 4 | dat = [ 5 | (97978,50277), 6 | (97978,51488), 7 | [...] 8 | ] 9 | 10 | #dat = [(7,1),(11,1),(11,7),(9,7),(9,5),(2,5),(2,3),(7,3),]; dat = [*dat[1:], dat[0]] 11 | 12 | print(max( 13 | (abs(x2-x1)+1) * (abs(y2-y1)+1) 14 | for x1, y1 in dat for x2, y2 in dat)) 15 | 16 | for i in range(len(dat)): 17 | if i % 2 == 0: 18 | assert dat[i][0] == dat[(i+1) % len(dat)][0] 19 | else: 20 | assert dat[i][1] == dat[(i+1) % len(dat)][1] 21 | 22 | #uniq_x = {x for x,y in dat} 23 | #uniq_y = {y for x,y in dat} 24 | #assert not (uniq_x & {i+1 for i in uniq_x}) 25 | #assert not (uniq_y & {i+1 for i in uniq_y}) 26 | v_edges = [ 27 | (x1, min(y1, y2), max(y1, y2)) 28 | for i, (x1, y1) in enumerate(dat) 29 | for x2, y2 in [dat[(i+1)%len(dat)]] 30 | if i % 2 == 0 31 | ] 32 | h_edges = [ 33 | (min(x1, x2), max(x1, x2), y1) 34 | for i, (x1, y1) in enumerate(dat) 35 | for x2, y2 in [dat[(i+1)%len(dat)]] 36 | if i % 2 == 1 37 | ] 38 | def is_valid(x1, y1, x2, y2): 39 | if x1 > x2: 40 | x1, x2 = x2, x1 41 | if y1 > y2: 42 | y1, y2 = y2, y1 43 | if any( 44 | x1 < ex < x2 and ey2 > y1 and ey1 < y2 45 | for ex, ey1, ey2 in v_edges): 46 | return False 47 | if any( 48 | y1 < ey < y2 and ex2 > x1 and ex1 < x2 49 | for ex1, ex2, ey in h_edges): 50 | return False 51 | return True 52 | 53 | print(max( 54 | (abs(x2-x1)+1) * (abs(y2-y1)+1) 55 | for x1, y1 in dat for x2, y2 in dat 56 | if is_valid(x1, y1, x2, y2))) 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------