#![allow( clippy::cast_sign_loss, clippy::cast_possible_wrap, clippy::too_many_lines )] use std::collections::HashSet; use tracing::debug; pub fn pt1(input: &str) -> usize { let mut res = 0; let mut visited: HashSet<(usize, usize)> = HashSet::new(); let fields = input .lines() .enumerate() .flat_map(|(y, line)| line.chars().enumerate().map(move |(x, c)| (c, x, y))) .collect::>(); for &(c, x, y) in &fields { if visited.contains(&(x, y)) { continue; } let mut current_matching: HashSet<(usize, usize)> = HashSet::new(); current_matching.insert((x, y)); visited.insert((x, y)); let mut previous_matching = 0; let mut cur_matching = current_matching.len(); while previous_matching != cur_matching { let m = current_matching.clone(); let v = visited.clone(); for &(_cc, cx, cy) in fields .iter() .filter(|(cc, _, _)| *cc == c) .filter(|(_cc, cx, cy)| !v.contains(&(*cx, *cy))) .filter(|&&(_cc, cx, cy)| { m.iter().any(|&(mx, my)| { (mx.abs_diff(cx) == 1 && my == cy) || (mx == cx && my.abs_diff(cy) == 1) }) }) { current_matching.insert((cx, cy)); visited.insert((cx, cy)); } previous_matching = cur_matching; cur_matching = current_matching.len(); } debug!("char: {c} -> current_matching = {current_matching:?}"); let area = current_matching.len(); let perimeter = get_perim(¤t_matching); debug!("area: {area}, perimeter:{perimeter}"); res += area * perimeter; } res } fn get_perim(current_matching: &HashSet<(usize, usize)>) -> usize { let mut perim = 0; for &(x, y) in current_matching { if x == 0 || !current_matching.contains(&(x - 1, y)) { perim += 1; } if y == 0 || !current_matching.contains(&(x, y - 1)) { perim += 1; } if !current_matching.contains(&(x + 1, y)) { perim += 1; } if !current_matching.contains(&(x, y + 1)) { perim += 1; } } perim } fn get_perimv2(current_matching: &HashSet<(usize, usize)>) -> usize { let mut perim = 0; for &(x, y) in current_matching { let left_is_empty = x == 0 || !current_matching.contains(&(x - 1, y)); let top_is_empty = y == 0 || !current_matching.contains(&(x, y - 1)); let right_is_empty = !current_matching.contains(&(x + 1, y)); let bottom_is_empty = !current_matching.contains(&(x, y + 1)); let top_left_is_empty = x == 0 || y == 0 || !current_matching.contains(&(x - 1, y - 1)); let top_right_is_empty = y == 0 || !current_matching.contains(&(x + 1, y - 1)); let bottom_left_is_empty = x == 0 || !current_matching.contains(&(x - 1, y + 1)); let bottom_right_is_empty = !current_matching.contains(&(x + 1, y + 1)); if left_is_empty && top_is_empty { perim += 1; } if left_is_empty && bottom_is_empty { perim += 1; } if right_is_empty && top_is_empty { perim += 1; } if right_is_empty && bottom_is_empty { perim += 1; } if !top_is_empty && top_left_is_empty && !left_is_empty { perim += 1; } if !bottom_is_empty && bottom_left_is_empty && !left_is_empty { perim += 1; } if top_right_is_empty && !top_is_empty && !right_is_empty { perim += 1; } if bottom_right_is_empty && !bottom_is_empty && !right_is_empty { perim += 1; } } perim } pub fn pt2(input: &str) -> usize { let mut res = 0; let mut visited: HashSet<(usize, usize)> = HashSet::new(); let fields = input .lines() .enumerate() .flat_map(|(y, line)| line.chars().enumerate().map(move |(x, c)| (c, x, y))) .collect::>(); for &(c, x, y) in &fields { if visited.contains(&(x, y)) { continue; } let mut current_matching: HashSet<(usize, usize)> = HashSet::new(); current_matching.insert((x, y)); visited.insert((x, y)); let mut previous_matching = 0; let mut cur_matching = current_matching.len(); while previous_matching != cur_matching { let m = current_matching.clone(); let v = visited.clone(); for &(_cc, cx, cy) in fields .iter() .filter(|(cc, _, _)| *cc == c) .filter(|(_cc, cx, cy)| !v.contains(&(*cx, *cy))) .filter(|&&(_cc, cx, cy)| { m.iter().any(|&(mx, my)| { (mx.abs_diff(cx) == 1 && my == cy) || (mx == cx && my.abs_diff(cy) == 1) }) }) { current_matching.insert((cx, cy)); visited.insert((cx, cy)); } previous_matching = cur_matching; cur_matching = current_matching.len(); } debug!("char: {c} -> current_matching = {current_matching:?}"); let area = current_matching.len(); let perimeter = get_perimv2(¤t_matching); debug!("area: {area}, perimeter:{perimeter}"); res += area * perimeter; } res } #[cfg(test)] mod tests { use tracing_test::traced_test; use super::{pt1, pt2}; use std::fs::read_to_string; #[traced_test] #[test] pub fn test_pt1() { let input = read_to_string("./inputs/day12/example.txt").expect("Missing example.txt for day12"); assert_eq!(pt1(&input), 1930) } #[traced_test] #[test] pub fn test_pt1_2() { let input = read_to_string("./inputs/day12/example_2.txt") .expect("Missing example_2.txt for day12"); assert_eq!(pt1(&input), 772) } #[traced_test] #[test] pub fn test_pt2() { let input = read_to_string("./inputs/day12/example.txt").expect("Missing example.txt for day12"); assert_eq!(pt2(&input), 1206) } }