advent-of-code 2025.5.0

Solutions to Advent of Code
Documentation
use std::collections::HashMap;

use crate::common::array_stack::ArrayStack;
use crate::input::Input;

pub fn solve(input: &Input) -> Result<String, String> {
    let mut nodes = HashMap::<u16, Vec<u16>>::with_capacity(1_000);
    let mut edges = vec![[false; 676]; 676];

    for edge in input.text.as_bytes().chunks(6) {
        let from = to_num(&edge[..2]);
        let to = to_num(&edge[3..]);

        nodes.entry(from).or_default().push(to);
        nodes.entry(to).or_default().push(from);

        edges[from as usize][to as usize] = true;
        edges[to as usize][from as usize] = true;
    }

    let mut seen = [false; 1024];

    Ok(if input.is_part_one() {
        let mut triangles = 0;

        for &n1 in nodes.keys() {
            seen[n1 as usize] = true;

            if let Some(neighbours) = nodes.get(&n1) {
                for (i, &n2) in neighbours.iter().enumerate() {
                    for &n3 in neighbours.iter().skip(i) {
                        if !seen[n2 as usize]
                            && !seen[n3 as usize]
                            && edges[n2 as usize][n3 as usize]
                            && (starts_with_t(n1) || starts_with_t(n2) || starts_with_t(n3))
                        {
                            triangles += 1;
                        }
                    }
                }
            }
        }

        triangles.to_string()
    } else {
        let mut clique = ArrayStack::<1024, u16>::new();
        let mut largest = [0_u16; 1024];
        let mut largest_len = 0;

        for (n1, neighbours) in nodes {
            if !seen[n1 as usize] {
                clique.clear();
                clique.push(n1)?;

                for n2 in neighbours {
                    if clique
                        .slice()
                        .iter()
                        .all(|&c| edges[n2 as usize][c as usize])
                    {
                        seen[n2 as usize] = true;
                        clique.push(n2)?;
                    }
                }

                if clique.len() > largest_len {
                    largest_len = clique.len();
                    largest[0..largest_len].copy_from_slice(clique.slice());
                }
            }
        }

        let mut result = String::new();
        largest[0..largest_len].sort_unstable();
        let mut first = true;
        for &n in &largest[0..largest_len] {
            if first {
                first = false;
            } else {
                result.push(',');
            }
            result.push_str(&from_num(n));
        }
        result
    })
}

fn to_num(p: &[u8]) -> u16 {
    26 * u16::from(p[0] - b'a') + u16::from(p[1] - b'a')
}

fn from_num(num: u16) -> String {
    let first_char = ((num / 26) as u8 + b'a') as char;
    let second_char = ((num % 26) as u8 + b'a') as char;
    format!("{first_char}{second_char}")
}

const fn starts_with_t(num: u16) -> bool {
    let first_char = (num / 26) as u8 + b'a';
    first_char == b't'
}

#[test]
pub fn tests() {
    assert_eq!("ab", from_num(to_num(b"ab")));
    assert_eq!("yz", from_num(to_num(b"yz")));

    let test_input = "kh-tc
qp-kh
de-cg
ka-co
yn-aq
qp-ub
cg-tb
vc-aq
tb-ka
wh-tc
yn-cg
kh-ub
ta-co
de-co
tc-td
tb-wq
wh-td
ta-ka
td-qp
aq-cg
wq-ub
ub-vc
de-ta
wq-aq
wq-vc
wh-yn
ka-de
kh-ta
co-tc
wh-qp
tb-vc
td-yn";
    test_part_one!(test_input => "7".to_string());
    test_part_two!(test_input => "co,de,ka,ta".to_string());

    let real_input = include_str!("day23_input.txt");
    test_part_one!(real_input => "1175".to_string());
    test_part_two!(real_input => "bw,dr,du,ha,mm,ov,pj,qh,tz,uv,vq,wq,xw".to_string());
}