Rust
Areas are found by flooding, in the meantime whenever the adjacent plot would be outside the region (or out of bounds) the edge (inside plot, outside plot) is saved in a perimeter list. Part 1 takes just the size of that list, in part 2 we remove fence parts and all entries directly next to it on both sides.
Solution
use std::collections::{HashSet, VecDeque};
use euclid::{default::*, point2, vec2};
type Fences = HashSet<(Point2D<i32>, Point2D<i32>)>;
const DIRS: [Vector2D<i32>; 4] = [vec2(0, -1), vec2(1, 0), vec2(0, 1), vec2(-1, 0)];
fn parse(input: &str) -> Vec<&[u8]> {
input.lines().map(|l| l.as_bytes()).collect()
}
fn price(field: &[&[u8]], start: (usize, usize), visited: &mut [Vec<bool>]) -> (u32, Fences) {
let crop = field[start.1][start.0];
let width = field[0].len();
let height = field.len();
let mut area_visited = vec![vec![false; width]; height];
let mut area = 0;
let mut fences: Fences = HashSet::new();
area_visited[start.1][start.0] = true;
visited[start.1][start.0] = true;
let start = point2(start.0 as i32, start.1 as i32);
let bounds = Rect::new(Point2D::origin(), Size2D::new(width, height).to_i32());
let mut frontier = VecDeque::from([start]);
while let Some(p) = frontier.pop_front() {
area += 1;
for dir in DIRS {
let next = p + dir;
if bounds.contains(next) {
let next_u = next.to_usize();
if area_visited[next_u.y][next_u.x] {
continue;
}
if field[next_u.y][next_u.x] == crop {
visited[next_u.y][next_u.x] = true;
area_visited[next_u.y][next_u.x] = true;
frontier.push_back(next);
continue;
}
}
fences.insert((p, next));
}
}
(area, fences)
}
fn part1(input: String) {
let field = parse(&input);
let width = field[0].len();
let height = field.len();
let mut visited = vec![vec![false; width]; height];
let mut total_price = 0;
for y in 0..height {
for x in 0..width {
if !visited[y][x] {
let (area, fences) = price(&field, (x, y), &mut visited);
total_price += area * fences.len() as u32;
}
}
}
println!("{total_price}");
}
fn count_perimeter(mut fences: Fences) -> u32 {
let list: Vec<_> = fences.iter().copied().collect();
let mut perimeter = 0;
for (v, w) in list {
if fences.contains(&(v, w)) {
perimeter += 1;
let dir = w - v;
let orth = dir.yx();
let mut next = v + orth;
while fences.remove(&(next, next + dir)) {
next += orth;
}
let mut next = v - orth;
while fences.remove(&(next, next + dir)) {
next -= orth;
}
}
}
perimeter
}
fn part2(input: String) {
let field = parse(&input);
let width = field[0].len();
let height = field.len();
let mut visited = vec![vec![false; width]; height];
let mut total_price = 0;
for y in 0..height {
for x in 0..width {
if !visited[y][x] {
let (area, fences) = price(&field, (x, y), &mut visited);
total_price += area * count_perimeter(fences);
}
}
}
println!("{total_price}");
}
util::aoc_main!();
Also on github
Rust
This problem is basically a linear system, which can be solved by inverting the 2x2 matrix of button distances. I put some more detail in the comments.
Solution
use std::sync::LazyLock; use regex::Regex; #[derive(Debug)] struct Machine { a: (i64, i64), b: (i64, i64), prize: (i64, i64), } impl Machine { fn tokens_100(&self) -> i64 { for b in 0..=100 { for a in 0..=100 { let pos = (self.a.0 * a + self.b.0 * b, self.a.1 * a + self.b.1 * b); if pos == self.prize { return b + 3 * a; } } } 0 } fn tokens_inv(&self) -> i64 { // If [ab] is the matrix containing our two button vectors: [ a.0 b.0 ] // [ a.1 b.1 ] // then prize = [ab] * x, where x holds the number of required button presses // for a and b, (na, nb). // By inverting [ab] we get // // x = [ab]⁻¹ * prize let det = (self.a.0 * self.b.1) - (self.a.1 * self.b.0); if det == 0 { panic!("Irregular matrix"); } let det = det as f64; // The matrix [ a b ] is the inverse of [ a.0 b.0 ] . // [ c d ] [ a.1 b.1 ] let a = self.b.1 as f64 / det; let b = -self.b.0 as f64 / det; let c = -self.a.1 as f64 / det; let d = self.a.0 as f64 / det; // Multiply [ab] * prize to get the result let na = self.prize.0 as f64 * a + self.prize.1 as f64 * b; let nb = self.prize.0 as f64 * c + self.prize.1 as f64 * d; // Only integer solutions are valid, verify rounded results: let ina = na.round() as i64; let inb = nb.round() as i64; let pos = ( self.a.0 * ina + self.b.0 * inb, self.a.1 * ina + self.b.1 * inb, ); if pos == self.prize { inb + 3 * ina } else { 0 } } fn translate(&self, tr: i64) -> Self { let prize = (self.prize.0 + tr, self.prize.1 + tr); Machine { prize, ..*self } } } impl From<&str> for Machine { fn from(s: &str) -> Self { static RE: LazyLock<(Regex, Regex)> = LazyLock::new(|| { ( Regex::new(r"Button [AB]: X\+(\d+), Y\+(\d+)").unwrap(), Regex::new(r"Prize: X=(\d+), Y=(\d+)").unwrap(), ) }); let (re_btn, re_prize) = &*RE; let mut caps = re_btn.captures_iter(s); let (_, [a0, a1]) = caps.next().unwrap().extract(); let a = (a0.parse().unwrap(), a1.parse().unwrap()); let (_, [b0, b1]) = caps.next().unwrap().extract(); let b = (b0.parse().unwrap(), b1.parse().unwrap()); let (_, [p0, p1]) = re_prize.captures(s).unwrap().extract(); let prize = (p0.parse().unwrap(), p1.parse().unwrap()); Machine { a, b, prize } } } fn parse(input: String) -> Vec<Machine> { input.split("\n\n").map(Into::into).collect() } fn part1(input: String) { let machines = parse(input); let sum = machines.iter().map(|m| m.tokens_100()).sum::<i64>(); println!("{sum}"); } const TRANSLATION: i64 = 10000000000000; fn part2(input: String) { let machines = parse(input); let sum = machines .iter() .map(|m| m.translate(TRANSLATION).tokens_inv()) .sum::<i64>(); println!("{sum}"); } util::aoc_main!();
Also on github