Skip to main content

aoc/year2021/
day17.rs

1//! # Trick Shot
2//!
3//! Although this problem is easy to brute force, we can apply some reasoning and simplify both
4//! parts.
5//!
6//! ## Part One
7//! Part one can be solved analytically. Movement upwards in the positive y direction is
8//! symmetrical. For example, launching a probe at a y-velocity of 5 initially,
9//! would result in a speed and y-position:
10//!
11//! ```text
12//!     Time:       0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12
13//!     Speed:      5,  4,  3,  2,  1,  0, -1, -2, -3, -4, -5, -6, -7
14//!     Y-Position: 0,  5,  9, 12, 14, 15, 15, 14, 12,  9,  5,  0, -6
15//! ```
16//!
17//! The maximum y velocity is reached when we *just* touch the target area on the way down at the
18//! bottom y-coordinate. For the example above, if the bottom y coordinate was -6 then the maximum
19//! initial upwards velocity is one less, our starting velocity of 5.
20//!
21//! The maximum height is `5 + 4 + 3 + 2 + 1`, which is the sum from 1 to n given by the formula for
22//! triangular numbers [`(n * (n + 1) / 2`](https://en.wikipedia.org/wiki/Triangular_number#Formula).
23//!
24//! ## Part Two
25//! A brute force solution would check every possible combination of `x` and `y` for a total
26//! complexity of `O(xy)`. By thinking in terms of time `t` instead and applying a dynamic
27//! programming solution we can instead solve in a complexity of `O(x + y)` by treating `x` and `y`
28//! independently.
29//!
30//! We create 2 `vecs`. The first `new` counts how many x-velocity values enter the target area at
31//! time `t` for the first time, only considering horizontal movement. The second `continuing`
32//! counts how many are still in the target area at time `t`.
33//!
34//! For example, using the sample `target area: x=20..30, y=-10..-5` gives a progression:
35//!
36//! ```text
37//!     X-Velocity : 6
38//!     Time:        0,  1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
39//!     New:         0,  0, 0, 0, 0, 1, 0, 0, 0, 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0
40//!     Continuing:  0,  0, 0, 0, 0, 0, 1, 1, 1, 1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1
41//!
42//!     X-Velocity : 7
43//!     Time:        0,  1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
44//!     New:         0,  0, 0, 0, 1, 1, 0, 0, 0, 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0
45//!     Continuing:  0,  0, 0, 0, 0, 1, 2, 2, 2, 2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2
46//!
47//!     X-Velocity : 8
48//!     Time:        0,  1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
49//!     New:         0,  0, 0, 1, 1, 1, 0, 0, 0, 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0
50//!     Continuing:  0,  0, 0, 0, 1, 2, 2, 2, 2, 2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2
51//!
52//!     ...
53//!
54//!     X-Velocity : 30
55//!     Time:        0,  1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
56//!     New:         0, 11, 5, 3, 1, 1, 0, 0, 0, 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0
57//!     Continuing:  0,  0, 0, 1, 2, 2, 2, 2, 2, 2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2
58//! ```
59//!
60//! Then for each y velocity value we find the time when it enters the target area. The first time
61//! this happens we add *both* `new` and `continuing` to the total. For subsequent times while we're
62//! still in the target area we add only the `new` values, as the `continuing` are trajectories
63//! that we've already considered. For example, for an initial y-velocity of 0:
64//!
65//! ```text
66//!     Time:       0,   1,   2,     3,   4
67//!     Speed:      0,  -1,  -2,    -3,  -4
68//!     Y-Position: 0,  -1,  -3,    -6, -10
69//!     Total:      0,   0,   0, 3 + 1,   5
70//! ```
71//!
72//! Summing this for all y-velocity values gives the desired result.
73use crate::util::iter::*;
74use crate::util::parse::*;
75
76type Input = [i32; 4];
77
78pub fn parse(input: &str) -> Input {
79    input.iter_signed().chunk::<4>().next().unwrap()
80}
81
82pub fn part1(input: &Input) -> i32 {
83    let &[_, _, bottom, _] = input;
84    let n = -(bottom + 1);
85    n * (n + 1) / 2
86}
87
88pub fn part2(input: &Input) -> usize {
89    let &[left, right, bottom, top] = input;
90
91    // Find minimum dx where triangular number reaches left boundary.
92    let min_dx = (1..left).find(|&n| n * (n + 1) / 2 >= left).unwrap();
93    let max_dx = right + 1;
94    let min_dy = bottom;
95    let max_dy = -bottom;
96
97    let max_t = (1 - 2 * bottom) as usize;
98    let mut new = vec![0; max_t];
99    let mut continuing = vec![0; max_t];
100    let mut total = 0;
101
102    for mut dx in min_dx..max_dx {
103        let mut x = 0;
104        let mut first = true;
105
106        for t in 0..max_t {
107            if x > right {
108                break;
109            }
110            if x >= left {
111                if first {
112                    first = false;
113                    new[t] += 1;
114                } else {
115                    continuing[t] += 1;
116                }
117            }
118            x += dx;
119            dx = (dx - 1).max(0);
120        }
121    }
122
123    for mut dy in min_dy..max_dy {
124        let mut y = 0;
125        let mut t = 0;
126        // Skip the positive arc, assuming top is negative.
127        if dy > 0 {
128            t = 2 * dy as usize + 1;
129            dy = -dy - 1;
130        }
131        let mut first = true;
132
133        while y >= bottom {
134            if y <= top {
135                if first {
136                    first = false;
137                    total += continuing[t];
138                }
139                total += new[t];
140            }
141            y += dy;
142            dy -= 1;
143            t += 1;
144        }
145    }
146
147    total
148}