advent_of_code_2021

My attempts to work through the 2021 Advent of Code problems.
git clone https://git.eamoncaddigan.net/advent_of_code_2021.git
Log | Files | Refs | README | LICENSE

day17_part2.py (4514B)


      1 #!/usr/bin/env python
      2 """Advent of Code 2021, day 17 (part 2): Trick Shot
      3 Find the starting x and y velocities that end inside a target area"""
      4 
      5 # No part 1 file for today; I solved it with my phone calculator while I was
      6 # feeding my baby. Once I realized that any positive y velocity of _n_ would
      7 # have a velocity of -n when it returned to the vertical position y=0, it was
      8 # easy to work out without code.
      9 #
     10 # I am coding part 2, but it seems like a simple problem so late in the AoC. I
     11 # will admit that I made one naive assumption that was incorrect: since the
     12 # updating process for x and y are independent, I thought that x and y would be
     13 # independent for the solution, but I didn't consider that both the x and y
     14 # position need to be inside the target area _on the same step_. Oops.
     15 #
     16 # The initial x velocity is bounded by that which will stop moving forward
     17 # immediately upon reaching the trench, and that which finishes its first step
     18 # at the far end of the trench. The initial y velocity is bounded by the one
     19 # that solved part 1, and that which finishes its first step at the low end of
     20 # the trench. I'll use more lax bounds than this because it's so quick it
     21 # doesn't hurt.
     22 
     23 from typing import List, Tuple
     24 import re
     25 from utils import get_puzzle_input
     26 
     27 EXAMPLE_INPUT = \
     28 """target area: x=20..30, y=-10..-5
     29 """
     30 
     31 def parse_input(input_string: str) -> Tuple[Tuple[int, int], Tuple[int, int]]:
     32     """Given the puzzle input, return the min and max of x and the min and max
     33     of y as tuples"""
     34     match = re.match(r"^target area: x=(?P<x1>\d+)..(?P<x2>\d+), " + \
     35                      r"y=(?P<y1>-?\d+)..(?P<y2>-?\d+)",
     36                      input_string)
     37     if match is None:
     38         raise RuntimeError("improperly foratted input string")
     39 
     40     return (tuple(sorted([int(x) for x in match.group('x1', 'x2')])),
     41             tuple(sorted([int(x) for x in match.group('y1', 'y2')])))
     42 
     43 def update_x(x_position: int, x_velocity: int) -> Tuple[int, int]:
     44     """Given an x position and velocity, return an updated position and
     45     velocity"""
     46     # x position increases by velocity and the velocity increments or
     47     # decrements by 1 toward 0. This code is a little hacky, but the expression
     48     # `(n>0)*2-1` evaluates to 1 if n is positive, and -1 if it's negative,
     49     # providing a concise `sign` function.
     50     return (x_position + x_velocity,
     51             ((x_velocity > 0) * 2 - 1) * max(0, abs(x_velocity) - 1))
     52 
     53 def update_y(y_position: int, y_velocity: int) -> Tuple[int, int]:
     54     """Given a y position and velocity, return an updated position and
     55     velocity"""
     56     # y position increases by velocity and the velocity decreases by 1
     57     return (y_position + y_velocity, y_velocity - 1)
     58 
     59 # XXX: These are pretty hacky in that they assume that x will be entirely
     60 # greater than 0 and y will be entirely less than 0.
     61 
     62 def test_xy(x_velocity: int, y_velocity: int,
     63             x_bounds: Tuple[int, int], y_bounds: Tuple[int, int]) -> bool:
     64     """Return True iff a probe with initial starting position of (0, 0), and
     65     velocity vector of (`x_velocity`, `y_velocity`) would finish any step
     66     inside the area bounded by `x_bound` and `y_bounds`."""
     67     x_position, y_position = (0, 0)
     68     while x_position <= x_bounds[1] and y_position >= y_bounds[0]:
     69         if x_bounds[0] <= x_position <= x_bounds[1] and \
     70                 y_bounds[0] <= y_position <= y_bounds[1]:
     71             return True
     72         x_position, x_velocity = update_x(x_position, x_velocity)
     73         y_position, y_velocity = update_y(y_position, y_velocity)
     74     return False
     75 
     76 def find_xy(x_bounds: Tuple[int, int],
     77             y_bounds: Tuple[int, int]) -> List[Tuple[int, int]]:
     78     """Given the specified boundaries, return a list of all the starting x and
     79     y velocities that would finish a step inside of them"""
     80     in_bounds_velocities = []
     81     for x_velocity in range(x_bounds[1]+1):
     82         for y_velocity in range(y_bounds[0], -y_bounds[0]+1):
     83             if test_xy(x_velocity, y_velocity, x_bounds, y_bounds):
     84                 in_bounds_velocities.append((x_velocity, y_velocity))
     85     return in_bounds_velocities
     86 
     87 def solve_puzzle(input_string: str) -> int:
     88     """Return the numeric solution to the puzzle"""
     89     return len(find_xy(*parse_input(input_string)))
     90 
     91 def main() -> None:
     92     """Run when the file is called as a script"""
     93     assert solve_puzzle(EXAMPLE_INPUT) == 112
     94     print("Total distinct initial velocities:",
     95           solve_puzzle(get_puzzle_input(17)))
     96 
     97 if __name__ == "__main__":
     98     main()