Skip to content

Commit

Permalink
Year 2023 Day 12 - Reverse DFA
Browse files Browse the repository at this point in the history
  • Loading branch information
Alextopher committed Dec 12, 2024
1 parent 1e9a91e commit f9c3c7a
Show file tree
Hide file tree
Showing 4 changed files with 301 additions and 183 deletions.
2 changes: 0 additions & 2 deletions y23/d12-hot-springs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,3 @@ edition = "2021"

[dependencies]
aoc = { path = "../../aoc" }
itertools = "0.12.0"
rayon = "1.8.0"
341 changes: 161 additions & 180 deletions y23/d12-hot-springs/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,206 +1,187 @@
use aoc::input_str;
use itertools::Itertools;
use rayon::prelude::*;

#[derive(Debug, PartialEq, Clone, Copy)]
enum Cell {
Empty,
Filled,
Unknown,
// Builds some kind of DFA
//
// In the example '?###???????? 3,2,1' the generate machine is
//
// \.* - beginning (repeat . && accept #)
// ### - 3! (accept # , accept # , accept .)
// \.+ - spacing (repeat . && accept #)
// ## - 2! (accept # , accept .)
// \.+ - spacing (repeat . && accept #)
// # - 1! (accept .)
// \.* - ending (repeat .)
//
// Once the 'machine' is built running through the particular input requires keeping track of how heads on are each particular state and successfully book keeping when the next token is read.
// question marks ? split individual DFA states into 2

use aoc::{input_str, time};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Condition {
Good, // .
Bad, // #
Unknown, // ?
}

impl Cell {
fn from_char(c: char) -> Self {
match c {
'.' => Self::Empty,
'#' => Self::Filled,
'?' => Self::Unknown,
_ => panic!("Invalid character"),
}
}
#[derive(Debug, Clone, Copy)]
enum States {
Spacing, // repeat . && accept #
AcceptDot, // accept .
AcceptTag, // accept #
Ending, // repeat .
}

#[derive(Debug, PartialEq)]
struct Row {
cells: Vec<Cell>,
clues: Vec<usize>,
fn parse_line(line: &str) -> (Vec<Condition>, Vec<usize>) {
let mut split = line.split(' ');

let conditions = split
.next()
.unwrap()
.chars()
.map(|c| match c {
'.' => Condition::Good,
'#' => Condition::Bad,
'?' => Condition::Unknown,
_ => unreachable!(),
})
.collect();

let groups = split
.next()
.unwrap()
.split(',')
.map(|s| s.parse::<usize>().unwrap())
.collect();

(conditions, groups)
}

impl Row {
fn from_str(s: &str) -> Self {
// split at spaces
let mut groups = s.split_whitespace();

// parse cells
let cells = groups
.next()
.unwrap()
.chars()
.map(Cell::from_char)
.collect();

// parse clues
let clues = groups
.next()
.unwrap()
.split(',')
.map(|s| s.parse().unwrap())
.collect();

Self { cells, clues }
}

fn duplicate(&mut self) {
self.cells = self
.cells
.iter()
.cycle()
.take(self.cells.len() * 5)
.cloned()
.collect();

self.clues = self
.clues
.iter()
.cycle()
.take(self.clues.len() * 5)
.cloned()
.collect();
}

// Checks if a set of spaces works for this row.
fn check(&self, spaces: &[usize]) -> bool {
let mut spaces = spaces.iter();
let clues = self.clues.iter();

// Run the first space manually.
let mut index = spaces.next().cloned().unwrap();
if self.cells[0..index].iter().any(|c| *c == Cell::Filled) {
return false;
fn process(gears: &[Condition], groups: &[usize]) -> usize {
// The dfa has 2 states for the start and end with \.* patterns
// Each group G gets g = |G| states for the line of # {g}
// Between each group 1 state in inserted for the \.+ pattern

let mut states: Vec<States> = groups
.iter()
.flat_map(|g| {
std::iter::once(States::Spacing)
.chain(std::iter::repeat(States::AcceptTag).take(g - 1))
.chain(std::iter::once(States::AcceptDot))
})
.collect();
states.push(States::Ending);

let mut counts = vec![0_usize; states.len() + 1];
let mut next = counts.clone();

// Start with 1 DFA in the starting state
counts[0] = 1;

for cond in gears {
for (idx, (state, count)) in states.iter().zip(counts.iter()).enumerate() {
match (state, cond) {
(States::Spacing, Condition::Good) => next[idx] += count,
(States::Spacing, Condition::Bad) => next[idx + 1] += count,
(States::Spacing, Condition::Unknown) => {
next[idx] += count;
next[idx + 1] += count;
}
(States::AcceptDot, Condition::Good) => next[idx + 1] += count,
(States::AcceptDot, Condition::Bad) => {}
(States::AcceptDot, Condition::Unknown) => next[idx + 1] += count,
(States::AcceptTag, Condition::Good) => {}
(States::AcceptTag, Condition::Bad) => next[idx + 1] += count,
(States::AcceptTag, Condition::Unknown) => next[idx + 1] += count,
(States::Ending, Condition::Good) => next[idx] += count,
(States::Ending, Condition::Bad) => {}
(States::Ending, Condition::Unknown) => next[idx] += count,
}
}

// Loop over the rest of pairs of spaces and clues.
debug_assert_eq!(spaces.len(), clues.len());
counts = vec![0; states.len()];
std::mem::swap(&mut counts, &mut next);
}

for (&clue, &space) in clues.zip(spaces) {
// Check that the clues are filled.
if self.cells[index..index + clue]
.iter()
.any(|c| *c == Cell::Empty)
{
return false;
}
counts[counts.len() - 2] + counts[counts.len() - 1]
}

index += clue;
fn part1(input: &str) -> usize {
input
.lines()
.map(parse_line)
.map(|(gears, groups)| process(&gears, &groups))
.sum()
}

// Check that the spaces are empty.
if self.cells[index..index + space]
.iter()
.any(|c| *c == Cell::Filled)
{
return false;
fn part2(input: &str) -> usize {
input
.lines()
.map(parse_line)
.map(|(gears, groups)| {
let mut joined_gears = gears.clone();
for _ in 0..4 {
joined_gears.push(Condition::Unknown);
joined_gears.extend(gears.iter().cloned());
}

index += space;
}

// If all tests passed, then this is a valid solution.
true
}

// Part 1, find number of possible solutions.
fn part1(&self) -> usize {
// empty squares + filled squares = total squares
//
// or we can rearrange to:
//
// empty squares = total squares - filled squares
let empty_squares = self.cells.len() - self.clues.iter().sum::<usize>();

// We start by giving each clue a single empty square to its sides.
let mut spaces = vec![0; self.clues.len() + 1];
// The middle spaces are at least length 1
let range = 1..=self.clues.len() - 1;
spaces[range].iter_mut().for_each(|s| *s = 1);

// In total we have empty_squares - self.clues.len() spaces to
// distribute among all the spaces.

let left_over = empty_squares - (self.clues.len() - 1);

let set = (0..spaces.len()).collect_vec();
set.into_iter()
.combinations_with_replacement(left_over)
.par_bridge()
.map(|s| {
let mut spaces = spaces.clone();
for i in s.iter() {
spaces[*i] += 1;
}
spaces
})
.filter(|s| {
// Check that the spaces work.
self.check(s)
})
.count()
}
(joined_gears, groups.repeat(5))
})
.map(|(gears, groups)| process(&gears, &groups))
.sum()
}

fn main() {
let input = input_str!(2023, 12);
let rows = input.lines().map(Row::from_str).collect::<Vec<_>>();

println!("Part 1: {}", rows.iter().map(Row::part1).sum::<usize>());

// Part 2: Duplicated cells and clues by 5
println!(
"Part 2: {}",
rows.into_par_iter()
.enumerate()
.map(|(i, mut row)| {
row.duplicate();
let result = row.part1();
println!("{}: {}", i, result);
result
})
.sum::<usize>()
);
let part1 = time("Part 1", || part1(input));
println!("Part 1: {}", part1);
let part2 = time("Part 2", || part2(input));
println!("Part 2: {}", part2);
}

#[cfg(test)]
mod test {
use super::*;
mod tests {
use crate::*;

#[test]
fn test_parsing() {
let input = "???.### 1,1,3";
let conditions = vec![
Condition::Unknown,
Condition::Unknown,
Condition::Unknown,
Condition::Good,
Condition::Bad,
Condition::Bad,
Condition::Bad,
];
let gears: Vec<usize> = vec![1, 1, 3];
assert_eq!((conditions, gears), parse_line(input));
}

#[test]
fn examples() {
let tests = [
("???.### 1,1,3", 1, 1),
(".??..??...?##. 1,1,3", 4, 16384),
("?#?#?#?#?#?#?#? 1,3,1,6", 1, 1),
("????.#...#... 4,1,1", 1, 16),
("????.######..#####. 1,6,5", 4, 2500),
("?###???????? 3,2,1", 10, 506250),
];

for (input, p1, p2) in tests {
assert_eq!(part1(input), p1, "{}", input);
assert_eq!(part2(input), p2, "{}", input);
}
}

#[test]
fn test_column() {
// ???.### 1,1,3
let row = Row::from_str("???.### 1,1,3");
assert_eq!(
row.cells,
vec![
Cell::Unknown,
Cell::Unknown,
Cell::Unknown,
Cell::Empty,
Cell::Filled,
Cell::Filled,
Cell::Filled
]
);
assert_eq!(row.clues, vec![1, 1, 3]);
fn answer_part1() {
let input = input_str!(2023, 12);
assert_eq!(part1(input), 7402);
}

#[test]
fn test_example() {
let input = "???.### 1,1,3\n.??..??...?##. 1,1,3\n?#?#?#?#?#?#?#? 1,3,1,6\n????.#...#... 4,1,1\n????.######..#####. 1,6,5\n?###???????? 3,2,1";
let rows = input.lines().map(Row::from_str).collect::<Vec<_>>();

assert_eq!(rows[0].part1(), 1);
assert_eq!(rows[1].part1(), 4);
assert_eq!(rows[2].part1(), 1);
assert_eq!(rows[3].part1(), 1);
assert_eq!(rows[4].part1(), 4);
assert_eq!(rows[5].part1(), 10);
fn answer_part2() {
let input = input_str!(2023, 12);
assert_eq!(part2(input), 3384337640277);
}
}
1 change: 1 addition & 0 deletions y23/d17/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ edition = "2021"

[dependencies]
aoc = { path = "../../aoc" }
smallvec = "1.11.2"
Loading

0 comments on commit f9c3c7a

Please sign in to comment.