Skip to main content

aoc/year2019/
day19.rs

1//! # Tractor Beam
2//!
3//! The intcode program computes a linear inequality: returning true if an integer point lies on
4//! or between two lines through the origin, often with irrational slope. The intcode program was
5//! designed so that the two lines are close enough that there are no integer solutions when `y=1`,
6//! so there are intentionally one or two discontinuities between the origin and the bulk of the
7//! beam. This solution finds the approximate boundary of the upper and lower edges of the beam
8//! expressed as an integer ratio for slope. We then skip the relatively expensive intcode test if
9//! the x and y coordinates lie outside. Once we identify an edge past the initial discontinuities,
10//! scaling along the lines buys more accuracy and thus fewer later intcode runs.
11//!
12//! For part 2, we can further speed up the process by using geometry to hone in on a viable
13//! target to start searching at. Our target point `(x,y)` is related to our two slopes as:
14//! ```none
15//!   scale*y = upper*(x+99)
16//!   scale*x = lower*(y+99)
17//! ```
18//! Those two equations can be represented in matrix form:
19//! ```none
20//!   [upper-scale][x] = [-99*upper]
21//!   [scale-lower][y] = [ 99*lower]
22//! ```
23//! where inverting the matrix gives a solution:
24//! ```none
25//!   determinant = scale * scale - lower * upper
26//!   x = 99 * (lower * upper + lower * scale) / determinant
27//!   y = 99 * (lower * upper + upper * scale) / determinant
28//! ```
29use super::intcode::*;
30use crate::util::parse::*;
31
32pub struct Input {
33    code: Vec<i64>,
34    scale: i64,
35    lower: i64, // The slope scale/lower just outside left boundary.
36    upper: i64, // The slope upper/scale just outside right boundary.
37}
38
39pub fn parse(input: &str) -> Input {
40    // Pick an initial scale large enough to be past the discontinuities for all known inputs.
41    let code: Vec<_> = input.iter_signed().collect();
42    let mut lower = 1;
43    let mut upper = 1;
44    let mut scale = 5;
45
46    // Find approximate slope of lower and upper edges, rounding down to prevent false negatives.
47    // Each scaling iteration adds another bit of accuracy to our approximation.
48    while scale < 1024 {
49        scale *= 2;
50        lower *= 2;
51        upper *= 2;
52        while !test(&code, lower + 1, scale) {
53            lower += 1;
54        }
55        while !test(&code, scale, upper + 1) {
56            upper += 1;
57        }
58    }
59
60    Input { code, scale, lower, upper }
61}
62
63pub fn part1(input: &Input) -> i64 {
64    // The origin is always set, but no other point occurs on that row or column.
65    let mut result = 1;
66
67    // Scan all remaining points; this works even on lines with no integer hits.
68    for y in 1..50 {
69        let left = (1..50).find(|&x| inside(input, x, y));
70        let right = (left.unwrap_or(50)..50).rfind(|&x| inside(input, x, y));
71        if let Some((left, right)) = left.zip(right) {
72            result += right - left + 1;
73        }
74    }
75
76    result
77}
78
79pub fn part2(input: &Input) -> i64 {
80    // See comments above about derivation of initial guess for x and y.
81    let determinant = input.scale * input.scale - input.lower * input.upper;
82    let mut x = 99 * (input.lower * input.upper + input.lower * input.scale) / determinant;
83    let mut y = 99 * (input.lower * input.upper + input.upper * input.scale) / determinant;
84    let mut moved = true;
85
86    // Increase the right and bottom edges of our box until they are both inside the beam.
87    while moved {
88        moved = false;
89
90        while !inside(input, x, y + 99) {
91            x += 1;
92            moved = true;
93        }
94
95        while !inside(input, x + 99, y) {
96            y += 1;
97            moved = true;
98        }
99    }
100
101    10000 * x + y
102}
103
104/// Skip the relatively expensive intcode test if the point lies outside the beam's slopes.
105/// The slope check has some false positives but no false negatives.
106fn inside(input: &Input, x: i64, y: i64) -> bool {
107    input.scale * y > input.upper * x
108        && input.scale * x > input.lower * y
109        && test(&input.code, x, y)
110}
111
112/// Definitive but slower check.
113fn test(code: &[i64], x: i64, y: i64) -> bool {
114    let mut computer = Computer::new(code);
115    computer.input(x);
116    computer.input(y);
117
118    let State::Output(result) = computer.run() else { unreachable!() };
119    result == 1
120}