-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1e9a91e
commit f9c3c7a
Showing
4 changed files
with
301 additions
and
183 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,5 +7,3 @@ edition = "2021" | |
|
||
[dependencies] | ||
aoc = { path = "../../aoc" } | ||
itertools = "0.12.0" | ||
rayon = "1.8.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,3 +7,4 @@ edition = "2021" | |
|
||
[dependencies] | ||
aoc = { path = "../../aoc" } | ||
smallvec = "1.11.2" |
Oops, something went wrong.