Skip to content

Commit 11ec71d

Browse files
committed
Day 14
1 parent a2d1f9c commit 11ec71d

File tree

1 file changed

+239
-0
lines changed

1 file changed

+239
-0
lines changed

src/day14.rs

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
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

Comments
 (0)