commit ed88e9b868a0e5ed795e43eb30dc8f5714c80f02 Author: Interitio Date: Thu Dec 5 20:07:44 2024 +1000 Initial commit with days 1 to 4. diff --git a/day1/solver.py b/day1/solver.py new file mode 100644 index 0000000..f40b94e --- /dev/null +++ b/day1/solver.py @@ -0,0 +1,32 @@ +from collections import defaultdict + +def part1(): + first, second = read_ids() + distance = sum(abs(id1 - id2) for id1, id2 in zip(first, second)) + print(f"Total distance is: {distance}") + +def part2(): + first, second = read_ids() + + # Make freqmap for second list + freq = defaultdict(int) + for id in second: + freq[id] += 1 + + # Sum first list weighted by freqmap + similarity = sum(freq[id] * id for id in first) + print(f"Similarity is {similarity}") + + + +def read_ids(): + idlist = [] + with open("id-list") as f: + for line in f: + if line: + idlist.append(map(int, line.split())) + return map(sorted, zip(*idlist)) + +if __name__ == '__main__': + part1() + part2() diff --git a/day2/solver.py b/day2/solver.py new file mode 100644 index 0000000..65ea892 --- /dev/null +++ b/day2/solver.py @@ -0,0 +1,69 @@ +from typing import Callable, Iterator +import sys + + +def check_report_basic(levels: tuple[int, ...]) -> bool: + safe = True + differences = [next_level - level for level, next_level in zip(levels, levels[1:])] + safe = safe and ( + all(-3 <= diff <= -1 for diff in differences) + or all(1 <= diff <= 3 for diff in differences) + ) + return safe + +def check_report_damp(levels: tuple[int, ...]) -> bool: + # Obvious solution is using check_report_basic iteratively.. + # Is there a better solution? + safe = check_report_basic(levels) + safe = safe or any(check_report_basic(levels[:i] + levels[i+1:]) for i in range(len(levels))) + return safe + +def tester(data: dict[tuple[int, ...], bool], checker: Callable[[tuple[int, ...]], bool]): + for report, expected in data.items(): + assert (checker(report) is expected), f"Check failed: {report=} should be {expected=}" + +def tests(): + print("Testing basic check") + data = { + (7, 6, 4, 2, 1): True, + (1, 2, 7, 8, 9): False, + (9, 7, 6, 2, 1): False, + (1, 3, 2, 4, 5): False, + (8, 6, 4, 4, 1): False, + (1, 3, 6, 7, 9): True, + } + tester(data, check_report_basic) + print("Basic check passed.") + + print("Testing dampened check.") + data = { + (7, 6, 4, 2, 1): True, + (1, 2, 7, 8, 9): False, + (9, 7, 6, 2, 1): False, + (1, 3, 2, 4, 5): True, + (8, 6, 4, 4, 1): True, + (1, 3, 6, 7, 9): True, + } + tester(data, check_report_damp) + print("Damp check passed.") + +def main(): + tests() + filename = sys.argv[1] + reports = load_data(filename) + total = sum(map(check_report_basic, reports)) + print(f"Total safe reports without damping: {total}") + + reports = load_data(filename) + total = sum(map(check_report_damp, reports)) + print(f"Total safe reports with damping: {total}") + + +def load_data(filename) -> Iterator[tuple[int, ...]]: + with open(filename) as f: + for line in f: + if line.strip(): + yield tuple(map(int, line.split())) + +if __name__ == '__main__': + main() diff --git a/day3/solver.py b/day3/solver.py new file mode 100644 index 0000000..7066de0 --- /dev/null +++ b/day3/solver.py @@ -0,0 +1,67 @@ +from typing import Iterator +import sys +import re + +mul_re = re.compile( + r"mul\((?P\d+),(?P\d+)\)" +) + +mul_re_cond = re.compile( + r"mul\((?P\d+),(?P\d+)\)|(?Pdo\(\))|(?Pdon't\(\))" +) + +def process_basic(data) -> int: + total = 0 + for match in re.finditer(mul_re, data): + n1 = int(match.group('n1')) + n2 = int(match.group('n2')) + total += n1 * n2 + return total + +def process_cond(data): + # Could also split into segments between do and the next don't + # And process each block between them + # But this is simpler + total = 0 + enabled = True + for match in re.finditer(mul_re_cond, data): + if match.group('do'): + enabled = True + elif match.group('dont'): + enabled = False + elif enabled: + n1 = int(match.group('n1')) + n2 = int(match.group('n2')) + total += n1 * n2 + return total + + +def load_data(filename) -> str: + with open(filename) as f: + return f.read() + +def test(): + print("Beginning Test 1") + data = r"xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))" + assert process_basic(data) == 161 + print("Test 1 passed") + + print("Beginning Test 2") + data = r"xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))" + assert process_cond(data) == 48 + print("Test 2 passed") + +def main(): + test() + data = load_data(sys.argv[1]) + total = process_basic(data) + print(f"Total from data without conditionals: {total}") + total = process_cond(data) + print(f"Total from data with conditionals: {total}") + + + + + +if __name__ == '__main__': + main() diff --git a/day4/solver.py b/day4/solver.py new file mode 100644 index 0000000..84029e5 --- /dev/null +++ b/day4/solver.py @@ -0,0 +1,131 @@ +import itertools +from typing import Iterator +import sys +import re + +test_data = r""" +MMMSXXMASM +MSAMXMSMSA +AMXSXMAAMM +MSAMASMSMX +XMASAMXAMM +XXAMMXXAMA +SMSMSASXSS +SAXAMASAAA +MAMMMXMMMM +MXMXAXMASX +""".strip() + +XMAS_RULES = list(zip( + *[( + (0, i), (0, -i), + (i, 0), (-i, 0), + (i, i), (-i, -i), + (i, -i), (-i, i) + ) for i in range(4)] +)) +MAS_RULES = [ + ( (-1, -1), (0, 0), (1, 1) ), + ( (-1, 1), (0, 0), (1, -1) ) +] + + +DEBUG = True + +def print_debug_matrix(rows, cols, letter_map: dict[tuple[int, int], str]): + for i in range(rows): + for j in range(cols): + print(letter_map.get((i, j), '.'), end='') + print('\n') + +def test_XMAS(): + print("Beginning test matrix search") + test_matrix = list(filter(bool,[line.strip() for line in test_data.splitlines()])) + assert letter_matrix_search_XMAS(test_matrix) == 18 + print("Test matrix search passed") + +def test_MAS(): + print("Beginning MAS matrix search") + test_matrix = list(filter(bool,[line.strip() for line in test_data.splitlines()])) + assert letter_matrix_search_MAS(test_matrix) == 9 + print("MAS matrix search passed") + +def tests(): + test_XMAS() + test_MAS() + + +def matrix_neighbour_rule(matrix: list[str], rowi, colj, rules: list[tuple[tuple[int, int], ...]]): + """ + Get the neighbours of the given point (rowi, colj) by the given rules. + E.g. if rules = [((-1, 0), (0, 0), (0, 1))], get a forwards diagonal. + Does not return a ruleset if it would go out of bounds. + """ + star = [] + rows, cols = len(matrix), len(matrix[0]) + for ruleset in rules: + this_line = [] + for rowstep, colstep in ruleset: + rowii = rowi + rowstep + colji = colj + colstep + + if not (0 <= rowii < rows and 0 <= colji < cols): + break + + this_line.append(((rowii, colji), matrix[rowii][colji])) + else: + star.append(this_line) + return star + + +def letter_matrix_search_XMAS(matrix: list[str], DEBUG=DEBUG): + total = 0 + debug_map = {} + + for rowi, row in enumerate(matrix): + for colj, letter in enumerate(row): + if letter == 'X': + star = matrix_neighbour_rule(matrix, rowi, colj, XMAS_RULES) + for line in star: + if ''.join(letter for _, letter in line) == 'XMAS': + total += 1 + debug_map |= dict(line) + + if DEBUG: + print_debug_matrix(len(matrix), len(matrix[0]), debug_map) + + return total + +def letter_matrix_search_MAS(matrix: list[str], DEBUG=DEBUG): + total = 0 + debug_map = {} + + for rowi, row in enumerate(matrix): + for colj, letter in enumerate(row): + if letter == 'A': + star = matrix_neighbour_rule(matrix, rowi, colj, MAS_RULES) + valid = star and all(''.join(list(zip(*line))[1]) in ('MAS', 'SAM') for line in star) + if valid: + total += 1 + for line in star: + debug_map |= dict(line) + + if DEBUG: + print_debug_matrix(len(matrix), len(matrix[0]), debug_map) + return total + +def load_data(filename) -> list[str]: + with open(filename) as f: + return list(filter(bool, (line.strip() for line in f.readlines()))) + +def main(): + tests() + data = load_data(sys.argv[1]) + total = letter_matrix_search_XMAS(data, DEBUG=False) + print(f"Total XMAS found: {total}") + total = letter_matrix_search_MAS(data, DEBUG=False) + print(f"Total MAS found: {total}") + + +if __name__ == '__main__': + main()