sim_lib_discrete_search/
word.rs1use std::collections::BTreeSet;
4
5use crate::{SearchError, SearchProblem, SearchStep};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct WordSearchState {
10 pub prefix: Vec<String>,
12 pub score: i64,
14}
15
16#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct WordSearchSolution {
19 pub word: Vec<String>,
21 pub score: i64,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct ConstrainedWordProblem {
28 alphabet: Vec<String>,
29 length: usize,
30 required_first: String,
31 required_last: String,
32 forbidden_pairs: BTreeSet<(String, String)>,
33 costly_symbol: Option<String>,
34}
35
36impl ConstrainedWordProblem {
37 pub fn new(
39 alphabet: Vec<String>,
40 length: usize,
41 required_first: String,
42 required_last: String,
43 ) -> Result<Self, SearchError> {
44 if alphabet.is_empty() {
45 return Err(SearchError::InvalidProblem(
46 "alphabet must not be empty".to_string(),
47 ));
48 }
49 if length < 2 {
50 return Err(SearchError::InvalidProblem(
51 "word length must be at least two".to_string(),
52 ));
53 }
54 let unique = alphabet.iter().collect::<BTreeSet<_>>();
55 if unique.len() != alphabet.len() {
56 return Err(SearchError::InvalidProblem(
57 "alphabet entries must be unique".to_string(),
58 ));
59 }
60 if !unique.contains(&required_first) || !unique.contains(&required_last) {
61 return Err(SearchError::InvalidProblem(
62 "required endpoints must be in the alphabet".to_string(),
63 ));
64 }
65 let costly_symbol = alphabet.get(1).cloned();
66 let mut forbidden_pairs = BTreeSet::new();
67 if alphabet.len() >= 3 {
68 forbidden_pairs.insert((required_last.clone(), alphabet[1].clone()));
69 }
70 Ok(Self {
71 alphabet,
72 length,
73 required_first,
74 required_last,
75 forbidden_pairs,
76 costly_symbol,
77 })
78 }
79
80 pub fn with_forbidden_pair(mut self, left: String, right: String) -> Self {
82 self.forbidden_pairs.insert((left, right));
83 self
84 }
85
86 pub fn alphabet(&self) -> &[String] {
88 &self.alphabet
89 }
90
91 pub fn length(&self) -> usize {
93 self.length
94 }
95}
96
97impl SearchProblem for ConstrainedWordProblem {
98 type State = WordSearchState;
99 type Choice = String;
100 type Output = WordSearchSolution;
101
102 fn initial_state(&self) -> Self::State {
103 WordSearchState {
104 prefix: Vec::new(),
105 score: 0,
106 }
107 }
108
109 fn expand(&self, state: &Self::State, out: &mut Vec<Self::Choice>) {
110 if state.prefix.len() < self.length {
111 out.extend(self.alphabet.iter().cloned());
112 }
113 }
114
115 fn apply(&self, state: &Self::State, choice: &Self::Choice) -> SearchStep<Self::State> {
116 let position = state.prefix.len();
117 if position == 0 && choice != &self.required_first {
118 return SearchStep::pruned("prefix does not match required first symbol");
119 }
120 if position + 1 == self.length && choice != &self.required_last {
121 return SearchStep::pruned("suffix does not match required last symbol");
122 }
123 if state.prefix.last() == Some(choice) {
124 return SearchStep::pruned("adjacent symbols must differ");
125 }
126 if let Some(previous) = state.prefix.last()
127 && self
128 .forbidden_pairs
129 .contains(&(previous.clone(), choice.clone()))
130 {
131 return SearchStep::pruned("forbidden adjacent pair");
132 }
133
134 let mut prefix = state.prefix.clone();
135 prefix.push(choice.clone());
136 let score = state.score + i64::from(self.costly_symbol.as_ref() == Some(choice));
137 SearchStep::Continue(WordSearchState { prefix, score })
138 }
139
140 fn propagate(&self, state: Self::State) -> SearchStep<Self::State> {
141 if state.prefix.len() > self.length {
142 return SearchStep::infeasible("prefix exceeds target length");
143 }
144 let remaining = self.length - state.prefix.len();
145 if remaining == 1 && state.prefix.last() == Some(&self.required_last) {
146 return SearchStep::pruned("required suffix would repeat");
147 }
148 SearchStep::Continue(state)
149 }
150
151 fn finish(&self, state: &Self::State) -> Option<Self::Output> {
152 (state.prefix.len() == self.length && state.prefix.last() == Some(&self.required_last))
153 .then(|| WordSearchSolution {
154 word: state.prefix.clone(),
155 score: state.score,
156 })
157 }
158
159 fn score_state(&self, state: &Self::State) -> i64 {
160 state.score
161 }
162
163 fn estimate_remaining(&self, _state: &Self::State) -> i64 {
164 0
165 }
166
167 fn bound(&self, state: &Self::State) -> Option<i64> {
168 Some(state.score)
169 }
170
171 fn output_score(&self, output: &Self::Output) -> Option<i64> {
172 Some(output.score)
173 }
174}
175
176pub fn render_constrained_word_demo(
178 solutions: &[WordSearchSolution],
179 receipt_digest: &str,
180) -> String {
181 let mut out = String::new();
182 for solution in solutions {
183 out.push_str(&solution.word.join(""));
184 out.push('\t');
185 out.push_str(&solution.score.to_string());
186 out.push('\n');
187 }
188 out.push_str("receipt\t");
189 out.push_str(receipt_digest);
190 out.push('\n');
191 out
192}