Skip to content

Commit 7f48eae

Browse files
committed
Move to incremental checking of constraints
1 parent 802eaf1 commit 7f48eae

File tree

1 file changed

+148
-113
lines changed

1 file changed

+148
-113
lines changed

src/day12.rs

Lines changed: 148 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -28,71 +28,150 @@ impl Display for Condition {
2828
}
2929
}
3030

31-
struct ConditionRecord {
31+
// State when searching for arrangements
32+
struct State {
3233
conditions: Vec<Condition>,
33-
damaged_spring_groups: Vec<usize>,
34+
last_damaged_count: usize,
35+
last_damaged_spring_groups_index: usize,
36+
invalid: bool,
37+
needs_constraint_check: bool,
3438
}
3539

36-
impl ConditionRecord {
37-
#[cfg(test)]
38-
pub fn new(conditions: &[Condition], damaged_spring_groups: &[usize]) -> ConditionRecord {
39-
ConditionRecord {
40-
conditions: conditions.to_vec(),
41-
damaged_spring_groups: damaged_spring_groups.to_vec(),
40+
impl State {
41+
fn empty() -> State {
42+
State {
43+
conditions: vec![],
44+
last_damaged_count: 0,
45+
last_damaged_spring_groups_index: 0,
46+
invalid: false,
47+
needs_constraint_check: false,
48+
}
49+
}
50+
fn and(&self, c: Condition) -> State {
51+
if self.invalid {
52+
panic!("cannot 'and' to an invalid state");
53+
}
54+
let mut new_conditions = self.conditions.clone();
55+
new_conditions.push(c);
56+
State {
57+
conditions: new_conditions,
58+
last_damaged_count: self.last_damaged_count,
59+
last_damaged_spring_groups_index: self.last_damaged_spring_groups_index,
60+
invalid: false,
61+
needs_constraint_check: true,
4262
}
4363
}
4464

65+
66+
// Check whether the given conditions violates the constraints given by the `damaged_spring_groups`
67+
// configuration.
68+
// If `partial` is true then the check accepts conditions that are incomplete and could be completed
69+
// to a full valid configuration, if it is false then the conditions must match perfectly.
70+
fn check_constraints(&mut self, damaged_spring_groups: &[usize], partial: bool) -> bool {
71+
// Invalid states stay invalid, and we can skip checks when nothing has changed
72+
// since the last check.
73+
if self.invalid || !self.needs_constraint_check {
74+
return !self.invalid;
75+
}
76+
77+
// needs_constraint_check is true on a state that recently got a new condition, so we check
78+
// whether the last condition is not breaking anything.
79+
match self.conditions.last() {
80+
Some(Condition::Operational) => {
81+
// An operational condition was added, so any incomplete damaged group would now be complete and must match exactly
82+
// the indicated group.
83+
if self.last_damaged_count > 0 {
84+
// A group was open, close it and match it.
85+
if let Some(expected_damaged_count) = damaged_spring_groups.get(self.last_damaged_spring_groups_index) {
86+
let result = *expected_damaged_count == self.last_damaged_count;
87+
self.invalid = !result;
88+
} else {
89+
// State is invalid: keep the constraint check flag on for now, so that the next one
90+
// will also flag it.
91+
self.invalid = true;
92+
}
93+
self.last_damaged_count = 0;
94+
self.last_damaged_spring_groups_index += 1;
95+
}
96+
},
97+
Some(Condition::Damaged) => {
98+
// A damaged condition was added, so we need to check whether that is still fitting the current index.
99+
self.last_damaged_count += 1;
100+
if let Some(expected_damaged_count) = damaged_spring_groups.get(self.last_damaged_spring_groups_index) {
101+
let result = if partial {
102+
*expected_damaged_count >= self.last_damaged_count
103+
} else {
104+
*expected_damaged_count == self.last_damaged_count
105+
};
106+
self.invalid = !result;
107+
} else {
108+
// State is invalid: keep the constraint check flag on for now, so that the next one
109+
// will also flag it.
110+
self.invalid = true;
111+
}
112+
},
113+
Some(condition) => {
114+
panic!("unexpected condition {}", condition);
115+
},
116+
None => {
117+
// Nothing to do really, the thing is empty. But: How did you get here?
118+
panic!("huh? state is empty");
119+
},
120+
}
121+
122+
// If we're not doing a partial check, then the group index should point after the end
123+
// of the groups, i.e. there is no missing group.
124+
if !self.invalid && !partial {
125+
self.invalid = self.last_damaged_spring_groups_index >= damaged_spring_groups.len();
126+
}
127+
128+
// Mark check as done, and return whether we're now invalid or not.
129+
self.needs_constraint_check = false;
130+
!self.invalid
131+
}
132+
}
133+
134+
impl Display for State {
135+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136+
let cond_str = std::str::from_utf8(&self.conditions.iter().map(|c| *c as u8).collect::<Vec<u8>>()).unwrap().to_string();
137+
write!(f, "\"{}\" (ldc = {}, i = {}", cond_str, self.last_damaged_count, self.last_damaged_spring_groups_index)
138+
}
139+
}
140+
141+
struct ConditionRecord {
142+
conditions: Vec<Condition>,
143+
damaged_spring_groups: Vec<usize>,
144+
}
145+
146+
impl ConditionRecord {
45147
pub fn num_arrangements(&self) -> usize {
46148
// Stupid logic: Each '?' can be either '.' or '#'. So, try both, and then proceed
47149
// and see if _at the end_. Essentially we're building a matching automaton here?????!?!?!!
48150
// Each state is the conditions we found.
49-
let states: &mut Vec<Vec<Condition>> = &mut vec![];
151+
let states: &mut Vec<State> = &mut vec![];
50152

51153
// Start out with one empty state:
52-
let initial_state = vec![];
53-
states.push(initial_state);
154+
states.push(State::empty());
54155
for c in &self.conditions {
55-
// println!("{:?} with states {}", c, states.iter().map(|s| Self::format_state(s)).collect::<Vec<_>>().join(","));
56-
57-
// Modify the states depending on the condition
58-
// If we see a '.' or '#' then it will be added immediately to the states, which may
59-
// make some switch from "could work" to "invalid".
60-
// If we see a '?' we fork the state, and keep the the valid parts.
61-
// Adding a '.' after a '.' will not actually change anything, so we can skip
62-
// the checks in that case.
63156
let next_states: Vec<_> = states.iter().flat_map(|s| {
64-
// println!(" ... looking at {}{}", Self::format_state(s), *c as u8 as char);
65157
match c {
66158
Condition::Unknown => {
67159
let mut tmp = vec![];
68-
let mut s1 = s.clone();
69-
s1.push(Condition::Damaged);
70-
if !self.violates_constraints(&s1, true) {
71-
// println!(" ... {:?} is acceptable", Self::format_state(&s1));
160+
let mut s1 = s.and(Condition::Damaged);
161+
if s1.check_constraints(&self.damaged_spring_groups, true) {
72162
tmp.push(s1);
73-
} else {
74-
// println!(" ... {:?} violates constraints", Self::format_state(&s1));
75163
}
76-
77-
let last = s.last();
78-
let mut s2 = s.clone();
79-
s2.push(Condition::Operational);
80-
if (last.is_some_and(|lc| *lc == Condition::Operational) && *c == Condition::Operational) || !self.violates_constraints(&s2, true) {
81-
// println!(" ... {:?} is acceptable", Self::format_state(&s2));
164+
let mut s2 = s.and(Condition::Operational);
165+
if s2.check_constraints(&self.damaged_spring_groups, true) {
82166
tmp.push(s2);
83-
} else {
84-
// println!(" ... {:?} violates constraints", Self::format_state(&s2));
85167
}
86168
tmp
87169
},
88-
_ => {
89-
let last = s.last();
90-
let mut s1 = s.clone();
91-
s1.push(*c);
92-
if (last.is_some_and(|lc| *lc == Condition::Operational) && *c == Condition::Operational) || !self.violates_constraints(&s1, true) {
170+
c => {
171+
let mut s1 = s.and(*c);
172+
if s1.check_constraints(&self.damaged_spring_groups, true) {
93173
vec![s1]
94174
} else {
95-
// println!(" ... {:?} violates constraints", Self::format_state(&s1));
96175
vec![]
97176
}
98177
}
@@ -106,65 +185,13 @@ impl ConditionRecord {
106185
*states = next_states;
107186
}
108187

109-
states.iter().filter(|s| {
110-
let ok = !self.violates_constraints(s, false);
111-
// if ok {
112-
// println!("{}", Self::format_state(s));
113-
// }
114-
ok
115-
}).count()
116-
}
117-
118-
// Check whether the given conditions violates the constraints given by the `damaged_spring_groups`
119-
// configuration.
120-
// If `partial` is true then the check accepts conditions that are incomplete and could be completed
121-
// to a full valid configuration, if it is false then the conditions must match perfectly.
122-
fn violates_constraints(&self, conditions: &[Condition], partial: bool) -> bool {
123-
let mut next_spring_group = self.damaged_spring_groups.iter();
124-
let mut damaged_count = 0;
125-
for c in conditions {
126-
if *c == Condition::Operational {
127-
if damaged_count > 0 {
128-
match next_spring_group.next() {
129-
Some(expected_damaged_count) => {
130-
if *expected_damaged_count != damaged_count {
131-
return true;
132-
}
133-
},
134-
None => {
135-
return true;
136-
},
137-
}
138-
damaged_count = 0;
139-
}
140-
} else if *c == Condition::Damaged {
141-
damaged_count += 1;
142-
} else {
143-
panic!("unexpected condition {:?}", c)
144-
}
145-
}
146-
147-
// We're only checking whether the given conditions could still pass, so at this point
148-
// we only want "close enough": If we collected some damaged springs, then the next value
149-
// in the iteration must not be smaller than that.
150-
// For a "full" check we would also verify that there is nothing left over at the end, and
151-
// any collected damamged springs would have to match exactly.
152-
match next_spring_group.next() {
153-
Some(expected_damaged_count) => {
154-
if partial {
155-
damaged_count > *expected_damaged_count
156-
} else {
157-
damaged_count != *expected_damaged_count || next_spring_group.next().is_some()
158-
}
159-
}
160-
None => {
161-
damaged_count > 0
188+
let mut result = 0;
189+
for s in states.iter_mut() {
190+
if s.check_constraints(&self.damaged_spring_groups, false) {
191+
result += 1;
162192
}
163193
}
164-
}
165-
166-
fn format_state(state: &[Condition]) -> String {
167-
std::str::from_utf8(&state.iter().map(|c| *c as u8).collect::<Vec<u8>>()).unwrap().to_string()
194+
result
168195
}
169196

170197
fn parse_state(s: &str) -> Vec<Condition> {
@@ -253,26 +280,34 @@ mod tests {
253280
}
254281
}
255282

283+
fn build_state(s: &str, damaged_spring_groups: &[usize]) -> Result<State, &'static str> {
284+
let mut result = State::empty();
285+
for c in s.chars() {
286+
let cond = Condition::from(c);
287+
result = result.and(cond);
288+
if !result.check_constraints(damaged_spring_groups, true) {
289+
return Err("cannot build state");
290+
}
291+
}
292+
Ok(result)
293+
}
294+
256295
#[test]
257-
fn condition_record_violates_constraints_partial() {
258-
assert!(!ConditionRecord::new(&[], &[]).violates_constraints(&[], true), "empty allows empty");
259-
assert!(!ConditionRecord::new(&[], &[1]).violates_constraints(&[], true), "non-empty allows empty");
260-
assert!(!ConditionRecord::new(&[], &[1]).violates_constraints(&[Condition::Damaged], true), "must match single");
261-
assert!(!ConditionRecord::new(&[], &[2,1]).violates_constraints(&[Condition::Damaged, Condition::Damaged, Condition::Operational, Condition::Damaged], true), "must match multiple with operational in the middle");
262-
assert!(!ConditionRecord::new(&[], &[2,2]).violates_constraints(&[Condition::Damaged, Condition::Damaged, Condition::Operational, Condition::Damaged], true), "last can be incomplete");
296+
fn state_check_constraints_partial() {
297+
assert!(State::empty().check_constraints(&[], true), "empty allows empty");
298+
let dsg1 = &[1];
299+
assert!(State::empty().check_constraints(dsg1, true), "non-empty allows empty");
300+
assert!(build_state("#", dsg1).unwrap().check_constraints(dsg1, true), "must match single");
301+
let dsg21 = &[2, 1];
302+
assert!(build_state("##.#", dsg21).unwrap().check_constraints(dsg21, true), "must match multiple with operational in the middle");
303+
let dsg22 = &[2, 2];
304+
assert!(build_state("##.#", dsg22).unwrap().check_constraints(dsg22, true), "last can be incomplete");
263305
}
264306

265307
#[test]
266-
fn condition_record_violates_constraints_partial_small() {
267-
assert!(ConditionRecord::new(&[], &[3,2,1]).violates_constraints(&[
268-
Condition::Operational,
269-
Condition::Damaged,
270-
Condition::Damaged,
271-
Condition::Damaged,
272-
Condition::Operational,
273-
Condition::Damaged,
274-
Condition::Operational,
275-
], false));
308+
fn state_check_constraints_small() {
309+
let dsg321 = &[3, 2, 1];
310+
assert!(build_state(".###.#.", dsg321).is_err());
276311
}
277312

278313
#[test]

0 commit comments

Comments
 (0)