Skip to content

Commit b45eeaa

Browse files
committed
Collapse similar states to reduce the processing effort
Ultimately it doesn't matter for the world if we're looking at `#..#..#` or `#.#...#`, they are identical in terms of the distribution of the groups, and therefore for the number of arrangements. By collapsing these states into each other regularly, and only keeping track of how many we collapsed, the processing time needed goes down from "days" to "seconds". Multithreading is still left in place, because it will be a good example to steal later, but the initial attempt of pruning was now completely useless and got removed.
1 parent f50f082 commit b45eeaa

File tree

1 file changed

+142
-43
lines changed

1 file changed

+142
-43
lines changed

src/day12.rs

Lines changed: 142 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::fmt::Display;
22

3-
#[derive(Clone, Copy, Debug, PartialEq)]
3+
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
44
enum Condition {
55
Operational = '.' as isize,
66
Damaged = '#' as isize,
@@ -29,13 +29,18 @@ impl Display for Condition {
2929
}
3030

3131
// State when searching for arrangements
32-
#[derive(Clone, Debug)]
32+
#[derive(Clone, Debug, Eq)]
3333
struct State {
3434
conditions: Vec<Condition>,
3535
unchecked_condition: Option<Condition>,
3636
last_damaged_count: usize,
3737
last_damaged_spring_groups_index: usize,
3838
invalid: bool,
39+
40+
// Number of states that collapsed into this one because
41+
// they had the same last_damaged_spring_groups_index, last_damaged_count and
42+
// last condition.
43+
num_similar: usize,
3944
}
4045

4146
impl State {
@@ -46,6 +51,7 @@ impl State {
4651
last_damaged_count: 0,
4752
last_damaged_spring_groups_index: 0,
4853
invalid: false,
54+
num_similar: 0,
4955
}
5056
}
5157
fn and(&self, c: Condition) -> State {
@@ -61,6 +67,7 @@ impl State {
6167
last_damaged_count: self.last_damaged_count,
6268
last_damaged_spring_groups_index: self.last_damaged_spring_groups_index,
6369
invalid: false,
70+
num_similar: self.num_similar,
6471
}
6572
}
6673

@@ -160,7 +167,7 @@ impl State {
160167
impl Display for State {
161168
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162169
let cond_str = std::str::from_utf8(&self.conditions.iter().map(|c| *c as u8).collect::<Vec<u8>>()).unwrap().to_string();
163-
let r = write!(f, "\"{}\" (ldc = {}, i = {}, invalid = {}", cond_str, self.last_damaged_count, self.last_damaged_spring_groups_index, self.invalid);
170+
let r = write!(f, "\"{}\" (ldc = {}, i = {}, invalid = {}, similar = {}", cond_str, self.last_damaged_count, self.last_damaged_spring_groups_index, self.invalid, self.num_similar);
164171
if r.is_err() {
165172
panic!("cannot format state");
166173
}
@@ -172,6 +179,80 @@ impl Display for State {
172179
}
173180
}
174181

182+
impl std::cmp::PartialEq for State {
183+
fn eq(&self, other: &Self) -> bool {
184+
if self.unchecked_condition.is_some() || other.unchecked_condition.is_some() {
185+
panic!("cannot compare states with unchecked conditions");
186+
}
187+
188+
// Easy things:
189+
if self.invalid != other.invalid {
190+
return false;
191+
}
192+
if self.conditions.len() != other.conditions.len() {
193+
return false;
194+
}
195+
if self.last_damaged_count != other.last_damaged_count {
196+
return false;
197+
}
198+
if self.last_damaged_spring_groups_index != other.last_damaged_spring_groups_index {
199+
return false;
200+
}
201+
202+
// Last condition must be the same:
203+
match (self.conditions.last(), other.conditions.last()) {
204+
(Some(last), Some(other_last)) => {
205+
if last != other_last {
206+
return false;
207+
}
208+
},
209+
(None, None) => {},
210+
_ => return false,
211+
}
212+
213+
// NB: We're not comparing num_similar, as that is just an internal
214+
// multiplier.
215+
true
216+
}
217+
}
218+
219+
impl std::cmp::PartialOrd for State {
220+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
221+
if self.unchecked_condition.is_some() || other.unchecked_condition.is_some() {
222+
panic!("cannot compare states with unchecked conditions");
223+
}
224+
225+
// First: Compare the parts we also use in the partial equality checks.
226+
match self.invalid.partial_cmp(&other.invalid) {
227+
Some(core::cmp::Ordering::Equal) => {}
228+
ord => return ord,
229+
}
230+
match self.conditions.len().partial_cmp(&other.conditions.len()) {
231+
Some(core::cmp::Ordering::Equal) => {}
232+
ord => return ord,
233+
}
234+
match self.last_damaged_count.partial_cmp(&other.last_damaged_count) {
235+
Some(core::cmp::Ordering::Equal) => {}
236+
ord => return ord,
237+
}
238+
match self.last_damaged_spring_groups_index.partial_cmp(&other.last_damaged_spring_groups_index) {
239+
Some(core::cmp::Ordering::Equal) => {}
240+
ord => return ord,
241+
}
242+
match self.conditions.last().partial_cmp(&other.conditions.last()) {
243+
Some(core::cmp::Ordering::Equal) => {}
244+
ord => return ord,
245+
}
246+
247+
// Rest "just" needs to be sane enough.
248+
match self.conditions.partial_cmp(&other.conditions) {
249+
Some(core::cmp::Ordering::Equal) => {}
250+
ord => return ord,
251+
}
252+
self.num_similar.partial_cmp(&other.num_similar)
253+
}
254+
}
255+
175256
struct ConditionRecord {
176257
conditions: Vec<Condition>,
177258
damaged_spring_groups: Vec<usize>,
@@ -188,7 +269,7 @@ impl ConditionRecord {
188269
}
189270

190271
pub fn num_arrangements(&self) -> usize {
191-
Self::arrangements(&self.conditions, &self.damaged_spring_groups, self.repeat, false).len()
272+
Self::arrangements(&self.conditions, &self.damaged_spring_groups, self.repeat, false).drain(..).fold(0, |acc, s| acc + s.num_similar + 1)
192273
}
193274

194275
fn process_state(damaged_spring_groups: &[usize], s: State, c: Condition, print: bool) -> Vec<State> {
@@ -220,7 +301,7 @@ impl ConditionRecord {
220301
result
221302
}
222303

223-
fn arrangements(conditions: &[Condition], damaged_spring_groups: &[usize], repeat: usize, print: bool) -> Vec<String> {
304+
fn arrangements(conditions: &[Condition], damaged_spring_groups: &[usize], repeat: usize, print: bool) -> Vec<State> {
224305
// Stupid logic: Each '?' can be either '.' or '#'. So, try both, and then proceed
225306
// and see if _at the end_. Essentially we're building a matching automaton here?????!?!?!!
226307
// Each state is the conditions we found.
@@ -231,18 +312,39 @@ impl ConditionRecord {
231312
states.push(State::empty());
232313
for r in 0..repeat {
233314
for (i, c) in conditions.iter().enumerate() {
234-
let next_states = &states
315+
let next_states = &mut states
235316
.drain(..)
236317
.flat_map(|s| Self::process_state(&repeated_damaged_spring_groups, s, *c, print))
237318
.collect::<Vec<_>>();
238-
if next_states.is_empty() {
239-
println!("no more states, can return empty early");
240-
return vec![];
241-
}
242-
*states = next_states.to_vec();
319+
320+
let next_states_len = next_states.len();
321+
322+
// Collapse states: Sort them, then drop equal ones and bump the num_similar counter. At the
323+
// end we just count these counters when summing up.
324+
states.sort_by(|a, b| a.partial_cmp(b).unwrap());
325+
let collapsed_states = &next_states
326+
.drain(..)
327+
.fold(vec![], |mut acc: Vec<State>, s| {
328+
if let Some(last) = acc.last_mut() {
329+
if *last == s {
330+
last.num_similar += s.num_similar + 1;
331+
if print {
332+
println!("... {} collapsed into {}", s, last);
333+
}
334+
} else {
335+
acc.push(s);
336+
}
337+
} else {
338+
acc.push(s);
339+
}
340+
acc
341+
});
342+
243343
if print {
244-
println!("{}/{}: {}/{} |states| = {:?}", r, repeat, i, conditions.len(), states.len());
344+
println!("{}/{}: {}/{} |next_states| = {:?}, |collapsed_states| = {:?}", r, repeat, i, conditions.len(), next_states_len, collapsed_states.len());
245345
}
346+
347+
*states = collapsed_states.to_vec();
246348
}
247349

248350
// Repeating adds a unknown condition, so process that.
@@ -254,47 +356,34 @@ impl ConditionRecord {
254356
.drain(..)
255357
.flat_map(|s| Self::process_state(&repeated_damaged_spring_groups, s, Condition::Unknown, print))
256358
.collect::<Vec<_>>();
257-
if next_states.is_empty() {
258-
println!("no more states, can return empty early");
259-
return vec![];
260-
}
261359

262-
// Prune everything that doesn't have at least a remote chance: We should have roughly
263-
// the same number of groups in this section than existed in the original damaged_spring_groups.
264-
states.clear();
265-
for s in next_states {
266-
// XXX: This is probably the same or related-to as last_damaged_group_index.
267-
// XXX: The problem is that we may have situations where the added Unknown actually makes some group
268-
// possible -- but I don't see yet how.
269-
let expected_num_groups = damaged_spring_groups.len() * (r + 1);
270-
if s.last_damaged_spring_groups_index >= expected_num_groups - 1 {
271-
states.push(s.clone());
272-
}
273-
}
360+
// We could do the collapsing here, but it will happen in the next iteration anyways
361+
// and shouldn't cost too much as we already did it just above.
362+
*states = next_states.to_vec();
363+
}
274364

275-
if print {
276-
println!("{}/{}: |states after repeat| = {:?}, dropped = {}", r, repeat, states.len(), next_states.len() - states.len());
277-
}
365+
if states.is_empty() {
366+
println!("no more states, can return empty early");
367+
return vec![];
278368
}
279369
}
280370

281371
// Filter out the invalid states
282-
let mut result = vec![];
372+
let mut final_states = vec![];
283373
for s in states.iter_mut() {
284374
let valid = s.check_constraints(&repeated_damaged_spring_groups, false, false);
285375
if valid {
286376
// println!("state = {}: counted, valid at full", s);
287-
let cond_str = std::str::from_utf8(&s.conditions.iter().map(|c| *c as u8).collect::<Vec<u8>>()).unwrap().to_string();
288-
result.push(cond_str);
377+
final_states.push(s.to_owned());
289378
} else {
290379
// println!("state = {}: pruned, invalid at full", s);
291380
}
292381
}
293382

294383
if print {
295-
println!("|result| = {:?}", result.len());
384+
println!("|result| = {:?}", final_states);
296385
}
297-
result
386+
final_states
298387
}
299388

300389
fn parse_state(s: &str) -> Vec<Condition> {
@@ -427,7 +516,7 @@ impl std::str::FromStr for ConditionRecord {
427516
}
428517
}
429518

430-
fn num_arrangements(input: &str, repeat: usize) -> usize {
519+
fn num_arrangements(input: &str, repeat: usize, concurrent: u8) -> usize {
431520
let thread_count_arc = std::sync::Arc::new((std::sync::Mutex::new(0u8), std::sync::Condvar::new()));
432521

433522
let mut v = vec![];
@@ -438,7 +527,7 @@ fn num_arrangements(input: &str, repeat: usize) -> usize {
438527
let (num, cvar) = &*thread_count;
439528

440529
let mut start = cvar
441-
.wait_while(num.lock().unwrap(), |start| *start >= 32)
530+
.wait_while(num.lock().unwrap(), |start| *start >= concurrent)
442531
.unwrap();
443532
*start += 1;
444533
drop(start);
@@ -469,7 +558,7 @@ pub fn main() {
469558
match std::fs::read_to_string("day12.input") {
470559
Ok(input) => {
471560
// println!("num_arrangements = {}", num_arrangements(&input, 1));
472-
println!("num_arrangements (part 2) = {}", num_arrangements(&input, 5));
561+
println!("num_arrangements (part 2) = {}", num_arrangements(&input, 5, 32));
473562
},
474563
Err(reason) => println!("error = {}", reason)
475564
}
@@ -566,17 +655,17 @@ mod tests {
566655
assert_eq!("????.######..#####. 1,6,5".parse::<ConditionRecord>().unwrap().num_arrangements(), 4);
567656
assert_eq!("?###???????? 3,2,1".parse::<ConditionRecord>().unwrap().num_arrangements(), 10);
568657

569-
assert_eq!(num_arrangements(DATA, 1), 21)
658+
assert_eq!(num_arrangements(DATA, 1, 1), 21)
570659
}
571660

572661
#[test]
573662
fn part1_tests() {
574663
//assert_eq!("???.#??#.??? 1,1,2,1".parse::<ConditionRecord>().unwrap().num_arrangements(), 10);
575664
let cr = "##???#??#?????????#? 11,6".parse::<ConditionRecord>().unwrap();
576-
assert_eq!(ConditionRecord::arrangements(&cr.conditions, &cr.damaged_spring_groups, 1, false), &[
665+
assert_eq!(ConditionRecord::arrangements(&cr.conditions, &cr.damaged_spring_groups, 1, false).len(), [
577666
"###########..######.",
578667
"###########...######",
579-
]);
668+
].len());
580669
}
581670

582671
#[test]
@@ -594,7 +683,17 @@ mod tests {
594683
assert_eq!("????.######..#####. 1,6,5".parse::<ConditionRecord>().unwrap().repeat(5).num_arrangements(), 2500);
595684
assert_eq!("?###???????? 3,2,1".parse::<ConditionRecord>().unwrap().repeat(5).num_arrangements(), 506250);
596685

597-
assert_eq!(num_arrangements(DATA, 5), 525152)
686+
assert_eq!(num_arrangements(DATA, 5, 1), 525152)
687+
}
688+
689+
#[test]
690+
fn part2_simple() {
691+
let cr = "????.#...#... 4,1,1".parse::<ConditionRecord>().unwrap();
692+
let states = ConditionRecord::arrangements(&cr.conditions, &cr.damaged_spring_groups, 5, true);
693+
for s in &states {
694+
println!("state = {}", s);
695+
}
696+
assert_eq!(states.iter().fold(0, |acc, s| acc + s.num_similar + 1), 16);
598697
}
599698

600699
#[test]

0 commit comments

Comments
 (0)