|
| 1 | +use std::{collections::HashMap, hash::Hash, num::ParseIntError, str::FromStr}; |
| 2 | + |
| 3 | +use itertools::Itertools; |
| 4 | +use thiserror::Error; |
| 5 | + |
| 6 | +pub fn solve(input: &str) -> (usize, usize) { |
| 7 | + let robots: Vec<_> = input.lines().map(|l| l.parse().unwrap()).collect(); |
| 8 | + (part1(&robots), part2(robots).unwrap()) |
| 9 | +} |
| 10 | + |
| 11 | +type Coord = i16; |
| 12 | +type Pos = [Coord; 2]; |
| 13 | +type Vel = [Coord; 2]; |
| 14 | + |
| 15 | +const EXTENT: Pos = [101, 103]; |
| 16 | + |
| 17 | +fn part1(robots: &[Robot]) -> usize { |
| 18 | + robots |
| 19 | + .into_iter() |
| 20 | + .map(|r| r.evolved_by(100).pos) |
| 21 | + .filter_map(|p| quadrant(p)) |
| 22 | + .counted() |
| 23 | + .into_values() |
| 24 | + .product() |
| 25 | +} |
| 26 | + |
| 27 | +fn part2(mut robots: Vec<Robot>) -> Option<usize> { |
| 28 | + let (mut min_non_symmetric, mut t_non_symmetric) = (robots.len(), 0); |
| 29 | + for i in 0..101 * 103 { |
| 30 | + let non_symmetric = count_non_symmetric(&robots); |
| 31 | + if non_symmetric < min_non_symmetric { |
| 32 | + min_non_symmetric = non_symmetric; |
| 33 | + t_non_symmetric = i; |
| 34 | + } |
| 35 | + robots = robots.into_iter().map(|r| r.evolved_by(1)).collect(); |
| 36 | + } |
| 37 | + Some(t_non_symmetric) |
| 38 | +} |
| 39 | + |
| 40 | +fn count_non_symmetric(robots: &[Robot]) -> usize { |
| 41 | + // Assumption: A christmas tree has symmetry around some vertical line |
| 42 | + |
| 43 | + // first: decide center by repeatedly taking the average of a contracting set |
| 44 | + let mut x_center = 0; |
| 45 | + for n in [500, 350, 200] { |
| 46 | + x_center = (robots |
| 47 | + .iter() |
| 48 | + .map(|r| r.pos[0]) |
| 49 | + .sorted_by_key(|x| (x - x_center).abs()) |
| 50 | + .take(n) |
| 51 | + .map(|x| x as f64) |
| 52 | + .sum::<f64>() |
| 53 | + / n as f64) |
| 54 | + .round() as i16; |
| 55 | + } |
| 56 | + |
| 57 | + // take only robots that are pretty close to the center |
| 58 | + let xs_by_y = robots |
| 59 | + .iter() |
| 60 | + .map(|r| r.pos) |
| 61 | + .filter(|p| (p[0] - x_center).abs() < 10) |
| 62 | + .map(|p| (p[1], p[0] - x_center)) |
| 63 | + .acc_into_vec(); |
| 64 | + |
| 65 | + let mut non_symmetric = 0; |
| 66 | + for xs in xs_by_y.into_values() { |
| 67 | + for x in xs.iter() { |
| 68 | + if !xs.contains(&-x) { |
| 69 | + non_symmetric += 1; |
| 70 | + } |
| 71 | + } |
| 72 | + } |
| 73 | + non_symmetric |
| 74 | +} |
| 75 | + |
| 76 | +#[derive(Debug)] |
| 77 | +struct Robot { |
| 78 | + pos: Pos, |
| 79 | + vel: Vel, |
| 80 | +} |
| 81 | + |
| 82 | +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] |
| 83 | +enum Quadrant { |
| 84 | + NorthEast, |
| 85 | + NorthWest, |
| 86 | + SouthWest, |
| 87 | + SouthEast, |
| 88 | +} |
| 89 | + |
| 90 | +impl Robot { |
| 91 | + fn new(pos: Pos, vel: Vel) -> Robot { |
| 92 | + Robot { pos, vel } |
| 93 | + } |
| 94 | + |
| 95 | + fn new_constrained(pos: Pos, vel: Vel, max: Pos) -> Robot { |
| 96 | + fn constrained(pos: Pos, max: Pos) -> Pos { |
| 97 | + // place c into the [0, m) interval |
| 98 | + fn constrain(c: Coord, m: Coord) -> Coord { |
| 99 | + ((c % m) + m) % m |
| 100 | + } |
| 101 | + [constrain(pos[0], max[0]), constrain(pos[1], max[1])] |
| 102 | + } |
| 103 | + Self::new(constrained(pos, max), vel) |
| 104 | + } |
| 105 | + |
| 106 | + fn evolved_by(&self, t: Coord) -> Robot { |
| 107 | + fn evolve(p: Coord, v: Coord, t: Coord) -> Coord { |
| 108 | + t.checked_mul(v).and_then(|d| d.checked_add(p)).unwrap() |
| 109 | + } |
| 110 | + let [px, py] = self.pos; |
| 111 | + let [vx, vy] = self.vel; |
| 112 | + // We don't have to make sure we're in the field after every step |
| 113 | + // (as long as we don't get overflow problems) |
| 114 | + Self::new_constrained([evolve(px, vx, t), evolve(py, vy, t)], self.vel, EXTENT) |
| 115 | + } |
| 116 | +} |
| 117 | + |
| 118 | +fn quadrant(p: Pos) -> Option<Quadrant> { |
| 119 | + match (p[0] - EXTENT[0] / 2, p[1] - EXTENT[1] / 2) { |
| 120 | + (x, y) if x > 0 && y < 0 => Some(Quadrant::NorthEast), |
| 121 | + (x, y) if x < 0 && y < 0 => Some(Quadrant::NorthWest), |
| 122 | + (x, y) if x < 0 && y > 0 => Some(Quadrant::SouthWest), |
| 123 | + (x, y) if x > 0 && y > 0 => Some(Quadrant::SouthEast), |
| 124 | + _ => None, |
| 125 | + } |
| 126 | +} |
| 127 | + |
| 128 | +impl FromStr for Robot { |
| 129 | + type Err = RobotParseError; |
| 130 | + |
| 131 | + fn from_str(s: &str) -> Result<Self, Self::Err> { |
| 132 | + let (p, v) = s.split_once(' ').ok_or(RobotParseError::WrongFormat)?; |
| 133 | + let (px, py): (Coord, Coord) = p |
| 134 | + .strip_prefix("p=") |
| 135 | + .and_then(|s| s.split_once(',')) |
| 136 | + .ok_or(RobotParseError::WrongFormat) |
| 137 | + .and_then(|(x, y)| Ok((x.parse()?, y.parse()?)))?; |
| 138 | + let (vx, vy): (Coord, Coord) = v |
| 139 | + .strip_prefix("v=") |
| 140 | + .and_then(|s| s.split_once(',')) |
| 141 | + .ok_or(RobotParseError::WrongFormat) |
| 142 | + .and_then(|(x, y)| Ok((x.parse()?, y.parse()?)))?; |
| 143 | + Ok(Robot::new([px, py], [vx, vy])) |
| 144 | + } |
| 145 | +} |
| 146 | + |
| 147 | +trait AccumulateExt: Iterator { |
| 148 | + fn accumulated_with<KeyFn, AccFn, K, U>(self, key: KeyFn, acc: AccFn) -> HashMap<K, U> |
| 149 | + where |
| 150 | + Self::Item: Clone, |
| 151 | + K: Eq + Hash, |
| 152 | + U: Default, |
| 153 | + KeyFn: Fn(Self::Item) -> K, |
| 154 | + AccFn: Fn(Self::Item, &mut U), |
| 155 | + // this 'feels' unnecessary, we should be able to 'stream' |
| 156 | + //values into the dictionary, but even `map` requires Sized |
| 157 | + Self: Sized, |
| 158 | + { |
| 159 | + let mut accumulator = HashMap::new(); |
| 160 | + for item in self { |
| 161 | + acc( |
| 162 | + item.clone(), |
| 163 | + accumulator.entry(key(item.clone())).or_default(), |
| 164 | + ); |
| 165 | + } |
| 166 | + accumulator |
| 167 | + } |
| 168 | +} |
| 169 | + |
| 170 | +impl<I: Iterator> AccumulateExt for I {} |
| 171 | + |
| 172 | +trait CountedExt: Iterator { |
| 173 | + fn counted(self) -> HashMap<Self::Item, usize> |
| 174 | + where |
| 175 | + Self: Sized, |
| 176 | + Self::Item: Clone + Eq + Hash, |
| 177 | + { |
| 178 | + self.accumulated_with(|k| k, |_, c| *c += 1) |
| 179 | + } |
| 180 | +} |
| 181 | + |
| 182 | +impl<I: Iterator> CountedExt for I {} |
| 183 | + |
| 184 | +trait AccIntoVecExt: Iterator { |
| 185 | + fn acc_into_vec<K, V>(self) -> HashMap<K, Vec<V>> |
| 186 | + where |
| 187 | + Self: Sized, |
| 188 | + Self::Item: Clone + Into<(K, V)>, |
| 189 | + K: Eq + Hash, |
| 190 | + { |
| 191 | + self.accumulated_with( |
| 192 | + |item| item.into().0, |
| 193 | + |item, items: &mut Vec<_>| items.push(item.into().1), |
| 194 | + ) |
| 195 | + } |
| 196 | +} |
| 197 | + |
| 198 | +impl<I: Iterator> AccIntoVecExt for I {} |
| 199 | + |
| 200 | +#[derive(Error, Debug)] |
| 201 | +enum RobotParseError { |
| 202 | + #[error("Wrong Format")] |
| 203 | + WrongNumberFormat(#[from] ParseIntError), |
| 204 | + #[error("Wrong Format")] |
| 205 | + WrongFormat, |
| 206 | +} |
| 207 | + |
| 208 | +// thanks claude |
| 209 | +#[allow(unused)] |
| 210 | +fn show(robots: &[Robot]) { |
| 211 | + const WIDTH: usize = 101; |
| 212 | + const HEIGHT: usize = 103; |
| 213 | + |
| 214 | + // Create a 2D grid to track robot positions |
| 215 | + let mut grid = vec![vec![false; WIDTH]; HEIGHT]; |
| 216 | + |
| 217 | + // Mark positions where robots are located |
| 218 | + for robot in robots { |
| 219 | + let x = robot.pos[0] as usize; |
| 220 | + let y = robot.pos[1] as usize; |
| 221 | + |
| 222 | + // Ensure coordinates are within bounds |
| 223 | + if x < WIDTH && y < HEIGHT { |
| 224 | + grid[y][x] = true; |
| 225 | + } |
| 226 | + } |
| 227 | + |
| 228 | + // Print the grid |
| 229 | + for row in &grid { |
| 230 | + for &has_robot in row { |
| 231 | + if has_robot { |
| 232 | + print!("#"); |
| 233 | + } else { |
| 234 | + print!("."); |
| 235 | + } |
| 236 | + } |
| 237 | + println!(); // New line after each row |
| 238 | + } |
| 239 | +} |
0 commit comments