Skip to content

Commit c46d358

Browse files
committed
Use better slope estimate and geometry for less intcode
Cut the number of test() runs of intcode (on my input) from 453 to just 145, by improving parse to come up with a more accurate slope estimate in fewer probes, for fewer false positives in part 1, and then by using that more accurate slope to start part 2 much closer to the actual answer. On my laptop, this cuts parse/part1/part2 runtime from 200/215/540us to 97/199/42us.
1 parent a07ef32 commit c46d358

1 file changed

Lines changed: 72 additions & 22 deletions

File tree

src/year2019/day19.rs

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,89 @@
11
//! # Tractor Beam
22
//!
3-
//! Finds the approximate boundary of the upper and lower edges of the beam expressed as a slope.
4-
//! We then skip the relatively expensive intcode test if the x and y coordinates lie outside.
3+
//! The intcode program computes a linear inequality: returning true if an integer point lies on
4+
//! or between two lines with irrational slope. The intcode program was designed so that the two
5+
//! lines are narrow enough that there are no integer solutions when `y=1`, so there are
6+
//! intentionally one or two discontinuities between the origin and the bulk of the beam. This
7+
//! solution finds the approximate boundary of the upper and lower edges of the beam expressed
8+
//! as an integer ratio for slope. We then skip the relatively expensive intcode test if the x
9+
//! 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. If we consider a scaled-down beam with target point A on slope
14+
//! upper as (x1, y1), and point B on slope lower as (x2, y2), the bounding box has a top left
15+
//! corner at point S at (x2, y1):
16+
//! ```none
17+
//! 1
18+
//! 11
19+
//! 111
20+
//! 1111
21+
//! 11111
22+
//! 111111
23+
//! 1111111
24+
//! 11111111
25+
//! 111SxxA11 <--- (x1, y1)
26+
//! 11xxxx1111
27+
//! 1xxxx111111
28+
//! (x2, y2)> Bxxx11111111
29+
//! 1111111111111
30+
//! ```
31+
//!
32+
//! For a 100x100 box, we have a system of equations x1=m1≈upper/scale, x1=x2+99, y2=m2≈scale/lower,
33+
//! and y2=y1+99, which we solve as:
34+
//! ```none
35+
//! x2 = ((m1 * 99 + 99) / (m2 - m1)
36+
//! ≈ ((upper/scale * 99) + 99) / (scale/lower - upper/scale)
37+
//! ≈ ((99*upper/scale) + (99*scale/scale)) / ((scale*scale/scale*lower - lower*upper/scale*lower))
38+
//! ≈ ((99 * (upper + scale)) * (scale*lower)) / (scale * (scale*scale - lower*upper))
39+
//! ≈ (99*lower * (upper + scale)) / (scale*scale - lower*upper)
40+
//! y1 = m2 * x2 - 99
41+
//! ≈ scale*x2 / lower - 99
42+
//! ```
543
use super::intcode::*;
644
use crate::util::parse::*;
745

846
pub struct Input {
947
code: Vec<i64>,
10-
lower: i64,
11-
upper: i64,
48+
scale: i64,
49+
lower: i64, // slope scale/lower just outside left boundary
50+
upper: i64, // slope upper/scale just outside right boundary
1251
}
1352

1453
pub fn parse(input: &str) -> Input {
54+
// Pick an initial scale large enough to be past the discontinuities for all known inputs.
1555
let code: Vec<_> = input.iter_signed().collect();
16-
let mut lower = 0;
17-
let mut upper = 0;
56+
let mut lower = 1;
57+
let mut upper = 1;
58+
let mut scale = 5;
1859

19-
// Find slope of lower and upper edges, rounding down to prevent false negatives.
20-
while !test(&code, lower + 1, 50) {
21-
lower += 1;
22-
}
23-
while !test(&code, 50, upper + 1) {
24-
upper += 1;
60+
// Find approximate slope of lower and upper edges, rounding down to prevent false negatives.
61+
// Scale the boundary for slightly more accuracy.
62+
while scale < 1024 {
63+
scale *= 2;
64+
lower *= 2;
65+
upper *= 2;
66+
while !test(&code, lower + 1, scale) {
67+
lower += 1;
68+
}
69+
while !test(&code, scale, upper + 1) {
70+
upper += 1;
71+
}
2572
}
2673

27-
Input { code, lower, upper }
74+
Input { code, scale, lower, upper }
2875
}
2976

3077
pub fn part1(input: &Input) -> i64 {
3178
let code = &input.code;
32-
// Handle origin specially.
33-
let mut result = test(code, 0, 0) as i64;
79+
// The origin is always set, and no other point on that row.
80+
let mut result = 1;
3481

35-
// The beam is continuous so we only need to find the left and right edges.
36-
for y in 0..50 {
37-
let left = (0..50).find(|&x| precheck(input, x, y) && test(code, x, y));
38-
let right = (0..50).rfind(|&x| precheck(input, x, y) && test(code, x, y));
82+
// Scanning the remaining lines works even around the known discontinuity at y=1, by finding
83+
// the left and right edges if any.
84+
for y in 2..50 {
85+
let left = (1..50).find(|&x| precheck(input, x, y) && test(code, x, y));
86+
let right = (1..50).rfind(|&x| precheck(input, x, y) && test(code, x, y));
3987
if let (Some(l), Some(r)) = (left, right) {
4088
result += r - l + 1;
4189
}
@@ -45,9 +93,11 @@ pub fn part1(input: &Input) -> i64 {
4593
}
4694

4795
pub fn part2(input: &Input) -> i64 {
96+
// See comments above about derivation of initial guess for x and y.
4897
let code = &input.code;
49-
let mut x = 0;
50-
let mut y = 0;
98+
let mut x = 99 * input.lower * (input.upper + input.scale)
99+
/ (input.scale * input.scale - input.lower * input.upper);
100+
let mut y = input.scale * x / input.lower - 99;
51101
let mut moved = true;
52102

53103
// Increase the right and bottom edges of our box until they are both inside the beam.
@@ -70,7 +120,7 @@ pub fn part2(input: &Input) -> i64 {
70120

71121
/// Quick check with some false positives but no false negatives.
72122
fn precheck(input: &Input, x: i64, y: i64) -> bool {
73-
50 * y > input.upper * x && 50 * x > input.lower * y
123+
input.scale * y > input.upper * x && input.scale * x > input.lower * y
74124
}
75125

76126
/// Definitive but slower check.

0 commit comments

Comments
 (0)