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()