diff --git a/src/adventofcode/year_2023/day_06_2023.py b/src/adventofcode/year_2023/day_06_2023.py index ec6eb67..6c8fe4e 100644 --- a/src/adventofcode/year_2023/day_06_2023.py +++ b/src/adventofcode/year_2023/day_06_2023.py @@ -1,3 +1,4 @@ +import math import re from itertools import starmap from math import prod @@ -17,6 +18,16 @@ def parse_input(data: list[str]) -> list[tuple[int, int]]: return list(zip(times, distances, strict=True)) +def parse_input_part_two(data: list[str]) -> tuple[int, int]: + """ + Parse input to a single pair of duration and distance + """ + pattern = re.compile("\\d+") + duration = int("".join(pattern.findall(data[0]))) + distance = int("".join(pattern.findall(data[1]))) + return duration, distance + + def calculate_distance(hold: int, max_time: int) -> int: """ Calculate the distance you can travel within the max limit @@ -38,6 +49,19 @@ def calculate_ways_to_win(duration: int, farthest_distance: int) -> int: return len(list(winning_distances)) +def calculate_ways_to_win_part_two(duration: int, farthest_distance: int) -> int: + """ + Calculate ways to beat the current farthest distance + """ + possibilities = zip(range(duration), [duration] * duration, strict=True) + winning_distances: int = 0 + for possibility in possibilities: + if calculate_distance(*possibility) > farthest_distance: + winning_distances += 1 + + return winning_distances + + def calculate_margin_of_error(data: list[str]) -> int: """ Calculate the margin of error @@ -46,6 +70,44 @@ def calculate_margin_of_error(data: list[str]) -> int: return prod(starmap(calculate_ways_to_win, parsed_input)) +def calculate_margin_of_error_part_two(data: list[str]) -> int: + """ + Calculate the margin of error + """ + parsed_input = parse_input_part_two(data) + return calculate_ways_to_win(*parsed_input) + + +def calculate_margin_of_error_quadratic(data: list[str]) -> int: + """ + I suspected that the outcome of calculate_distance for x was a parabola, + when running calculate distance for x in range(12) with max time of 12 the values were: + 0, 11, 20, 27, 32, 35, 32, 27, 20, 11, 0. + + We can use a quadratic equation to solve the problem. + By first calculating the discriminant and then the upper and lower bound. + + After calculating the bounds, we can add/subtract from that value until the + distance is no longer a winning distance, each time it is, add 1 to total + """ + duration, distance = parse_input_part_two(data) + discriminant = duration * duration - 4 * distance + lower_bound = math.ceil((-duration + math.sqrt(discriminant)) / - 2) + upper_bound = math.floor((-duration - math.sqrt(discriminant)) / - 2) + + total = 0 + + while calculate_distance(lower_bound - 1, duration) > distance: + lower_bound = lower_bound - 1 + total += 1 + + while calculate_distance(upper_bound + 1, duration) > distance: + upper_bound = upper_bound + 1 + total += 1 + + return upper_bound - lower_bound + 1 + + @register_solution(2023, 6, 1) def part_one(input_data: list[str]): answer = calculate_margin_of_error(input_data) @@ -58,7 +120,27 @@ def part_one(input_data: list[str]): @register_solution(2023, 6, 2) def part_two(input_data: list[str]): - answer = ... + answer = calculate_margin_of_error_part_two(input_data) + + if not answer: + raise SolutionNotFoundError(2023, 6, 2) + + return answer + + +@register_solution(2023, 6, 2, version="quadratic") +def part_two_quadratic(input_data: list[str]): + answer = calculate_margin_of_error_quadratic(input_data) + + if not answer: + raise SolutionNotFoundError(2023, 6, 2) + + return answer + + +@register_solution(2023, 6, 2, version="quadratic") +def part_two_quadratic(input_data: list[str]): + answer = calculate_margin_of_error_quadratic(input_data) if not answer: raise SolutionNotFoundError(2023, 6, 2) @@ -70,3 +152,4 @@ def part_two(input_data: list[str]): data = get_input_for_day(2023, 6) part_one(data) part_two(data) + part_two_quadratic(data) diff --git a/tests/adventofcode/year_2023/test_day_06_2023.py b/tests/adventofcode/year_2023/test_day_06_2023.py index 029222f..8bbf9d6 100644 --- a/tests/adventofcode/year_2023/test_day_06_2023.py +++ b/tests/adventofcode/year_2023/test_day_06_2023.py @@ -5,7 +5,7 @@ calculate_ways_to_win, parse_input, part_one, - part_two, + part_two, parse_input_part_two, calculate_ways_to_win_part_two, ) test_input = [ @@ -18,6 +18,10 @@ def test_parse_input(): assert parse_input(test_input) == [(7, 9), (15, 40), (30, 200)] +def test_parse_input_part_two(): + assert parse_input_part_two(test_input) == (71530, 940200) + + @pytest.mark.parametrize( ["hold", "max_time", "expected"], [ @@ -47,9 +51,13 @@ def test_calculate_ways_to_win(duration, farthest_distance, expected): assert calculate_ways_to_win(duration, farthest_distance) == expected +def test_calculate_ways_to_win_part_two(): + assert calculate_ways_to_win_part_two(71530, 940200) == 71503 + + def test_part_one(): assert part_one(test_input) == 288 def test_part_two(): - assert part_two(test_input) == "x" + assert part_two(test_input) == 71503