Skip to main content

year2019/
day10.rs

1use utils::geometry::Vec2;
2use utils::grid;
3use utils::number::gcd;
4use utils::prelude::*;
5
6/// Ordering visible grid points in a rotational sweep.
7#[derive(Clone, Debug)]
8pub struct Day10 {
9    asteroids: Vec<Vec2<i16>>,
10    station: usize,
11    part1: u32,
12}
13
14impl Day10 {
15    pub fn new(input: &str, _: InputType) -> Result<Self, InputError> {
16        let (rows, cols, grid) = grid::parse(
17            input,
18            0,
19            false,
20            |b| b == b'#',
21            |b| matches!(b, b'.' | b'#'),
22            |_, _| Err("expected '.' or '#'"),
23        )?;
24        if rows > i16::MAX as usize || cols > i16::MAX as usize {
25            return Err(InputError::new(input, 0, "grid too large"));
26        }
27
28        let asteroids: Vec<Vec2<i16>> = grid
29            .iter()
30            .enumerate()
31            .filter(|&(_, &cell)| cell)
32            .map(|(index, _)| Vec2::new((index % cols) as i16, (index / cols) as i16))
33            .collect();
34        if asteroids.is_empty() {
35            return Err(InputError::new(input, 0, "expected at least one asteroid"));
36        }
37
38        let delta_rows = 2 * rows - 1;
39        let delta_cols = 2 * cols - 1;
40        let delta_index = |delta: Vec2<i16>| {
41            (delta.y as i32 + rows as i32 - 1) as usize * delta_cols
42                + (delta.x as i32 + cols as i32 - 1) as usize
43        };
44
45        let mut reduced_cache = vec![u32::MAX; delta_rows * delta_cols];
46        let mut seen = vec![0u32; reduced_cache.len()];
47        let mut visible = vec![0u32; asteroids.len()];
48
49        for i in 0..asteroids.len() {
50            for j in i + 1..asteroids.len() {
51                let delta = asteroids[j] - asteroids[i];
52                let index = delta_index(delta);
53
54                // reduced_cache[delta_index(delta)] caches the gcd reduced direction index for each
55                // delta to avoid recalculating the same gcd multiple times.
56                let reduced_index = if reduced_cache[index] == u32::MAX {
57                    let reduced = delta / gcd(delta.x, delta.y).abs();
58                    let reduced_index = delta_index(reduced);
59                    reduced_cache[index] = reduced_index as u32;
60                    reduced_index
61                } else {
62                    reduced_cache[index] as usize
63                };
64
65                // seen[reduced_index] is set to i + 1 each time a reduced direction is first
66                // seen for asteroid i.
67                // Iterating through asteroids > i in grid index order means that the first asteroid
68                // j seen in each direction is the closest.
69                // This allows just visiting each pair of asteroids once and incrementing the
70                // visible count for both asteroids at once.
71                if seen[reduced_index] <= i as u32 {
72                    seen[reduced_index] = i as u32 + 1;
73                    visible[i] += 1;
74                    visible[j] += 1;
75                }
76            }
77        }
78
79        let (best_station, part1) = visible
80            .into_iter()
81            .enumerate()
82            .max_by_key(|&(_, visible)| visible)
83            .unwrap();
84
85        Ok(Self {
86            asteroids,
87            station: best_station,
88            part1,
89        })
90    }
91
92    #[must_use]
93    pub fn part1(&self) -> u32 {
94        self.part1
95    }
96
97    #[must_use]
98    pub fn part2(&self) -> u32 {
99        struct Target {
100            dir: Vec2<i16>,
101            dir_multiple: i16,
102            pos: Vec2<i16>,
103        }
104
105        assert!(
106            self.asteroids.len() > 200,
107            "expected there to be at least 201 asteroids"
108        );
109
110        let station_position = self.asteroids[self.station];
111        let mut targets: Vec<Target> = self
112            .asteroids
113            .iter()
114            .enumerate()
115            .filter(|&(i, _)| i != self.station)
116            .map(|(_, &position)| {
117                let delta = position - station_position;
118                let multiple = gcd(delta.x, delta.y).abs();
119                Target {
120                    dir: delta / multiple,
121                    dir_multiple: multiple,
122                    pos: position,
123                }
124            })
125            .collect();
126
127        // Sort the targets by (angle, distance)
128        targets.sort_unstable_by(|a, b| {
129            Self::quadrant(a.dir)
130                .cmp(&Self::quadrant(b.dir))
131                .then_with(|| {
132                    // Within each quadrant, cross > 0 means the angle comes first clockwise
133                    let cross = i32::from(a.dir.x) * i32::from(b.dir.y)
134                        - i32::from(a.dir.y) * i32::from(b.dir.x);
135                    0.cmp(&cross)
136                })
137                .then(a.dir_multiple.cmp(&b.dir_multiple))
138        });
139
140        // After sorting by (angle, distance), calculate the pass each target will be vaporized on
141        // based on how many closer asteroids have the same angle.
142        // Ordering by (pass, index sorted by angle) gives the final vaporization order.
143        let mut target_keys = Vec::with_capacity(targets.len());
144        let mut pass = 0u32;
145        for (i, target) in targets.iter().enumerate() {
146            if i > 0 && target.dir == targets[i - 1].dir {
147                pass += 1;
148            } else {
149                pass = 0;
150            }
151            target_keys.push((pass, i));
152        }
153
154        let (_, &mut (_, index), _) = target_keys.select_nth_unstable(199);
155        let position = targets[index].pos;
156        (position.x as u32) * 100 + position.y as u32
157    }
158
159    fn quadrant(direction: Vec2<i16>) -> u8 {
160        match (direction.x, direction.y) {
161            (0.., ..0) => 0, // top-right, including up
162            (1.., 0..) => 1, // bottom-right, including right
163            (..1, 1..) => 2, // bottom-left, including down
164            (..0, ..1) => 3, // top-left, including left
165            (0, 0) => unreachable!("expected non-zero direction"),
166        }
167    }
168}
169
170examples!(Day10 -> (u32, u32) [
171    {file: "day10_example0.txt", part1: 8},
172    {file: "day10_example1.txt", part1: 33},
173    {file: "day10_example2.txt", part1: 35},
174    {file: "day10_example3.txt", part1: 41},
175    {file: "day10_example4.txt", part1: 210, part2: 802},
176]);