Skip to content

Commit 68515cc

Browse files
committed
Day 13
1 parent 005a468 commit 68515cc

File tree

8 files changed

+269
-5
lines changed

8 files changed

+269
-5
lines changed

Cargo.lock

+31
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ members = [
4343
"y23/d10-pipe-maze",
4444
"y23/d11-cosmic-expansion",
4545
"y23/d12-hot-springs",
46-
"y23/d13",
46+
"y23/d13-point-of-incidence",
4747
"y23/d14",
4848
"y23/d15",
4949
"y23/d16",

aoc/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ aoc-macro = { path = "../aoc-macro" }
1010

1111
binary-heap-plus = "0.5.0"
1212
itertools = "0.12.0"
13-
num-traits = "0.2.17"
13+
num-traits = "0.2.17"

aoc/src/iterstuff.rs

+27
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,31 @@ pub trait IterJunk: Iterator {
167167
{
168168
self.k_largest_by(k, |a, b| f(a).cmp(&f(b)))
169169
}
170+
171+
/// `partial_sums` is a function that returns an iterator over the partial sums of the iterator.
172+
///
173+
/// The first element is the first element of the iterator, the second element is the sum of the
174+
/// first two elements, and so on.
175+
///
176+
/// # Examples
177+
///
178+
/// ```
179+
/// use aoc::iterstuff::IterJunk;
180+
///
181+
/// let v = vec![1, 2, 3, 4, 5];
182+
/// let sums: Vec<i32> = v.into_iter().partial_sums().collect();
183+
///
184+
/// assert_eq!(sums, vec![1, 3, 6, 10, 15]);
185+
/// ```
186+
#[must_use = "iterators are lazy and do nothing unless consumed"]
187+
fn partial_sums(self) -> impl Iterator<Item = <Self as Iterator>::Item>
188+
where
189+
Self: Sized,
190+
Self::Item: std::ops::AddAssign + Copy + Default,
191+
{
192+
self.scan(Default::default(), |sum, x| {
193+
*sum += x;
194+
Some(*sum)
195+
})
196+
}
170197
}

y23/d13/Cargo.toml renamed to y23/d13-point-of-incidence/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ edition = "2021"
66
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
77

88
[dependencies]
9+
aoc = { path = "../../aoc" }
10+
itertools = "0.12.0"
File renamed without changes.
+207
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
use std::iter::repeat;
2+
3+
use aoc::{input_str, IterJunk};
4+
use itertools::Itertools;
5+
6+
// Read in 2d array of ash and rocks
7+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8+
enum Tile {
9+
Ash,
10+
Rock,
11+
}
12+
13+
impl Tile {
14+
fn from_char(c: char) -> Self {
15+
match c {
16+
'.' => Self::Ash,
17+
'#' => Self::Rock,
18+
_ => panic!("Invalid tile"),
19+
}
20+
}
21+
}
22+
23+
#[derive(Debug, Clone)]
24+
struct Map {
25+
tiles: Vec<Tile>,
26+
transposed: Vec<Tile>,
27+
width: usize,
28+
height: usize,
29+
}
30+
31+
impl Map {
32+
fn from_str(s: &str) -> Self {
33+
let mut height = 0;
34+
let tiles = s
35+
.lines()
36+
.map(|s| s.chars())
37+
.flat_map(|line| {
38+
height += 1;
39+
line
40+
})
41+
.map(Tile::from_char)
42+
.collect_vec();
43+
44+
let width = tiles.len() / height;
45+
46+
let transposed = (0..width)
47+
.flat_map(|x| (0..height).zip(repeat(x)))
48+
.map(|(y, x)| tiles[y * width + x])
49+
.collect_vec();
50+
51+
Self {
52+
tiles,
53+
transposed,
54+
width,
55+
height,
56+
}
57+
}
58+
59+
fn get(&self, x: usize, y: usize) -> Tile {
60+
self.tiles[y * self.width + x]
61+
}
62+
63+
fn get_row(&self, y: usize) -> &[Tile] {
64+
&self.tiles[y * self.width..(y + 1) * self.width]
65+
}
66+
67+
fn get_col(&self, x: usize) -> &[Tile] {
68+
&self.transposed[x * self.height..(x + 1) * self.height]
69+
}
70+
71+
fn detect_mirror(&self) -> usize {
72+
fn is_mirror((left, right): (&[Tile], &[Tile])) -> bool {
73+
left.iter().rev().zip(right.iter()).all(|(l, r)| l == r)
74+
}
75+
76+
if let Some(col) = (1..self.width).find(|&col| {
77+
(0..self.height)
78+
.map(|r| self.get_row(r).split_at(col))
79+
.all(is_mirror)
80+
}) {
81+
return col;
82+
}
83+
84+
if let Some(row) = (1..self.height).find(|&row| {
85+
(0..self.width)
86+
.map(|c| self.get_col(c).split_at(row))
87+
.all(is_mirror)
88+
}) {
89+
return 100 * row;
90+
}
91+
92+
panic!("No mirror found");
93+
}
94+
95+
// In part 2, there is a second mirror that is _almost_ correct.
96+
// It is off by exactly one tile
97+
fn detect_with_smudge(&self, smudges: usize) -> usize {
98+
// Helper function that returns how close 2 sets of tiles are to being a mirror
99+
// (returns how many tiles are different)
100+
fn mirror_diff(left: &[Tile], right: &[Tile]) -> usize {
101+
left.iter()
102+
.rev()
103+
.zip(right.iter())
104+
.filter(|(l, r)| l != r)
105+
.count()
106+
}
107+
108+
// Find a column that is almost a mirror with a difference of 1
109+
if let Some(col) = (1..self.width).find(|&col| {
110+
(0..self.height)
111+
.map(|r| self.get_row(r).split_at(col))
112+
.map(|(left, right)| mirror_diff(left, right))
113+
.partial_sums()
114+
.find_or_last(|&x| x > smudges)
115+
.eq(&Some(smudges))
116+
}) {
117+
return col;
118+
}
119+
120+
// Find a row that is almost a mirror with a difference of 1
121+
if let Some(row) = (1..self.height).find(|&row| {
122+
(0..self.width)
123+
.map(|c| self.get_col(c).split_at(row))
124+
.map(|(left, right)| mirror_diff(left, right))
125+
.partial_sums()
126+
.find_or_last(|&x| x > smudges)
127+
.eq(&Some(smudges))
128+
}) {
129+
return 100 * row;
130+
}
131+
132+
panic!("No mirror found");
133+
}
134+
}
135+
136+
impl std::fmt::Display for Map {
137+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138+
for y in 0..self.height {
139+
for x in 0..self.width {
140+
let c = match self.get(x, y) {
141+
Tile::Ash => '.',
142+
Tile::Rock => '#',
143+
};
144+
write!(f, "{}", c)?;
145+
}
146+
writeln!(f)?;
147+
}
148+
Ok(())
149+
}
150+
}
151+
152+
fn main() {
153+
let input = input_str!(2023, 13);
154+
155+
// Each map uses multiple lines, maps are separated by a blank line
156+
let start = std::time::Instant::now();
157+
let maps = input.split("\n\n").map(Map::from_str).collect_vec();
158+
println!("Parsed: {:?}", start.elapsed());
159+
160+
// Part 1 - sum the mirror values
161+
let start = std::time::Instant::now();
162+
let sum: usize = maps.iter().map(|m| m.detect_mirror()).sum();
163+
println!("Part 1: {}", sum);
164+
println!("Part 1: {:?}", start.elapsed());
165+
166+
// Part 2 - sum the mirror values with the smudge
167+
let start = std::time::Instant::now();
168+
let sum: usize = maps.iter().map(|m| m.detect_with_smudge(1)).sum();
169+
println!("Part 2: {}", sum);
170+
println!("Part 2: {:?}", start.elapsed());
171+
}
172+
173+
#[cfg(test)]
174+
mod tests {
175+
use super::*;
176+
177+
#[test]
178+
fn test_map() {
179+
let input = "#.##..##.\n..#.##.#.\n##......#\n##......#\n..#.##.#.\n..##..##.\n#.#.##.#.";
180+
let map = Map::from_str(input);
181+
182+
assert_eq!(map.width, 9);
183+
assert_eq!(map.height, 7);
184+
185+
// Detect the mirror
186+
assert_eq!(map.detect_mirror(), 5);
187+
188+
let input = "#...##..#\n#....#..#\n..##..###\n#####.##.\n#####.##.\n..##..###\n#....#..#";
189+
let map = Map::from_str(input);
190+
191+
assert_eq!(map.width, 9);
192+
assert_eq!(map.height, 7);
193+
194+
// Detect the mirror
195+
assert_eq!(map.detect_mirror(), 400);
196+
}
197+
198+
#[test]
199+
fn test_regressions() {
200+
let input = "##.#.##...####.\n#####..#...##..\n##.##.#.#####.#\n.##.#..##..##..\n###..#.#####.#.\n###..#.#####.#.\n.##.#..##..##..\n##.##.#.#####.#\n#####..#...##..\n##.#.##..#####.\n...#...#....###\n##.#.###.###..#\n..#.###.#.#....\n####...#.#.#...\n.#..#....##.#.#\n....####.###.##\n....####.###.##";
201+
let map = Map::from_str(input);
202+
203+
// Detect the mirror (row 4)
204+
println!("{} {}", map.width, map.height);
205+
assert_eq!(map.detect_mirror(), 1600);
206+
}
207+
}

y23/d13/src/main.rs

-3
This file was deleted.

0 commit comments

Comments
 (0)