diff --git a/.gitignore b/.gitignore index f5f3199..554fbc2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .idea/ -web_secrets.py +lib puzzle_inputs +web_secrets.py **/part1_attempts.json **/part2_attempts.json diff --git a/2023day12.py b/2023day12.py new file mode 100644 index 0000000..f2777d8 --- /dev/null +++ b/2023day12.py @@ -0,0 +1,80 @@ +import functools + +from util import * +from util import Grid as g +import itertools as it +import re + +h = Helper(day=12, test_mode=True, test_input=""" +??? 1 +""") + +inp = h.get_input_list() + + +@functools.cache +def check_record_consistency(springs, groups): + springs = list(springs) + group_len = 0 + group_index = -1 + in_group = False + for c in springs + ["."]: + if c == '?': + return True + if in_group: + if c == "#": + group_len += 1 + if group_len > groups[group_index]: + return False + else: + if groups[group_index] != group_len: + return False + in_group = False + else: + if c == "#": + group_index += 1 + if group_index >= len(groups): + return False + in_group = True + group_len = 1 + + return group_index == len(groups) - 1 + + +def count_valid_combinations(springs, groups): + springs = list(springs) + if '?' not in springs: + return 1 + + question_index = springs.index('?') + + total_options = 0 + + for c in ".#": + springs[question_index] = c + if check_record_consistency(tuple(springs), groups): + total_options += count_valid_combinations(tuple(springs), groups) + + return total_options + + +records = [] + +for line in inp: + records.append(( + list("?".join(it.repeat(line.split(" ")[0], 5))), + list(it.chain.from_iterable(it.repeat(list(int(d) for d in line.split(" ")[1].split(",")), 5))) + )) + # print(records[-1]) + # print("".join(records[-1][0])) + # print(records[-1][1]) + + +options_sum = 0 +for record in records: + print("".join(record[0]), ",".join(str(n) for n in record[1])) + options = count_valid_combinations(tuple(record[0]), tuple(record[1])) + print(options) + options_sum += options + +h.submit(options_sum) diff --git a/day20.py b/day20.py new file mode 100644 index 0000000..b998c92 --- /dev/null +++ b/day20.py @@ -0,0 +1,166 @@ +import queue +from collections import defaultdict + +from util import * +from util import Grid as g +import itertools as it +import re + +h = Helper(test_mode=False, test_input=""" +broadcaster -> a +%a -> inv, con +&inv -> b +%b -> con +&con -> output +""") + + +# pulses_queue = queue.Queue() +# +# +# class SolutionFoundException(Exception): +# pass +# +# +# class Module: +# def __init__(self, name: str, link_strs: t.List[str]): +# self.name: str = name +# self.link_strs: t.List[str] = link_strs +# self.link_objects: t.List[Module] = [] +# +# def init_link_objects(self, all_modules): +# for li in self.link_strs: +# try: +# self.link_objects.append(all_modules[li]) +# except KeyError: +# pass +# +# def send_pulse(self, is_high_pulse: bool): +# global pulses_queue +# +# for li in self.link_objects: +# if li.name == "kj" and is_high_pulse: +# raise SolutionFoundException +# pulses_queue.put((li, is_high_pulse, self.name)) +# +# def activate(self, is_high_pulse: bool, activator_name: str): +# raise NotImplemented() +# +# +# class FlipFlop(Module): +# def __init__(self, name: str, link_strs: t.List[str]): +# super().__init__(name, link_strs) +# self.state = False +# +# def activate(self, is_high_pulse: bool, activator_name): +# if is_high_pulse: +# return +# +# self.state = not self.state +# self.send_pulse(self.state) +# +# +# class Conjunction(Module): +# def __init__(self, name: str, link_strs: t.List[str]): +# super().__init__(name, link_strs) +# self.input_states = {} +# +# def init_link_objects(self, all_modules): +# super().init_link_objects(all_modules) +# for mod in all_modules.values(): +# if self.name in mod.link_strs: +# self.input_states[mod.name] = False +# +# def activate(self, is_high_pulse: bool, activator_name: str): +# self.input_states[activator_name] = is_high_pulse +# self.send_pulse(not all(self.input_states.values())) +# +# +# class Broadcast(Module): +# def activate(self, is_high_pulse: bool, activator_name: str): +# self.send_pulse(is_high_pulse) +# +# +# class Button(Module): +# def activate(self, is_high_pulse: bool, activator_name: str): +# self.send_pulse(False) +# +# +# TYPE_CODES = { +# '%': FlipFlop, +# '&': Conjunction, +# 'b': Broadcast, +# } +# +# inp = h.get_input_list() +# +# modules: t.Dict[str, Module] = {} +# +# for line in inp: +# type_code = line[0] +# module_type = TYPE_CODES[type_code] +# name = line.split(' -> ')[0].strip('%&') +# links = line.split(' -> ')[1].split(', ') +# modules[name] = module_type(name, links) +# +# modules["button"] = (Button("button", ["broadcaster"])) +# +# for m in modules.values(): +# m.init_link_objects(modules) +# +# step = 0 +# last_high = 0 +# +# while True: +# states = [str(int(m.state)) for m in modules.values() if isinstance(m, FlipFlop)] +# # print("".join(states)) +# +# step += 1 +# modules["pd"].activate(False, "") +# +# while not pulses_queue.empty(): +# pulse = pulses_queue.get(block=False) +# # print(pulse, pulse[0].name) +# try: +# pulse[0].activate(pulse[1], pulse[2]) +# except SolutionFoundException: +# print(step, step - last_high) +# last_high = step +# break + +high_steps = defaultdict(lambda: 0) + +steps = [ + 4003, + 3989, + 3863, + 3943, +] + +periods = [ + 3910, + 3882, + 3630, + 3790, +] + +""" +kt: goes high on step 4003 and 4004, repeats for 2 steps with period 3910 steps +xv: goes high on step 3989 and 3990, repeats for 2 steps with period 3882 steps +rg: goes high on step 3863 and 3864, repeats for 2 steps with period 3630 steps +pd: goes high on step 3943 and 3944, repeats for 2 steps with period 3790 steps +""" + + +while True: + for i, p in enumerate(periods): + s = steps[i] + high_steps[s] += 1 + if high_steps[s] == 4: + h.submit(s) + + high_steps[s + 1] += 1 + if high_steps[s + 1] == 4: + h.submit(s + 1) + + steps[i] += p diff --git a/day20vis.py b/day20vis.py new file mode 100644 index 0000000..bba6199 --- /dev/null +++ b/day20vis.py @@ -0,0 +1,31 @@ +from pyvis.network import Network + +from util import Helper + +net = Network(directed=True) + +h = Helper(test_mode=False) + +inp = h.get_input_list() + +modules = {} + +for line in inp: + type_code = line[0] + name = line.split(' -> ')[0].strip('%&') + links = line.split(' -> ')[1].split(', ') + modules[name] = (type_code, name, links) + if type_code == '&': + net.add_node(name, name, shape="box") + else: + net.add_node(name, name) + +net.add_node("rx", "rx") +net.add_node("button", "button") +modules["button"] = ("but", "button", ["broadcaster"]) + +for m in modules.values(): + for n in m[2]: + net.add_edge(m[1], n) + +net.show('mygraph.html', notebook=False) diff --git a/solutions/2023_day06/part1.py b/solutions/2023_day06/part1.py new file mode 100644 index 0000000..19918d1 --- /dev/null +++ b/solutions/2023_day06/part1.py @@ -0,0 +1,38 @@ +import operator + +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input=""" +Time: 7 15 30 +Distance: 9 40 200 +""") + +inp = h.get_input_list() + + +def get_race_distance(total_time, hold_time): + return (total_time - hold_time) * hold_time + + +def count_winning_times(max_time, dist_to_beat): + c = 0 + for ht in range(max_time): + if get_race_distance(max_time, ht) > dist_to_beat: + c += 1 + return c + + +times = [int(n) for n in inp[0].split(" ")[1:] if n] +distances = [int(n) for n in inp[1].split(" ")[1:] if n] + + +races = list(zip(times, distances)) + + +h.submit(list(it.accumulate((count_winning_times(*r) for r in races), operator.mul))[-1]) + + diff --git a/solutions/2023_day06/part2.py b/solutions/2023_day06/part2.py new file mode 100644 index 0000000..3c5923a --- /dev/null +++ b/solutions/2023_day06/part2.py @@ -0,0 +1,32 @@ +import operator + +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input=""" +Time: 7 15 30 +Distance: 9 40 200 +""") + +inp = h.get_input_list() + +time = int(inp[0].split(":")[1].replace(' ', '')) +dist = int(inp[1].split(":")[1].replace(' ', '')) + + +def get_race_distance(total_time, hold_time): + return (total_time - hold_time) * hold_time + + +def count_winning_times(max_time, dist_to_beat): + c = 0 + for ht in range(max_time): + if get_race_distance(max_time, ht) > dist_to_beat: + c += 1 + return c + + +h.submit(count_winning_times(time, dist)) diff --git a/solutions/2023_day07/part1.py b/solutions/2023_day07/part1.py new file mode 100644 index 0000000..89963c5 --- /dev/null +++ b/solutions/2023_day07/part1.py @@ -0,0 +1,79 @@ +from collections import defaultdict + +from util import * +from util import Grid as g +import itertools as it +import re + +h = Helper(test_mode=False, test_input=""" +32T3K 765 +T55J5 684 +KK677 28 +KTJJT 220 +QQQJA 483 +""") + +CARD_RANKS = { + "2": 2, + "3": 3, + "4": 4, + "5": 5, + "6": 6, + "7": 7, + "8": 8, + "9": 9, + "T": 10, + "J": 11, + "Q": 12, + "K": 13, + "A": 14 +} + + +def score_hand(hand): + score = 0 + m = 10e7 + for c in hand: + score += m * c + m /= 100 + # print(c, score) + + freqs = defaultdict(lambda: 0) + for c in hand: + freqs[c] += 1 + + if 5 in freqs.values(): + score += 6e10 + elif 4 in freqs.values(): + score += 5e10 + elif 3 in freqs.values() and 2 in freqs.values(): + score += 4e10 + elif 3 in freqs.values(): + score += 3e10 + elif list(freqs.values()).count(2) == 2: + score += 2e10 + elif 2 in freqs.values(): + score += 1e10 + + print(hand, score) + return score + + +inp = h.get_input_list() + +input_hands = [([CARD_RANKS[c] for c in line.split(" ")[0]], int(line.split(" ")[1])) for line in inp] + +scored_hands = [] +for hand, bet in input_hands: + score = score_hand(hand) + scored_hands.append((hand, bet, score)) + +sorted_hands = sorted(scored_hands, key=lambda h: h[2], reverse=False) +print(sorted_hands) + +winnings = 0 +for i, hand in enumerate(sorted_hands): + print((i + 1), hand[1]) + winnings += (i + 1) * hand[1] + +h.submit(int(winnings)) diff --git a/solutions/2023_day07/part2.py b/solutions/2023_day07/part2.py new file mode 100644 index 0000000..e9f16c2 --- /dev/null +++ b/solutions/2023_day07/part2.py @@ -0,0 +1,85 @@ +from collections import defaultdict + +from util import * +from util import Grid as g +import itertools as it +import re + +h = Helper(test_mode=False, test_input=""" +32T3K 765 +T55J5 684 +KK677 28 +KTJJT 220 +QQQJA 483 +""") + +CARD_RANKS = { + "2": 2, + "3": 3, + "4": 4, + "5": 5, + "6": 6, + "7": 7, + "8": 8, + "9": 9, + "T": 10, + "J": 1, + "Q": 12, + "K": 13, + "A": 14 +} + + +def score_hand(hand): + score = 0 + m = 10e7 + for c in hand: + score += m * c + m /= 100 + # print(c, score) + + freqs = defaultdict(lambda: 0) + jokers = 0 + for c in hand: + if c == CARD_RANKS["J"]: + jokers += 1 + else: + freqs[c] += 1 + + if jokers == 5 or max(freqs.values()) + jokers == 5: + score += 6e10 + elif max(freqs.values()) + jokers == 4: + score += 5e10 + elif 3 in freqs.values() and 2 in freqs.values() or \ + jokers == 2 and 2 in freqs.values() or \ + jokers == 1 and list(freqs.values()).count(2) == 2: + score += 4e10 + elif max(freqs.values()) + jokers == 3: + score += 3e10 + elif list(freqs.values()).count(2) == 2 or 2 in freqs.values() and jokers == 1: + score += 2e10 + elif 2 in freqs.values() or jokers == 1: + score += 1e10 + + print(hand, score) + return score + + +inp = h.get_input_list() + +input_hands = [([CARD_RANKS[c] for c in line.split(" ")[0]], int(line.split(" ")[1])) for line in inp] + +scored_hands = [] +for hand, bet in input_hands: + score = score_hand(hand) + scored_hands.append((hand, bet, score)) + +sorted_hands = sorted(scored_hands, key=lambda h: h[2], reverse=False) +print(sorted_hands) + +winnings = 0 +for i, hand in enumerate(sorted_hands): + print((i + 1), hand[1]) + winnings += (i + 1) * hand[1] + +h.submit(int(winnings)) diff --git a/solutions/2023_day08/part1.py b/solutions/2023_day08/part1.py new file mode 100644 index 0000000..7754c16 --- /dev/null +++ b/solutions/2023_day08/part1.py @@ -0,0 +1,29 @@ +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input=""" +LLR + +AAA = (BBB, BBB) +BBB = (AAA, ZZZ) +ZZZ = (ZZZ, ZZZ) +""") + +inp = h.get_input_list() + +dir_string = inp[0] +nodes = {l.split(" = ")[0]: {"L": l.split(" = ")[1][1:4], "R": l.split(" = ")[1][6:9]} for l in inp[2:]} + +cur_node = "AAA" +step = 0 +for d in it.cycle(dir_string): + print(cur_node) + cur_node = nodes[cur_node][d] + step += 1 + if cur_node == "ZZZ": + break + +h.submit(step) diff --git a/solutions/2023_day08/part2.py b/solutions/2023_day08/part2.py new file mode 100644 index 0000000..ae3692a --- /dev/null +++ b/solutions/2023_day08/part2.py @@ -0,0 +1,44 @@ +import math + +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input=""" +LR + +11A = (11B, XXX) +11B = (XXX, 11Z) +11Z = (11B, XXX) +22A = (22B, XXX) +22B = (22C, 22C) +22C = (22Z, 22Z) +22Z = (22B, 22B) +XXX = (XXX, XXX) +""") + +inp = h.get_input_list() + +dir_string = inp[0] +nodes = {l.split(" = ")[0]: {"L": l.split(" = ")[1][1:4], "R": l.split(" = ")[1][6:9]} for l in inp[2:]} + +cur_nodes = [k for k in nodes.keys() if k[-1] == "A"] +step = 0 +cycle_lens = [None] * len(cur_nodes) + +for d in it.cycle(dir_string): + # print(cur_nodes) + cur_nodes = [nodes[n][d] for n in cur_nodes] + step += 1 + + for i, n in enumerate(cur_nodes): + if n[-1] == "Z": + if cycle_lens[i] is None: + cycle_lens[i] = step + if None not in cycle_lens: + print(cycle_lens) + h.submit(math.lcm(*cycle_lens)) + +h.submit(step) diff --git a/solutions/2023_day09/part1.py b/solutions/2023_day09/part1.py new file mode 100644 index 0000000..c7703d9 --- /dev/null +++ b/solutions/2023_day09/part1.py @@ -0,0 +1,32 @@ +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input=""" +0 3 6 9 12 15 +1 3 6 10 15 21 +10 13 16 21 30 45 +""") + +inp = h.get_input_list() + +total = 0 +for line in inp: + seq = [int(c) for c in line.split(" ")] + seqs = [seq] + while any(n != 0 for n in seq): + seq = [b - a for a, b in it.pairwise(seq)] + seqs.append(seq) + + seqs.reverse() + seqs[0].append(0) + prev_val = 0 + for seq in seqs[1:]: + val = seq[-1] + prev_val + prev_val = val + seq.append(val) + total += prev_val + +h.submit(total) diff --git a/solutions/2023_day09/part2.py b/solutions/2023_day09/part2.py new file mode 100644 index 0000000..64e94a8 --- /dev/null +++ b/solutions/2023_day09/part2.py @@ -0,0 +1,34 @@ +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input=""" +0 3 6 9 12 15 +1 3 6 10 15 21 +10 13 16 21 30 45 +""") + +inp = h.get_input_list() + +total = 0 +for line in inp: + seq = [int(c) for c in line.split(" ")] + seqs = [seq] + while any(n != 0 for n in seq): + seq = [b - a for a, b in it.pairwise(seq)] + seqs.append(seq) + + seqs.reverse() + seqs[0].append(0) + prev_val = 0 + for seq in seqs[1:]: + print(seq) + val = seq[0] - prev_val + prev_val = val + seq.insert(0, val) + # print(prev_val) + total += prev_val + +h.submit(total) diff --git a/solutions/2023_day10/part1.py b/solutions/2023_day10/part1.py new file mode 100644 index 0000000..f2af336 --- /dev/null +++ b/solutions/2023_day10/part1.py @@ -0,0 +1,64 @@ +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input=""" +..F7. +.FJ|. +SJ.L7 +|F--J +LJ... +""") + +PIPES = { + "|": ((1, 0), (-1, 0)), + "-": ((0, 1), (0, -1)), + "L": ((-1, 0), (0, 1)), + "J": ((-1, 0), (0, -1)), + "7": ((1, 0), (0, -1)), + "F": ((1, 0), (0, 1)), + ".": [] +} + + +def follow_pipe(grd, pos, prev_pos): + last_dir = (pos[0] - prev_pos[0], pos[1] - prev_pos[1]) + for d in PIPES[Grid.get(grd, pos)]: + if not (d[0] == -last_dir[0] and d[1] == -last_dir[1]): + return pos[0] + d[0], pos[1] + d[1] + + +inp = h.get_input_grid() + +starting_pos = None +for y, row in enumerate(inp): + for x, c in enumerate(row): + if c == "S": + starting_pos = (y, x) + break + +second_pos = None +for first_dir in Grid.DIR4: + next_pos = (starting_pos[0] + first_dir[0], starting_pos[1] + first_dir[1]) + if Grid.get(inp, next_pos) is None: + continue + for d in PIPES[Grid.get(inp, next_pos)]: + if d[0] == -first_dir[0] and d[1] == -first_dir[1]: + second_pos = next_pos + break + +print(starting_pos, second_pos) + +cur_pos = second_pos +last_pos = starting_pos +step = 1 +while Grid.get(inp, cur_pos) != 'S': + new_pos = follow_pipe(inp, cur_pos, last_pos) + print(new_pos) + last_pos = cur_pos + cur_pos = new_pos + step += 1 + +h.submit(step // 2) diff --git a/solutions/2023_day10/part2.py b/solutions/2023_day10/part2.py new file mode 100644 index 0000000..3a5b5e7 --- /dev/null +++ b/solutions/2023_day10/part2.py @@ -0,0 +1,78 @@ +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input="""FF7FSF7F7F7F7F7F---7 +L|LJ||||||||||||F--J +FL-7LJLJ||||||LJL-77 +F--JF--7||LJLJ7F7FJ- +L---JF-JLJ.||-FJLJJ7 +|F|F-JF---7F7-L7L|7| +|FFJF7L7F-JF7|JL---7 +7-L-JL7||F7|L7F-7F7| +L.L7LFJ|||||FJL7||LJ +L7JLJL-JLJLJL--JLJ.L""") + +inp = h.get_input_grid() + +PIPES = { + "|": ((1, 0), (-1, 0)), + "-": ((0, 1), (0, -1)), + "L": ((-1, 0), (0, 1)), + "J": ((-1, 0), (0, -1)), + "7": ((1, 0), (0, -1)), + "F": ((1, 0), (0, 1)), + ".": [] +} + + +def follow_pipe(grd, pos, prev_pos): + last_dir = (pos[0] - prev_pos[0], pos[1] - prev_pos[1]) + for d in PIPES[Grid.get(grd, pos)]: + if not (d[0] == -last_dir[0] and d[1] == -last_dir[1]): + return pos[0] + d[0], pos[1] + d[1] + + +starting_pos = None +for y, row in enumerate(inp): + for x, c in enumerate(row): + if c == "S": + starting_pos = (y, x) + break + +second_pos = None +for first_dir in Grid.DIR4: + next_pos = (starting_pos[0] + first_dir[0], starting_pos[1] + first_dir[1]) + if Grid.get(inp, next_pos) is None: + continue + for d in PIPES[Grid.get(inp, next_pos)]: + if d[0] == -first_dir[0] and d[1] == -first_dir[1]: + second_pos = next_pos + break + +visited = [starting_pos, second_pos] + +cur_pos = second_pos +last_pos = starting_pos +while Grid.get(inp, cur_pos) != 'S': + new_pos = follow_pipe(inp, cur_pos, last_pos) + visited.append(new_pos) + last_pos = cur_pos + cur_pos = new_pos + +enclosed_count = 0 +for y, row in enumerate(inp): + a = 0 + for x, c in enumerate(row): + if (y, x) not in visited: + enclosed_count += a % 2 + print("I" if a % 2 > 0 else c, end='') + continue + elif (y, x) in visited and c not in "-F7": + a += 1 + print(c if (y, x) in visited else ' ', end='') + print() + +h.submit(enclosed_count) diff --git a/solutions/2023_day11/part1.py b/solutions/2023_day11/part1.py new file mode 100644 index 0000000..786f493 --- /dev/null +++ b/solutions/2023_day11/part1.py @@ -0,0 +1,50 @@ +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input=""" +...#...... +.......#.. +#......... +.......... +......#... +.#........ +.........# +.......... +.......#.. +#...#..... +""") + +inp = h.get_input_grid() + +y = 0 +while y < len(inp): + if all(c == '.' for c in inp[y]): + inp = inp[:y] + [inp[y]] + inp[y:] + y += 1 + y += 1 + +inp = list(zip(*inp)) + +y = 0 +while y < len(inp): + if all(c == '.' for c in inp[y]): + inp = inp[:y] + [inp[y]] + inp[y:] + y += 1 + y += 1 + +inp = list(zip(*inp)) + +galaxies = [] +for y, row in enumerate(inp): + for x, c in enumerate(row): + if c == "#": + galaxies.append((x, y)) + +total_dist = 0 +for g1, g2 in it.combinations(galaxies, 2): + total_dist += abs(g1[0] - g2[0]) + abs(g1[1] - g2[1]) + +h.submit(total_dist) \ No newline at end of file diff --git a/solutions/2023_day11/part2.py b/solutions/2023_day11/part2.py new file mode 100644 index 0000000..eb9278b --- /dev/null +++ b/solutions/2023_day11/part2.py @@ -0,0 +1,58 @@ +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input=""" +...#...... +.......#.. +#......... +.......... +......#... +.#........ +.........# +.......... +.......#.. +#...#..... +""") + +inp = h.get_input_grid() + +EXPANSION = 1000000 + +expansions_x = [] +expansions_y = [] + +y = 0 +while y < len(inp): + if all(c == '.' for c in inp[y]): + expansions_y.append(y) + y += 1 + +inp = list(zip(*inp)) + +x = 0 +while x < len(inp): + if all(c == '.' for c in inp[x]): + expansions_x.append(x) + x += 1 + +inp = list(zip(*inp)) + +galaxies = [] +for y, row in enumerate(inp): + for x, c in enumerate(row): + if c == "#": + galaxies.append((y, x)) + +total_dist = 0 +print(galaxies) +print(expansions_y) +print(expansions_x) +for g1, g2 in it.combinations(galaxies, 2): + total_dist += abs(g1[0] - g2[0]) + abs(g1[1] - g2[1]) + total_dist += len(list(e for e in expansions_y if min(g1[0], g2[0]) < e < max(g1[0], g2[0]))) * (EXPANSION - 1) + total_dist += len(list(e for e in expansions_x if min(g1[1], g2[1]) < e < max(g1[1], g2[1]))) * (EXPANSION - 1) + +h.submit(total_dist) diff --git a/solutions/2023_day12/part1.py b/solutions/2023_day12/part1.py new file mode 100644 index 0000000..1181605 --- /dev/null +++ b/solutions/2023_day12/part1.py @@ -0,0 +1,70 @@ +from util import * +from util import Grid as g +import itertools as it +import re + +h = Helper(test_mode=False, test_input=""" +???.### 1,1,3 +.??..??...?##. 1,1,3 +?#?#?#?#?#?#?#? 1,3,1,6 +????.#...#... 4,1,1 +????.######..#####. 1,6,5 +?###???????? 3,2,1 +""") + +inp = h.get_input_list() + + +def check_record_consistency(springs, groups): + group_len = 0 + group_index = -1 + in_group = False + for c in springs + ["."]: + if c == '?': + return True + if in_group: + if c == "#": + group_len += 1 + else: + if groups[group_index] != group_len: + return False + in_group = False + else: + if c == "#": + group_index += 1 + if group_index >= len(groups): + return False + in_group = True + group_len = 1 + + return group_index == len(groups) - 1 + + +def count_valid_combinations(springs, groups): + if '?' not in springs: + return 1 + + question_index = springs.index('?') + + total_options = 0 + + for c in ".#": + springs[question_index] = c + if check_record_consistency(springs, groups): + total_options += count_valid_combinations(springs.copy(), groups) + + return total_options + + +records = [] + +for line in inp: + records.append((list(line.split(" ")[0]), tuple(int(d) for d in line.split(" ")[1].split(",")))) + + +options_sum = 0 +for record in records: + options = count_valid_combinations(record[0].copy(), record[1]) + options_sum += options + +h.submit(options_sum) diff --git a/solutions/2023_day13/part1.py b/solutions/2023_day13/part1.py new file mode 100644 index 0000000..d3efa7c --- /dev/null +++ b/solutions/2023_day13/part1.py @@ -0,0 +1,47 @@ +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(day=13, test_mode=False, test_input=""" +#.##..##. +..#.##.#. +##......# +##......# +..#.##.#. +..##..##. +#.#.##.#. + +#...##..# +#....#..# +..##..### +#####.##. +#####.##. +..##..### +#....#..# +""") + +inp = h.get_input_list_2d() + + +def get_mirrors(grid): + mirrors = [] + + for y in range(1, len(grid)): + if y <= len(grid) / 2: + if grid[0:y] == list(reversed(grid[y:2*y])): + mirrors.append(y) + else: + if grid[len(grid)-2*(len(grid)-y):y] == list(reversed(grid[y:])): + mirrors.append(y) + return mirrors + + +total_sum = 0 +for mat in inp: + total_sum += 100 * sum(get_mirrors(mat)) + total_sum += sum(get_mirrors(list(zip(*mat)))) + +# print() +h.submit(total_sum) diff --git a/solutions/2023_day13/part2.py b/solutions/2023_day13/part2.py new file mode 100644 index 0000000..7a73f5f --- /dev/null +++ b/solutions/2023_day13/part2.py @@ -0,0 +1,74 @@ +import copy + +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(day=13, test_mode=False, test_input=""" +#.##..##. +..#.##.#. +##......# +##......# +..#.##.#. +..##..##. +#.#.##.#. + +#...##..# +#....#..# +..##..### +#####.##. +#####.##. +..##..### +#....#..# +""") + +inp = h.get_input_list_2d() + + +def comp_list(l1, l2): + return sum(sum(c1 != c2 for c1, c2 in zip(r1, r2)) for r1, r2 in zip(l1, l2)) + + +def make_equal(l1, l2): + for r1, r2 in zip(l1, l2): + for i, v in enumerate(r2): + r1[i] = v + + +def correct_smudge(grid): + grid = copy.deepcopy(grid) + for y in range(1, len(grid)): + if y <= len(grid) / 2: + if comp_list(grid[0:y], list(reversed(grid[y:2 * y]))) == 1: + make_equal(grid[0:y], list(reversed(grid[y:2 * y]))) + return True, grid, y + else: + if comp_list(grid[len(grid) - 2 * (len(grid) - y):y], list(reversed(grid[y:]))) == 1: + make_equal(grid[len(grid) - 2 * (len(grid) - y):y], list(reversed(grid[y:]))) + return True, grid, y + return False, grid, None + + +total_sum = 0 +for str_list in inp: + mat = list(map(list, str_list)) + print() + Grid.print(mat) + + was_corrected, mat, value = correct_smudge(mat) + if was_corrected: + total_sum += 100 * value + else: + was_corrected, flipped_mat, value = correct_smudge(list(map(list, zip(*mat)))) + mat = list(zip(*flipped_mat)) + total_sum += value + + if not was_corrected: + raise ValueError + + print(value) + Grid.print(mat) + +h.submit(total_sum) diff --git a/solutions/2023_day14/part1.py b/solutions/2023_day14/part1.py new file mode 100644 index 0000000..747fb84 --- /dev/null +++ b/solutions/2023_day14/part1.py @@ -0,0 +1,41 @@ +import copy + +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input="""O....#.... +O.OO#....# +.....##... +OO.#O....O +.O.....O#. +O.#..O.#.# +..O..#O..O +.......O.. +#....###.. +#OO..#....""") + + +def tilt_north(grid): + new_grid = copy.deepcopy(grid) + Grid.print(new_grid) + + for x, column in enumerate(zip(*grid)): + obstacle_y = -1 + for y, c in enumerate(column): + if c == "#": + obstacle_y = y + elif c == "O": + new_grid[y][x] = "." + obstacle_y += 1 + new_grid[obstacle_y][x] = "O" + return new_grid + + +inp = h.get_input_grid() + +res = tilt_north(inp) + +h.submit(sum((len(res) - y) * row.count("O") for y, row in enumerate(res))) diff --git a/solutions/2023_day14/part2.py b/solutions/2023_day14/part2.py new file mode 100644 index 0000000..aae6547 --- /dev/null +++ b/solutions/2023_day14/part2.py @@ -0,0 +1,76 @@ +import copy +from collections import defaultdict + +from util import * +from util import Grid as g +import itertools as it +import re + +h = Helper(test_mode=False, test_input="""O....#.... +O.OO#....# +.....##... +OO.#O....O +.O.....O#. +O.#..O.#.# +..O..#O..O +.......O.. +#....###.. +#OO..#....""") + + +def tilt_north(grid): + new_grid = copy.deepcopy(grid) + + for x, column in enumerate(zip(*grid)): + obstacle_y = -1 + for y, c in enumerate(column): + if c == "#": + obstacle_y = y + elif c == "O": + new_grid[y][x] = "." + obstacle_y += 1 + new_grid[obstacle_y][x] = "O" + return new_grid + + +def tilt_south(grid): + return list(reversed(tilt_north(list(reversed(grid))))) + + +def tilt_west(grid): + return list(map(list, zip(*tilt_north(list(map(list, zip(*grid))))))) + + +def tilt_east(grid): + return list(map(lambda l: list(reversed(l)), tilt_west(list(map(lambda l: list(reversed(l)), grid))))) + + +inp = h.get_input_grid() + +visited_states = defaultdict(lambda: 0) + +spin_cycle = (tilt_north, tilt_west, tilt_south, tilt_east) + +cycle_start = None +cycle_len = None +cycle_states = [] + +for step in range(1000000000): + key = tuple(map(tuple, inp)) + prev_visits = visited_states[key] + if prev_visits == 1: + if cycle_start is None: + cycle_start = step + cycle_states.append(copy.deepcopy(inp)) + elif prev_visits == 2: + cycle_len = step - cycle_start + break + + visited_states[key] = prev_visits + 1 + + for tilt in spin_cycle: + inp = tilt(inp) + +last_state = cycle_states[(1000000000 - cycle_start) % cycle_len] + +h.submit(sum((len(last_state) - y) * row.count("O") for y, row in enumerate(last_state))) diff --git a/solutions/2023_day15/part1.py b/solutions/2023_day15/part1.py new file mode 100644 index 0000000..3b4160f --- /dev/null +++ b/solutions/2023_day15/part1.py @@ -0,0 +1,30 @@ +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input=""" +rn=1,cm-,qp=3,cm=2,qp-,pc=4,ot=9,ab=5,pc-,pc=6,ot=7 +""") + +inp = h.get_input_raw() +inp = inp.replace("\n", "") +inp = inp.split(",") + + +def ascii_hash(s): + val = 0 + for c in s: + val += ord(c) + val *= 17 + val %= 256 + return val + + +total = 0 +for code in inp: + print(ascii_hash(code)) + total += ascii_hash(code) + +h.submit(total) diff --git a/solutions/2023_day15/part2.py b/solutions/2023_day15/part2.py new file mode 100644 index 0000000..4b6b6fb --- /dev/null +++ b/solutions/2023_day15/part2.py @@ -0,0 +1,51 @@ +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input=""" +rn=1,cm-,qp=3,cm=2,qp-,pc=4,ot=9,ab=5,pc-,pc=6,ot=7 +""") + +inp = h.get_input_raw() +inp = inp.replace("\n", "") +inp = inp.split(",") + + +def ascii_hash(s): + val = 0 + for c in s: + val += ord(c) + val *= 17 + val %= 256 + return val + + +hashmap = [[] for _ in range(256)] + +for code in inp: + if code[-1] == "-": + hsh = ascii_hash(code[:-1]) + for lens in hashmap[hsh]: + if lens[0] == code[:-1]: + hashmap[hsh].remove(lens) + else: + hsh = ascii_hash(code[:-2]) + focus = code[-1] + for lens in hashmap[hsh]: + if lens[0] == code[:-2]: + lens[1] = focus + break + else: + hashmap[hsh].append([code[:-2], focus]) + + +print(hashmap) + +total = 0 +for i, box in enumerate(hashmap): + for j, lens in enumerate(box): + total += (1 + i) * (1 + j) * int(lens[1]) + +h.submit(total) diff --git a/solutions/2023_day16/part1.py b/solutions/2023_day16/part1.py new file mode 100644 index 0000000..9f5ccff --- /dev/null +++ b/solutions/2023_day16/part1.py @@ -0,0 +1,76 @@ +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input=r""" +.|...\.... +|.-.\..... +.....|-... +........|. +.......... +.........\ +..../.\\.. +.-.-/..|.. +.|....-|.\ +..//.|.... +""") + + +FORWARD_SLASH_DIR_MAP = { + (1, 0): (0, -1), + (-1, 0): (0, 1), + (0, 1): (-1, 0), + (0, -1): (1, 0), +} + +BACK_SLASH_DIR_MAP = { + (1, 0): (0, 1), + (-1, 0): (0, -1), + (0, 1): (1, 0), + (0, -1): (-1, 0), +} + +inp = h.get_input_grid() +Grid.print(inp) + + +def energize_tiles(tiles, energy_grid, visited_states_grid, pos, start_dir): + d = start_dir + while True: + c = Grid.get(tiles, pos) + if c is None: + return energy_grid + + if d in visited_states_grid[pos[0]][pos[1]]: + return energy_grid + + energy_grid[pos[0]][pos[1]] += 1 + visited_states_grid[pos[0]][pos[1]].append(d) + + if c == '/': + d = FORWARD_SLASH_DIR_MAP[d] + elif c == '\\': + d = BACK_SLASH_DIR_MAP[d] + elif c == '|': + if d[1] != 0: + energize_tiles(tiles, energy_grid, visited_states_grid, (pos[0] + 1, pos[1]), (1, 0)) + energize_tiles(tiles, energy_grid, visited_states_grid, (pos[0] - 1, pos[1]), (-1, 0)) + return energized_tiles + elif c == '-': + if d[0] != 0: + energize_tiles(tiles, energy_grid, visited_states_grid, (pos[0], pos[1] + 1), (0, 1)) + energize_tiles(tiles, energy_grid, visited_states_grid, (pos[0], pos[1] - 1), (0, -1)) + return energized_tiles + + pos = (pos[0] + d[0], pos[1] + d[1]) + + +energized_tiles = [list(0 for _ in range(len(row))) for row in inp] +visited = [list([] for _ in range(len(row))) for row in inp] + +energize_tiles(inp, energized_tiles, visited, (0, 0), (0, 1)) +Grid.print(energized_tiles) + +h.submit(sum(len(list(n for n in row if n > 0)) for row in energized_tiles)) diff --git a/solutions/2023_day16/part2.py b/solutions/2023_day16/part2.py new file mode 100644 index 0000000..982f852 --- /dev/null +++ b/solutions/2023_day16/part2.py @@ -0,0 +1,88 @@ +from util import * +from util import Grid as g +import itertools as it +import re + +h = Helper(test_mode=False, test_input=r""" +.|...\.... +|.-.\..... +.....|-... +........|. +.......... +.........\ +..../.\\.. +.-.-/..|.. +.|....-|.\ +..//.|.... +""") + +FORWARD_SLASH_DIR_MAP = { + (1, 0): (0, -1), + (-1, 0): (0, 1), + (0, 1): (-1, 0), + (0, -1): (1, 0), +} + +BACK_SLASH_DIR_MAP = { + (1, 0): (0, 1), + (-1, 0): (0, -1), + (0, 1): (1, 0), + (0, -1): (-1, 0), +} + +inp = h.get_input_grid() +Grid.print(inp) + + +def energize_tiles(tiles, energy_grid, visited_states_grid, pos, start_dir): + d = start_dir + while True: + c = Grid.get(tiles, pos) + if c is None: + return energy_grid + + if d in visited_states_grid[pos[0]][pos[1]]: + return energy_grid + + energy_grid[pos[0]][pos[1]] += 1 + visited_states_grid[pos[0]][pos[1]].append(d) + + if c == '/': + d = FORWARD_SLASH_DIR_MAP[d] + elif c == '\\': + d = BACK_SLASH_DIR_MAP[d] + elif c == '|': + if d[1] != 0: + energize_tiles(tiles, energy_grid, visited_states_grid, (pos[0] + 1, pos[1]), (1, 0)) + energize_tiles(tiles, energy_grid, visited_states_grid, (pos[0] - 1, pos[1]), (-1, 0)) + return energy_grid + elif c == '-': + if d[0] != 0: + energize_tiles(tiles, energy_grid, visited_states_grid, (pos[0], pos[1] + 1), (0, 1)) + energize_tiles(tiles, energy_grid, visited_states_grid, (pos[0], pos[1] - 1), (0, -1)) + return energy_grid + + pos = (pos[0] + d[0], pos[1] + d[1]) + + +max_tiles = 0 + +for s_dir, i in it.product(Grid.DIR4, range(len(inp))): + start_pos = None + if s_dir == (0, 1): + start_pos = (i, 0) + elif s_dir == (0, -1): + start_pos = (i, len(inp) - 1) + elif s_dir == (1, 0): + start_pos = (0, i) + elif s_dir == (-1, 0): + start_pos = (len(inp) - 1, i) + + energized_tiles = [list(0 for _ in range(len(row))) for row in inp] + visited = [list([] for _ in range(len(row))) for row in inp] + + energize_tiles(inp, energized_tiles, visited, start_pos, s_dir) + + max_tiles = max(max_tiles, sum(len(list(n for n in row if n > 0)) for row in energized_tiles)) + +h.submit(max_tiles) diff --git a/solutions/2023_day17/part1.py b/solutions/2023_day17/part1.py new file mode 100644 index 0000000..89d527d --- /dev/null +++ b/solutions/2023_day17/part1.py @@ -0,0 +1,67 @@ +from util import * +from util import Grid as g +import itertools as it +import re +from queue import PriorityQueue + + +h = Helper(test_mode=False, test_input=""" +2413432311323 +3215453535623 +3255245654254 +3446585845452 +4546657867536 +1438598798454 +4457876987766 +3637877979653 +4654967986887 +4564679986453 +1224686865563 +2546548887735 +4322674655533 +""") + +inp = h.get_input_grid() + + +def get_successors(state): + pos = state[0] + curr_dir = state[1] + straight_line_steps = state[2] + + successors = [] + + # Turning left or right + for d in ((1, 0), (-1, 0)) if curr_dir[0] == 0 else ((0, 1), (0, -1)): + successors.append(((pos[0] + d[0], pos[1] + d[1]), d, 1)) + + if straight_line_steps < 3: + successors.append(((pos[0] + curr_dir[0], pos[1] + curr_dir[1]), curr_dir, straight_line_steps + 1)) + + return successors + + +q = PriorityQueue() +q.put((0, ((0, 0), (0, 1), 0))) + +goal_pos = (len(inp) - 1, len(inp[0]) - 1) +visited = set() + +while True: + curr_cost, curr_state = q.get(block=False) + + if curr_state in visited: + continue + + # print(curr_state) + if curr_state == ((11, 7), (0, -1), 3): + pass + visited.add(curr_state) + + if curr_state[0] == goal_pos: + h.submit(curr_cost) + break + + for successor in get_successors(curr_state): + if successor not in visited and (cost := Grid.get(inp, successor[0])) is not None: + q.put(((curr_cost + int(cost)), successor)) diff --git a/solutions/2023_day17/part2.py b/solutions/2023_day17/part2.py new file mode 100644 index 0000000..e59ba76 --- /dev/null +++ b/solutions/2023_day17/part2.py @@ -0,0 +1,66 @@ +from util import * +from util import Grid as g +import itertools as it +import re +from queue import PriorityQueue + + +h = Helper(test_mode=False, test_input=""" +2413432311323 +3215453535623 +3255245654254 +3446585845452 +4546657867536 +1438598798454 +4457876987766 +3637877979653 +4654967986887 +4564679986453 +1224686865563 +2546548887735 +4322674655533 +""") + +inp = h.get_input_grid() + + +def get_successors(state): + pos = state[0] + curr_dir = state[1] + straight_line_steps = state[2] + + successors = [] + + # Turning left or right + if straight_line_steps >= 4: + for d in ((1, 0), (-1, 0)) if curr_dir[0] == 0 else ((0, 1), (0, -1)): + successors.append(((pos[0] + d[0], pos[1] + d[1]), d, 1)) + + if straight_line_steps < 10: + successors.append(((pos[0] + curr_dir[0], pos[1] + curr_dir[1]), curr_dir, straight_line_steps + 1)) + + return successors + + +q = PriorityQueue() +q.put((0, ((0, 0), (0, 1), 0))) +q.put((0, ((0, 0), (1, 0), 0))) + +goal_pos = (len(inp) - 1, len(inp[0]) - 1) +visited = set() + +while True: + curr_cost, curr_state = q.get(block=False) + + if curr_state in visited: + continue + + visited.add(curr_state) + + if curr_state[0] == goal_pos and curr_state[2] >= 4: + h.submit(curr_cost) + break + + for successor in get_successors(curr_state): + if successor not in visited and (cost := Grid.get(inp, successor[0])) is not None: + q.put(((curr_cost + int(cost)), successor)) diff --git a/solutions/2023_day18/part1.py b/solutions/2023_day18/part1.py new file mode 100644 index 0000000..88c579d --- /dev/null +++ b/solutions/2023_day18/part1.py @@ -0,0 +1,99 @@ +from collections import defaultdict + +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input=""" +R 6 (#70c710) +D 5 (#0dc571) +L 2 (#5713f0) +D 2 (#d2c081) +R 2 (#59c680) +D 2 (#411b91) +L 5 (#8ceee2) +U 2 (#caa173) +L 1 (#1b58a2) +U 2 (#caa171) +R 2 (#7807d2) +U 3 (#a77fa3) +L 2 (#015232) +U 2 (#7a21e3) +""") + +inp = h.get_input_list() + + +DIR_NAMES = { + 'R': (0, 1), + 'L': (0, -1), + 'D': (1, 0), + 'U': (-1, 0) +} + + +vertical_edges = [] +horizontal_edges = [] +pos = (0, 0) + +for line in inp: + m = re.match(r"(\w) (\d+) \(#(.{6})\)", line) + + d = DIR_NAMES[m.group(1)] + length = int(m.group(2)) + + new_pos = (pos[0] + d[0] * length, pos[1] + d[1] * length) + + (vertical_edges if d[1] == 0 else horizontal_edges).append((pos, new_pos, m.group(3))) + + pos = new_pos + +y_bounds = (min(min(e[0][0], e[1][0]) for e in vertical_edges), max(max(e[0][0], e[1][0]) for e in vertical_edges)) + +area = 0 +for y in range(y_bounds[0], y_bounds[1] + 1): + intersecting_edges = [] + for e in vertical_edges: + p1 = e[0] + p2 = e[1] + max_y = max(p1[0], p2[0]) + min_y = min(p1[0], p2[0]) + + if min_y <= y <= max_y: + intersecting_edges.append(e) + + interior = False + on_edge = False + last_x = 0 + last_was_dug = False + + for e in sorted(intersecting_edges, key=lambda e: e[0][1]): + p1 = e[0] + p2 = e[1] + max_y = max(p1[0], p2[0]) + min_y = min(p1[0], p2[0]) + + x = p1[1] + if on_edge or interior: + area += x - last_x + + last_x = x + + if y == max_y: + on_edge = not on_edge + elif y == min_y: + on_edge = not on_edge + interior = not interior + else: + interior = not interior + + if not last_was_dug and (interior or on_edge): + area += 1 + last_was_dug = interior or on_edge + + assert not on_edge + assert not interior + +h.submit(area) diff --git a/solutions/2023_day18/part2.py b/solutions/2023_day18/part2.py new file mode 100644 index 0000000..0cad36a --- /dev/null +++ b/solutions/2023_day18/part2.py @@ -0,0 +1,111 @@ +from collections import defaultdict + +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input=""" +R 6 (#70c710) +D 5 (#0dc571) +L 2 (#5713f0) +D 2 (#d2c081) +R 2 (#59c680) +D 2 (#411b91) +L 5 (#8ceee2) +U 2 (#caa173) +L 1 (#1b58a2) +U 2 (#caa171) +R 2 (#7807d2) +U 3 (#a77fa3) +L 2 (#015232) +U 2 (#7a21e3) +""") + +inp = h.get_input_list() + + +DIRS = [(0, 1), (1, 0), (0, -1), (-1, 0)] + + +def get_area(y, edges): + a = 0 + intersecting_edges = [] + for e in edges: + p1 = e[0] + p2 = e[1] + max_y = max(p1[0], p2[0]) + min_y = min(p1[0], p2[0]) + + if min_y <= y <= max_y: + intersecting_edges.append(e) + + interior = False + on_edge = False + last_x = 0 + last_was_dug = False + + for e in sorted(intersecting_edges, key=lambda e: e[0][1]): + p1 = e[0] + p2 = e[1] + max_y = max(p1[0], p2[0]) + min_y = min(p1[0], p2[0]) + + x = p1[1] + if on_edge or interior: + a += x - last_x + + last_x = x + + if y == max_y: + on_edge = not on_edge + elif y == min_y: + on_edge = not on_edge + interior = not interior + else: + interior = not interior + + if not last_was_dug and (interior or on_edge): + a += 1 + last_was_dug = interior or on_edge + + assert not on_edge + assert not interior + + return a + +vertical_edges = [] +horizontal_edges = [] +pos = (0, 0) + +for line in inp: + m = re.match(r"(\w) (\d+) \(#(.{6})\)", line) + + d = DIRS[int(m.group(3)[-1])] + length = int(m.group(3)[:-1], 16) + + new_pos = (pos[0] + d[0] * length, pos[1] + d[1] * length) + + (vertical_edges if d[1] == 0 else horizontal_edges).append((pos, new_pos)) + + pos = new_pos + +critical_y = set() + +for e in vertical_edges: + critical_y.add(e[0][0]) + critical_y.add(e[0][1]) + +critical_y_sorted = sorted(critical_y) +print(critical_y_sorted) + +area = 0 +for y in critical_y: + area += get_area(y, vertical_edges) + +for y1, y2 in it.pairwise(critical_y_sorted): + area += (y2 - y1 - 1) * get_area(y1 + 1, vertical_edges) + + +h.submit(area) diff --git a/solutions/2023_day19/part1.py b/solutions/2023_day19/part1.py new file mode 100644 index 0000000..6627f8a --- /dev/null +++ b/solutions/2023_day19/part1.py @@ -0,0 +1,83 @@ +import functools + +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input=""" +px{a<2006:qkq,m>2090:A,rfg} +pv{a>1716:R,A} +lnx{m>1548:A,A} +rfg{s<537:gd,x>2440:R,A} +qs{s>3448:A,lnx} +qkq{x<1416:A,crn} +crn{x>2662:A,R} +in{s<1351:px,qqz} +qqz{s>2770:qs,m<1801:hdj,R} +gd{a>3333:R,R} +hdj{m>838:A,pv} + +{x=787,m=2655,a=1222,s=2876} +{x=1679,m=44,a=2067,s=496} +{x=2036,m=264,a=79,s=2244} +{x=2461,m=1339,a=466,s=291} +{x=2127,m=1623,a=2188,s=1013} +""") + +inp = h.get_input_list_2d() + +workflows = {} +for line in inp[0]: + m = re.fullmatch(r"(\w+)\{((?:[xmas][><]\d+:\w+,)+)(\w+)}", line) + name = m.group(1) + flows_raw = m.group(2) + default = m.group(3) + + flows = [] + for f in flows_raw.strip(',').split(','): + var = f[0] + op = f[1] + thresh = int(f.split(':')[0][2:]) + next_name = f.split(':')[1] + if op == '>': + flows.append((functools.partial(lambda v, th, p: p[v] > th, var, thresh), next_name)) + elif op == '<': + flows.append((functools.partial(lambda v, th, p: p[v] < th, var, thresh), next_name)) + else: + raise ValueError(op) + + workflows[name] = (flows, default) + + +parts = [] +for line in inp[1]: + parts.append({v[0]: int(v[2:]) for v in line.strip('{}').split(',')}) + + +total_rating = 0 +for part in parts: + print(part) + flow_name = "in" + + while True: + flow = workflows[flow_name] + + for cond in flow[0]: + if cond[0](part): + flow_name = cond[1] + break + else: + flow_name = flow[1] + + if flow_name == 'R': + print('R') + break + + if flow_name == 'A': + print('A') + total_rating += sum(part.values()) + break + +h.submit(total_rating) diff --git a/solutions/2023_day19/part2.py b/solutions/2023_day19/part2.py new file mode 100644 index 0000000..6f3f97a --- /dev/null +++ b/solutions/2023_day19/part2.py @@ -0,0 +1,90 @@ +import copy +import functools +import operator + +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input=""" +px{a<2006:qkq,m>2090:A,rfg} +pv{a>1716:R,A} +lnx{m>1548:A,A} +rfg{s<537:gd,x>2440:R,A} +qs{s>3448:A,lnx} +qkq{x<1416:A,crn} +crn{x>2662:A,R} +in{s<1351:px,qqz} +qqz{s>2770:qs,m<1801:hdj,R} +gd{a>3333:R,R} +hdj{m>838:A,pv} + +{x=787,m=2655,a=1222,s=2876} +{x=1679,m=44,a=2067,s=496} +{x=2036,m=264,a=79,s=2244} +{x=2461,m=1339,a=466,s=291} +{x=2127,m=1623,a=2188,s=1013} +""") + + +def count_accepted_parts(name, flows, ranges): + print(name, ranges) + if name == 'A': + return functools.reduce(operator.mul, (v2 - v1 for v2, v1 in ranges.values())) + + if name == 'R': + return 0 + + flow = flows[name] + ranges = {k: copy.copy(v) for k, v in ranges.items()} + + total_accepted = 0 + for cond in flow[0]: + if cond[3] == '>': + if not ranges[cond[1]][1] - 1 > cond[2]: + continue + if ranges[cond[1]][0] > cond[2]: + return count_accepted_parts(cond[0], flows, ranges) + + new_ranges = {k: copy.copy(v) for k, v in ranges.items()} + new_ranges[cond[1]][0] = cond[2] + 1 + total_accepted += count_accepted_parts(cond[0], flows, new_ranges) + ranges[cond[1]][1] = cond[2] + 1 + else: + if not ranges[cond[1]][0] < cond[2]: + continue + if ranges[cond[1]][1] - 1 < cond[2]: + return count_accepted_parts(cond[0], flows, ranges) + new_ranges = {k: copy.copy(v) for k, v in ranges.items()} + new_ranges[cond[1]][1] = cond[2] + total_accepted += count_accepted_parts(cond[0], flows, new_ranges) + ranges[cond[1]][0] = cond[2] + return total_accepted + count_accepted_parts(flow[1], flows, ranges) + + +inp = h.get_input_list_2d() + +workflows = {} +for line in inp[0]: + m = re.fullmatch(r"(\w+)\{((?:[xmas][><]\d+:\w+,)+)(\w+)}", line) + name = m.group(1) + flows_raw = m.group(2) + default = m.group(3) + + flows = [] + for f in flows_raw.strip(',').split(','): + var = f[0] + op = f[1] + thresh = int(f.split(':')[0][2:]) + next_name = f.split(':')[1] + if op in '<>': + flows.append((next_name, var, thresh, op)) + else: + raise ValueError(op) + + workflows[name] = (flows, default) + + +h.submit(count_accepted_parts("in", workflows, {k: [1, 4001] for k in 'xmas'})) diff --git a/solutions/2023_day20/part1.py b/solutions/2023_day20/part1.py new file mode 100644 index 0000000..c2a4a8f --- /dev/null +++ b/solutions/2023_day20/part1.py @@ -0,0 +1,125 @@ +import queue + +from util import * +from util import Grid as g +import itertools as it +import re + +h = Helper(test_mode=False, test_input=""" +broadcaster -> a +%a -> inv, con +&inv -> b +%b -> con +&con -> output +""") + + +pulses_queue = queue.Queue() + + +class Module: + def __init__(self, name: str, link_strs: t.List[str]): + self.name: str = name + self.link_strs: t.List[str] = link_strs + self.link_objects: t.List[Module] = [] + + self.low_pulses_sent = 0 + self.high_pulses_sent = 0 + + def init_link_objects(self, all_modules): + for li in self.link_strs: + try: + self.link_objects.append(all_modules[li]) + except KeyError: + pass + + def send_pulse(self, is_high_pulse: bool): + global pulses_queue + + if is_high_pulse: + self.high_pulses_sent += len(self.link_strs) + else: + self.low_pulses_sent += len(self.link_strs) + + for li in self.link_objects: + pulses_queue.put((li, is_high_pulse, self.name)) + + def activate(self, is_high_pulse: bool, activator_name: str): + raise NotImplemented() + + +class FlipFlop(Module): + def __init__(self, name: str, link_strs: t.List[str]): + super().__init__(name, link_strs) + self.state = False + + def activate(self, is_high_pulse: bool, activator_name): + if is_high_pulse: + return + + self.state = not self.state + self.send_pulse(self.state) + + +class Conjunction(Module): + def __init__(self, name: str, link_strs: t.List[str]): + super().__init__(name, link_strs) + self.input_states = {} + + def init_link_objects(self, all_modules): + super().init_link_objects(all_modules) + for mod in all_modules.values(): + if self.name in mod.link_strs: + self.input_states[mod.name] = False + + def activate(self, is_high_pulse: bool, activator_name: str): + self.input_states[activator_name] = is_high_pulse + self.send_pulse(not all(self.input_states.values())) + + +class Broadcast(Module): + def activate(self, is_high_pulse: bool, activator_name: str): + self.send_pulse(is_high_pulse) + + +class Button(Module): + def activate(self, is_high_pulse: bool, activator_name: str): + self.send_pulse(False) + + +TYPE_CODES = { + '%': FlipFlop, + '&': Conjunction, + 'b': Broadcast, +} + +inp = h.get_input_list() + +modules: t.Dict[str, Module] = {} + +for line in inp: + type_code = line[0] + module_type = TYPE_CODES[type_code] + name = line.split(' -> ')[0].strip('%&') + links = line.split(' -> ')[1].split(', ') + modules[name] = module_type(name, links) + +modules["button"] = (Button("button", ["broadcaster"])) + +for m in modules.values(): + m.init_link_objects(modules) + +for _ in range(1000): + modules["button"].activate(False, "") + + while not pulses_queue.empty(): + pulse = pulses_queue.get(block=False) + # print(pulse, pulse[0].name) + pulse[0].activate(pulse[1], pulse[2]) + +total_low_pulses = sum(m.low_pulses_sent for m in modules.values()) +total_high_pulses = sum(m.high_pulses_sent for m in modules.values()) +print(total_low_pulses) +print(total_high_pulses) + +h.submit(total_low_pulses * total_high_pulses) diff --git a/solutions/2023_day21/part1.py b/solutions/2023_day21/part1.py new file mode 100644 index 0000000..0198db6 --- /dev/null +++ b/solutions/2023_day21/part1.py @@ -0,0 +1,48 @@ +from queue import Queue + +from util import * +from util import Grid as g +import itertools as it +import re + +h = Helper(test_mode=False, test_input="""........... +.....###.#. +.###.##..#. +..#.#...#.. +....#.#.... +.##..S####. +.##..#...#. +.......##.. +.##.#.####. +.##..##.##. +...........""") + + +def flood_fill(grid, start_pos, max_depth): + queue = Queue() + queue.put((start_pos, max_depth)) + visited = set() + while not queue.empty(): + p, depth = queue.get(block=False) + for d in Grid.DIR4: + new_p = (p[0] + d[0], p[1] + d[1]) + if new_p not in visited and Grid.get(grid, new_p) == '.': + visited.add(new_p) + if depth > 1: + queue.put((new_p, depth - 1)) + return visited + + +inp = h.get_input_grid() + +starting_pos = None +for y, row in enumerate(inp): + if 'S' in row: + starting_pos = (y, row.index('S')) + inp[y][starting_pos[1]] = '.' + + +filled_cells = flood_fill(inp, starting_pos, max_depth=64) +valid_moves = [c for c in filled_cells if (starting_pos[0] + starting_pos[1] + c[0] + c[1]) % 2 == 0] + +h.submit(len(valid_moves)) diff --git a/solutions/2023_day22/part1.py b/solutions/2023_day22/part1.py new file mode 100644 index 0000000..ca4f353 --- /dev/null +++ b/solutions/2023_day22/part1.py @@ -0,0 +1,75 @@ +import copy + +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input=""" +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 +""") + + +def do_bricks_intersect(b1, b2): + for a in range(3): + b1_max = max(b1[0][a], b1[1][a]) + b1_min = min(b1[0][a], b1[1][a]) + b2_max = max(b2[0][a], b2[1][a]) + b2_min = min(b2[0][a], b2[1][a]) + if not (b1_min <= b2_max and b2_min <= b1_max): + return False + return True + + +inp = h.get_input_list() + + +falling_bricks = [] + +for line in inp: + raw_coords = line.split('~') + coords = [[int(n) for n in c.split(',')] for c in raw_coords] + falling_bricks.append(coords) + + +falling_bricks.sort(key=lambda b: min(b[0][2], b[1][2])) +print(falling_bricks) + +resting_bricks = [] +would_cause_fall = [] + +for falling_brick in falling_bricks: + while True: + if min(falling_brick[0][2], falling_brick[1][2]) == 1: + resting_bricks.append(falling_brick) + break + + next_pos = copy.deepcopy(falling_brick) + next_pos[0][2] -= 1 + next_pos[1][2] -= 1 + + supporting_bricks = [] + for resting_brick in reversed(resting_bricks): + if do_bricks_intersect(resting_brick, next_pos): + supporting_bricks.append(resting_brick) + + if supporting_bricks: + resting_bricks.append(falling_brick) + if len(supporting_bricks) == 1 and supporting_bricks[0] not in would_cause_fall: + would_cause_fall.append(supporting_bricks[0]) + break + + falling_brick = next_pos + + +print(resting_bricks) +print(would_cause_fall) + +h.submit(len(resting_bricks) - len(would_cause_fall)) diff --git a/solutions/2023_day22/part2.py b/solutions/2023_day22/part2.py new file mode 100644 index 0000000..3b3e4b8 --- /dev/null +++ b/solutions/2023_day22/part2.py @@ -0,0 +1,106 @@ +import copy +import queue +from collections import defaultdict + +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input=""" +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 +""") + + +def do_bricks_intersect(b1, b2): + for a in range(3): + b1_max = max(b1[0][a], b1[1][a]) + b1_min = min(b1[0][a], b1[1][a]) + b2_max = max(b2[0][a], b2[1][a]) + b2_min = min(b2[0][a], b2[1][a]) + if not (b1_min <= b2_max and b2_min <= b1_max): + return False + return True + + +inp = h.get_input_list() + + +falling_bricks = [] + +for line in inp: + raw_coords = line.split('~') + coords = [[int(n) for n in c.split(',')] for c in raw_coords] + falling_bricks.append(coords) + + +falling_bricks.sort(key=lambda b: min(b[0][2], b[1][2])) +print(falling_bricks) + +resting_bricks = [] +would_cause_fall = [] +supporters_graph = defaultdict(lambda: []) + +for falling_brick in falling_bricks: + while True: + if min(falling_brick[0][2], falling_brick[1][2]) == 1: + resting_bricks.append(falling_brick) + break + + next_pos = copy.deepcopy(falling_brick) + next_pos[0][2] -= 1 + next_pos[1][2] -= 1 + + supporting_bricks = [] + for resting_brick in reversed(resting_bricks): + if do_bricks_intersect(resting_brick, next_pos): + supporting_bricks.append(resting_brick) + + if supporting_bricks: + resting_bricks.append(falling_brick) + if len(supporting_bricks) == 1 and supporting_bricks[0] not in would_cause_fall: + would_cause_fall.append(supporting_bricks[0]) + + brick_tuple = tuple(tuple(c) for c in falling_brick) + supporters_graph[brick_tuple] = [tuple(tuple(c) for c in b) for b in supporting_bricks] + break + + falling_brick = next_pos + + +print(resting_bricks) +print(would_cause_fall) +print(supporters_graph) + +supporting_graph = defaultdict(lambda: []) +for brick, supporting_bricks in supporters_graph.items(): + for supporting_brick in supporting_bricks: + supporting_graph[supporting_brick].append(brick) +print(supporting_graph) + +total_falls = 0 +for disintegrated_brick in would_cause_fall: + has_fallen = defaultdict(lambda: False) + fall_queue = queue.Queue() + disintegrated_brick = tuple(tuple(c) for c in disintegrated_brick) + fall_queue.put(disintegrated_brick) + + while not fall_queue.empty(): + curr_brick = fall_queue.get() + if curr_brick == disintegrated_brick or all(has_fallen[b] for b in supporters_graph[curr_brick]): + has_fallen[curr_brick] = True + for next_brick in supporting_graph[curr_brick]: + fall_queue.put(next_brick) + + falls = list(has_fallen.values()).count(True) - 1 + print(disintegrated_brick, falls) + total_falls += falls + +h.submit(total_falls) diff --git a/solutions/2023_day23/part1.py b/solutions/2023_day23/part1.py new file mode 100644 index 0000000..eb47499 --- /dev/null +++ b/solutions/2023_day23/part1.py @@ -0,0 +1,84 @@ +import dataclasses +import queue + +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input=""" +#.##################### +#.......#########...### +#######.#########.#.### +###.....#.>.>.###.#.### +###v#####.#v#.###.#.### +###.>...#.#.#.....#...# +###v###.#.#.#########.# +###...#.#.#.......#...# +#####.#.#.#######.#.### +#.....#.#.#.......#...# +#.#####.#.#.#########v# +#.#...#...#...###...>.# +#.#.#v#######v###.###v# +#...#.>.#...>.>.#.###.# +#####v#.#.###v#.#.###.# +#.....#...#...#.#.#...# +#.#########.###.#.#.### +#...###...#...#...#.### +###.###.#.###v#####v### +#...#...#.#.>.>.#.>.### +#.###.###.#.###.#.#v### +#.....###...###...#...# +#####################.# +""") + + +SLOPE_DIR = { + '>': (0, 1), + '<': (0, -1), + '^': (-1, 0), + 'v': (1, 0) +} + + +@dataclasses.dataclass +class State: + pos: t.Tuple[int, int] + visited: set[t.Tuple[int, int]] + + +inp = h.get_input_grid() + +starting_pos = (0, inp[0].index('.')) +ending_pos = (len(inp) - 1, inp[-1].index('.')) + +q = queue.Queue() +q.put(State(starting_pos, set())) + +longest_path = 0 +while not q.empty(): + s: State = q.get() + if s.pos == ending_pos: + longest_path = max(longest_path, len(s.visited)) + continue + + for d in Grid.DIR4: + new_pos = (s.pos[0] + d[0], s.pos[1] + d[1]) + c = Grid.get(inp, new_pos) + if c == '#' or new_pos in s.visited: + continue + if c == '.': + new_visited = s.visited.copy() + new_visited.add(new_pos) + q.put(State(new_pos, new_visited)) + if c in SLOPE_DIR.keys(): + slope_dir = SLOPE_DIR[c] + if slope_dir[0] != -d[0] or slope_dir[1] != - d[1]: + new_new_pos = (new_pos[0] + slope_dir[0], new_pos[1] + slope_dir[1]) + new_visited = s.visited.copy() + new_visited.add(new_pos) + new_visited.add(new_new_pos) + q.put(State(new_new_pos, new_visited)) + +h.submit(longest_path) diff --git a/solutions/2023_day24/part1.py b/solutions/2023_day24/part1.py new file mode 100644 index 0000000..7cc0b22 --- /dev/null +++ b/solutions/2023_day24/part1.py @@ -0,0 +1,43 @@ +from util import * +from util import Grid as g +import itertools as it +import re + +h = Helper(test_mode=False, test_input=""" +19, 13, 30 @ -2, 1, -2 +18, 19, 22 @ -1, -1, -2 +20, 25, 34 @ -2, -2, -4 +12, 31, 28 @ -1, -2, -1 +20, 19, 15 @ 1, -5, -3 +""") + +inp = h.get_input_list() + +TEST_MIN = 200000000000000 +TEST_MAX = 400000000000000 + +hails = [] + +for line in inp: + line_split = line.split(' @ ') + pos = tuple(int(n) for n in line_split[0].split(', ')) + vel = tuple(int(n) for n in line_split[1].split(', ')) + hails.append((pos, vel)) + +total = 0 +for h1, h2 in it.combinations(hails, 2): + # print(h1, h2) + dx = h2[0][0] - h1[0][0] + dy = h2[0][1] - h1[0][1] + det = h2[1][0] * h1[1][1] - h2[1][1] * h1[1][0] + if det == 0: + continue + u = (dy * h2[1][0] - dx * h2[1][1]) / det + v = (dy * h1[1][0] - dx * h1[1][1]) / det + + total += u >= 0 and v >= 0 and \ + TEST_MIN <= h1[0][0] + h1[1][0] * u <= TEST_MAX and \ + TEST_MIN <= h1[0][1] + h1[1][1] * u <= TEST_MAX + + +h.submit(total) diff --git a/solutions/2023_day25/part1.py b/solutions/2023_day25/part1.py new file mode 100644 index 0000000..9a0d694 --- /dev/null +++ b/solutions/2023_day25/part1.py @@ -0,0 +1,66 @@ +import queue +from collections import defaultdict + +from pyvis.network import Network + +from util import * +from util import Grid as g +import itertools as it +import re + + +h = Helper(test_mode=False, test_input=""" + +""") + +inp = h.get_input_list() + +components = set() +edges = [] +for line in inp: + comp = line.split(': ')[0] + connections = line.split(': ')[1].split(' ') + components.add(comp) + for c in connections: + components.add(c) + edges.append((comp, c)) + +# net = Network() +# +# for comp in components: +# net.add_node(comp, comp) +# +# for e in edges: +# net.add_edge(*e) +# +# net.show('mygraph.html', notebook=False) + + +IGNORED_EDGES = [("lxt", "lsv"), ("dhn", "xvh"), ("ptj", "qmr")] + + +q = queue.Queue() +q.put(components.copy().pop()) +visited = set() + +edges_dict = defaultdict(lambda: []) + +for e in edges: + if e in IGNORED_EDGES or (e[1], e[0]) in IGNORED_EDGES: + print(e) + continue + edges_dict[e[0]].append(e[1]) + edges_dict[e[1]].append(e[0]) + +while not q.empty(): + comp = q.get() + if comp in visited: + continue + visited.add(comp) + for c in edges_dict[comp]: + q.put(c) + +group_size = len(visited) +other_size = len(components) - group_size +print(group_size, other_size) +h.submit(group_size * other_size) diff --git a/util.py b/util.py index 50d06a2..54927a4 100644 --- a/util.py +++ b/util.py @@ -99,11 +99,13 @@ def submit(self, answer=None, part=None, confirm_answer=True): attempts = [] violates_feedback = False - for attempt, feedback in attempts: - if (feedback == "low" and answer <= attempt) or (feedback == "high" and answer >= attempt): - print(f"Answer {answer} violates previous feedback: {attempt} is too {feedback}.") + for attempt in attempts: + attempt_value = attempt["value"] + attempt_feedback = attempt["feedback"] + if (attempt_feedback == "low" and answer <= attempt_value) or (attempt_feedback == "high" and answer >= attempt_value): + print(f"Answer {answer} violates previous feedback: {attempt_value} is too {attempt_feedback}.") violates_feedback = True - elif feedback != "correct" and answer == attempt: + elif attempt_feedback != "correct" and answer == attempt_value: print(f"Answer {answer} was already submitted and is incorrect.") violates_feedback = True