Skip to content

Commit d555f7a

Browse files
committed
2025: day 8
1 parent 4d597ed commit d555f7a

File tree

7 files changed

+285
-8
lines changed

7 files changed

+285
-8
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# [Advent of Code](https://adventofcode.com) in Rust 🦀
22

3-
![Stars: 514](https://img.shields.io/badge/Stars-514⭐-blue)
4-
![Rust: 257](https://img.shields.io/badge/Rust-257-cyan?logo=Rust)
3+
![Stars: 516](https://img.shields.io/badge/Stars-516⭐-blue)
4+
![Rust: 258](https://img.shields.io/badge/Rust-258-cyan?logo=Rust)
55
![Python: 127](https://img.shields.io/badge/Python-127-cyan?logo=Python)
66

77
<img src="./scripts/assets/christmas_ferris_2015_2024.png" alt="Christmas Ferris" width="164" />
@@ -10,7 +10,7 @@
1010

1111
Made for fun 😎 and to practice Rust. Many thanks to [Eric Wastl](https://twitter.com/ericwastl).
1212

13-
## 2025 (current event) ([Calendar](https://adventofcode.com/2025)) ([Solutions](src/year2025/)) : 14
13+
## 2025 (current event) ([Calendar](https://adventofcode.com/2025)) ([Solutions](src/year2025/)) : 16
1414

1515
Puzzle | Stars | Languages
1616
----------------------------------------------------------------- | ----- | -----------
@@ -21,12 +21,13 @@ Puzzle | Stars | Lang
2121
[Day 5: Cafeteria](https://adventofcode.com/2025/day/5) | ⭐⭐ | [![Rust](./scripts/assets/rust.png)](src/year2025/day5/day5.rs) [![Go](./scripts/assets/go.png)](src/year2025/day5/day5.go)
2222
[Day 6: Trash Compactor](https://adventofcode.com/2025/day/6) | ⭐⭐ | [![Rust](./scripts/assets/rust.png)](src/year2025/day6/day6.rs) [![Rust](./scripts/assets/rust.png)](src/year2025/day6/day6_declarative.rs) [![Python](./scripts/assets/python.png)](src/year2025/day6/day6.py)
2323
[Day 7: Laboratories](https://adventofcode.com/2025/day/7) | ⭐⭐ | [![Rust](./scripts/assets/rust.png)](src/year2025/day7/day7.rs) [![Go](./scripts/assets/go.png)](src/year2025/day7/day7.go) [🎁](src/year2025/day7/README.md)
24+
[Day 8: Playground](https://adventofcode.com/2025/day/8) | ⭐⭐ | [![Rust](./scripts/assets/rust.png)](src/year2025/day8/day8.rs)
2425

2526
## All years
2627

2728
Calendar | Solutions | Stars | Rust | Python | 🎁
2829
-------- | --------- | ----- | ---- | ------ | --
29-
[Advent of Code 2025](https://adventofcode.com/2025) | [Solutions](src/year2025/README.md) | 14⭐ | 7 | 5 | 1
30+
[Advent of Code 2025](https://adventofcode.com/2025) | [Solutions](src/year2025/README.md) | 16⭐ | 8 | 5 | 1
3031
[Advent of Code 2024](https://adventofcode.com/2024) | [Solutions](src/year2024/README.md) | 50⭐ | 25 | 11 | 3
3132
[Advent of Code 2023](https://adventofcode.com/2023) | [Solutions](src/year2023/README.md) | 50⭐ | 25 | 10 | 2
3233
[Advent of Code 2022](https://adventofcode.com/2022) | [Solutions](src/year2022/README.md) | 50⭐ | 25 | 18 | 1

crates/aoc/src/dsu.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/// A Disjoint Set Union (Union-Find) structure using 0..n indices.
2+
///
3+
/// Uses path compression and union by size (attach smaller tree under larger).
4+
#[derive(Debug, Clone)]
5+
pub struct UnionFind {
6+
parent: Vec<usize>,
7+
size: Vec<usize>,
8+
components: usize,
9+
}
10+
11+
impl UnionFind {
12+
/// Create a `UnionFind` with `n` elements (0..n-1), each in its own set.
13+
#[must_use]
14+
pub fn new(n: usize) -> Self {
15+
Self {
16+
parent: (0..n).collect(),
17+
size: vec![1; n],
18+
components: n,
19+
}
20+
}
21+
22+
/// Returns the number of elements.
23+
#[must_use]
24+
pub const fn len(&self) -> usize {
25+
self.parent.len()
26+
}
27+
28+
/// Returns true if no elements.
29+
#[must_use]
30+
pub const fn is_empty(&self) -> bool {
31+
self.parent.is_empty()
32+
}
33+
34+
/// Find the representative (root) of `mut x` with path compression.
35+
///
36+
/// # Panics
37+
/// Panics if `x` is out of bounds.
38+
pub fn find(&mut self, mut x: usize) -> usize {
39+
let n = self.len();
40+
assert!(x < n, "index out of bounds in UnionFind::find");
41+
42+
// Find root
43+
let mut root = x;
44+
while self.parent[root] != root {
45+
root = self.parent[root];
46+
}
47+
48+
// Path compression
49+
while self.parent[x] != x {
50+
let next = self.parent[x];
51+
self.parent[x] = root;
52+
x = next;
53+
}
54+
55+
root
56+
}
57+
58+
/// Returns true if `a` and `b` are in the same set.
59+
///
60+
/// # Panics
61+
/// Panics if indices out of bounds.
62+
pub fn same(&mut self, a: usize, b: usize) -> bool {
63+
self.find(a) == self.find(b)
64+
}
65+
66+
/// Unite the sets containing `a` and `b`. Returns `true` if they were separate (merge happened).
67+
///
68+
/// # Panics
69+
/// Panics if indices out of bounds.
70+
pub fn unite(&mut self, a: usize, b: usize) -> bool {
71+
let mut ra = self.find(a);
72+
let mut rb = self.find(b);
73+
74+
if ra == rb {
75+
return false;
76+
}
77+
78+
// Union by size: ensure ra is the larger root
79+
if self.size[ra] < self.size[rb] {
80+
std::mem::swap(&mut ra, &mut rb);
81+
}
82+
83+
// Attach rb under ra
84+
self.parent[rb] = ra;
85+
self.size[ra] += self.size[rb];
86+
self.components -= 1;
87+
true
88+
}
89+
90+
/// Size of the set containing `x`.
91+
///
92+
/// # Panics
93+
/// Panics if `x` out of bounds.
94+
pub fn set_size(&mut self, x: usize) -> usize {
95+
let r = self.find(x);
96+
self.size[r]
97+
}
98+
99+
/// Number of connected components (disjoint sets)
100+
#[must_use]
101+
pub const fn component_count(&self) -> usize {
102+
self.components
103+
}
104+
}
105+
106+
#[cfg(test)]
107+
mod tests {
108+
use super::UnionFind;
109+
110+
#[test]
111+
fn basic_union_find() {
112+
let mut uf = UnionFind::new(5);
113+
assert_eq!(uf.component_count(), 5);
114+
assert!(!uf.same(0, 1));
115+
assert!(uf.unite(0, 1));
116+
assert!(uf.same(0, 1));
117+
assert_eq!(uf.set_size(0), 2);
118+
assert_eq!(uf.component_count(), 4);
119+
120+
// idempotent union returns false
121+
assert!(!uf.unite(0, 1));
122+
123+
// connect remaining
124+
uf.unite(1, 2);
125+
uf.unite(3, 4);
126+
assert_eq!(uf.component_count(), 2);
127+
assert!(uf.same(0, 2));
128+
assert!(uf.same(3, 4));
129+
assert!(!uf.same(2, 3));
130+
}
131+
132+
#[test]
133+
#[should_panic]
134+
fn find_out_of_bounds() {
135+
let mut uf = UnionFind::new(3);
136+
let _ = uf.find(10);
137+
}
138+
}

crates/aoc/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod args;
22
mod coord;
33
mod counter;
44
mod direction;
5+
mod dsu;
56
mod grid;
67
mod gridu;
78
pub mod hexslice;
@@ -23,6 +24,8 @@ pub type GridU<T> = gridu::GridU<T>;
2324
pub type Square<T> = square::Square<T>;
2425
pub type Counter<T> = counter::Counter<T>;
2526

27+
pub type UnionFind = dsu::UnionFind;
28+
2629
pub struct Christmas(());
2730

2831
/// Christmay Day

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,5 +127,5 @@ make_year!(year2024
127127
);
128128

129129
make_year!(year2025
130-
day1,day2,day3,day4,day5,day6,day7
130+
day1,day2,day3,day4,day5,day6,day7,day8
131131
);

src/year2025/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# Advent of Code in Rust 🦀
22

33
![AoC2025](https://img.shields.io/badge/Advent_of_Code-2025-8A2BE2)
4-
![Stars: 14](https://img.shields.io/badge/Stars-14⭐-blue)
5-
![Rust: 8](https://img.shields.io/badge/Rust-8-cyan?logo=Rust)
4+
![Stars: 16](https://img.shields.io/badge/Stars-16⭐-blue)
5+
![Rust: 9](https://img.shields.io/badge/Rust-9-cyan?logo=Rust)
66
![Python: 5](https://img.shields.io/badge/Python-5-cyan?logo=Python)
77

8-
## 2025 ([Calendar](https://adventofcode.com/2025)) ([Solutions](./)) : 14
8+
## 2025 ([Calendar](https://adventofcode.com/2025)) ([Solutions](./)) : 16
99

1010
Puzzle | Stars | Languages
1111
----------------------------------------------------------------- | ----- | -----------
@@ -16,3 +16,4 @@ Puzzle | Stars | Lang
1616
[Day 5: Cafeteria](https://adventofcode.com/2025/day/5) | ⭐⭐ | [![Rust](../../scripts/assets/rust.png)](day5/day5.rs) [![Go](../../scripts/assets/go.png)](day5/day5.go)
1717
[Day 6: Trash Compactor](https://adventofcode.com/2025/day/6) | ⭐⭐ | [![Rust](../../scripts/assets/rust.png)](day6/day6.rs) [![Rust](../../scripts/assets/rust.png)](day6/day6_declarative.rs) [![Python](../../scripts/assets/python.png)](day6/day6.py)
1818
[Day 7: Laboratories](https://adventofcode.com/2025/day/7) | ⭐⭐ | [![Rust](../../scripts/assets/rust.png)](day7/day7.rs) [![Go](../../scripts/assets/go.png)](day7/day7.go) [🎁](day7/README.md)
19+
[Day 8: Playground](https://adventofcode.com/2025/day/8) | ⭐⭐ | [![Rust](../../scripts/assets/rust.png)](day8/day8.rs)

src/year2025/day8/day8.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//! [Day 8: Playground](https://adventofcode.com/2025/day/8)
2+
3+
use rustc_hash::FxHashMap;
4+
5+
struct Point3D {
6+
x: i64,
7+
y: i64,
8+
z: i64,
9+
}
10+
11+
impl Point3D {
12+
const fn square_dist(&self, rhs: &Self) -> i64 {
13+
(rhs.x - self.x).pow(2) + (rhs.y - self.y).pow(2) + (rhs.z - self.z).pow(2)
14+
}
15+
}
16+
17+
struct Puzzle {
18+
points: Vec<Point3D>,
19+
edges: Vec<(i64, usize, usize)>,
20+
}
21+
22+
impl Puzzle {
23+
/// Initialize from the puzzle input.
24+
fn new(data: &str) -> Self {
25+
let mut points = vec![];
26+
let mut edges = vec![];
27+
28+
for line in data.lines() {
29+
let xyz: Vec<_> = line.split(',').map(|s| s.parse::<i64>().unwrap()).collect();
30+
31+
points.push(Point3D {
32+
x: xyz[0],
33+
y: xyz[1],
34+
z: xyz[2],
35+
});
36+
}
37+
38+
for i in 0..(points.len() - 1) {
39+
for j in (i + 1)..points.len() {
40+
edges.push((points[i].square_dist(&points[j]), i, j));
41+
}
42+
}
43+
edges.sort_unstable_by_key(|e| e.0);
44+
45+
Self { points, edges }
46+
}
47+
48+
/// Solve part one.
49+
fn part1(&self, connections: usize) -> i32 {
50+
let mut dsu = aoc::UnionFind::new(self.points.len());
51+
52+
for i in 0..connections.min(self.edges.len()) {
53+
let e = self.edges[i];
54+
dsu.unite(e.1, e.2);
55+
}
56+
57+
let mut comps = FxHashMap::default();
58+
for i in 0..self.points.len() {
59+
let r = dsu.find(i);
60+
*comps.entry(r).or_insert(0) += 1;
61+
}
62+
63+
let mut sizes: Vec<_> = comps.values().copied().collect();
64+
sizes.sort_unstable_by(|a, b| b.cmp(a));
65+
sizes.iter().take(3).product()
66+
}
67+
68+
/// Solve part two.
69+
/// [Kruskal's algorithm](https://en.wikipedia.org/wiki/Kruskal%27s_algorithm)
70+
fn part2(&self) -> i64 {
71+
let mut dsu = aoc::UnionFind::new(self.points.len());
72+
73+
for &(_, i, j) in &self.edges {
74+
if dsu.unite(i, j) {
75+
// If this union makes everything connected, it's the answer
76+
if dsu.component_count() == 1 {
77+
return self.points[i].x * self.points[j].x;
78+
}
79+
}
80+
}
81+
0
82+
}
83+
}
84+
85+
/// # Panics
86+
#[must_use]
87+
pub fn solve(data: &str) -> (i32, i64) {
88+
let puzzle = Puzzle::new(data);
89+
(puzzle.part1(1000), puzzle.part2())
90+
}
91+
92+
pub fn main() {
93+
let args = aoc::parse_args();
94+
args.run(solve);
95+
}
96+
97+
#[cfg(test)]
98+
mod test {
99+
use super::*;
100+
101+
const TEST_INPUT: &str = include_str!("test.txt");
102+
103+
#[test]
104+
fn part1() {
105+
let puzzle = Puzzle::new(TEST_INPUT);
106+
assert_eq!(puzzle.part1(10), 40);
107+
}
108+
109+
#[test]
110+
fn part2() {
111+
let puzzle = Puzzle::new(TEST_INPUT);
112+
assert_eq!(puzzle.part2(), 25272);
113+
}
114+
}

src/year2025/day8/test.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
162,817,812
2+
57,618,57
3+
906,360,560
4+
592,479,940
5+
352,342,300
6+
466,668,158
7+
542,29,236
8+
431,825,988
9+
739,650,466
10+
52,470,668
11+
216,146,977
12+
819,987,18
13+
117,168,530
14+
805,96,715
15+
346,949,466
16+
970,615,88
17+
941,993,340
18+
862,61,35
19+
984,92,344
20+
425,690,689

0 commit comments

Comments
 (0)