problemreductions/models/graph/
generalized_hex.rs1use std::collections::{HashMap, VecDeque};
7
8use serde::{Deserialize, Serialize};
9
10use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension};
11use crate::topology::{Graph, SimpleGraph};
12use crate::traits::Problem;
13use crate::variant::VariantParam;
14
15inventory::submit! {
16 ProblemSchemaEntry {
17 name: "GeneralizedHex",
18 display_name: "Generalized Hex",
19 aliases: &[],
20 dimensions: &[
21 VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]),
22 ],
23 module_path: module_path!(),
24 description: "Determine whether Player 1 has a forced blue path between two terminals",
25 fields: &[
26 FieldInfo { name: "graph", type_name: "G", description: "The underlying graph G=(V,E)" },
27 FieldInfo { name: "source", type_name: "usize", description: "The source terminal s" },
28 FieldInfo { name: "target", type_name: "usize", description: "The target terminal t" },
29 ],
30 }
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
39#[serde(bound(deserialize = "G: serde::Deserialize<'de>"))]
40pub struct GeneralizedHex<G> {
41 graph: G,
42 source: usize,
43 target: usize,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47enum ClaimState {
48 Unclaimed,
49 Blue,
50 Red,
51}
52
53impl<G: Graph> GeneralizedHex<G> {
54 pub fn new(graph: G, source: usize, target: usize) -> Self {
56 let num_vertices = graph.num_vertices();
57 assert!(source < num_vertices, "source must be a valid graph vertex");
58 assert!(target < num_vertices, "target must be a valid graph vertex");
59 assert_ne!(source, target, "source and target must be distinct");
60 Self {
61 graph,
62 source,
63 target,
64 }
65 }
66
67 pub fn graph(&self) -> &G {
69 &self.graph
70 }
71
72 pub fn source(&self) -> usize {
74 self.source
75 }
76
77 pub fn target(&self) -> usize {
79 self.target
80 }
81
82 pub fn num_vertices(&self) -> usize {
84 self.graph.num_vertices()
85 }
86
87 pub fn num_edges(&self) -> usize {
89 self.graph.num_edges()
90 }
91
92 pub fn num_playable_vertices(&self) -> usize {
94 self.graph.num_vertices().saturating_sub(2)
95 }
96
97 pub fn is_valid_solution(&self, config: &[usize]) -> bool {
99 if !config.is_empty() {
100 return false;
101 }
102 let playable_vertices = self.playable_vertices();
103 let vertex_to_state_index = self.vertex_to_state_index(&playable_vertices);
104 let mut state = vec![ClaimState::Unclaimed; playable_vertices.len()];
105 let mut memo = HashMap::new();
106 self.first_player_wins(&mut state, &vertex_to_state_index, &mut memo)
107 }
108
109 fn playable_vertices(&self) -> Vec<usize> {
110 (0..self.graph.num_vertices())
111 .filter(|&vertex| vertex != self.source && vertex != self.target)
112 .collect()
113 }
114
115 fn vertex_to_state_index(&self, playable_vertices: &[usize]) -> Vec<Option<usize>> {
116 let mut index = vec![None; self.graph.num_vertices()];
117 for (state_idx, &vertex) in playable_vertices.iter().enumerate() {
118 index[vertex] = Some(state_idx);
119 }
120 index
121 }
122
123 fn first_player_wins(
124 &self,
125 state: &mut [ClaimState],
126 vertex_to_state_index: &[Option<usize>],
127 memo: &mut HashMap<Vec<ClaimState>, bool>,
128 ) -> bool {
129 if self.has_path(state, vertex_to_state_index, |claim| {
130 matches!(claim, ClaimState::Blue)
131 }) {
132 return true;
133 }
134 if !self.has_path(state, vertex_to_state_index, |claim| {
135 claim != ClaimState::Red
136 }) {
137 return false;
138 }
139 if let Some(&cached) = memo.get(state) {
140 return cached;
141 }
142
143 let blue_turn = state
144 .iter()
145 .filter(|&&claim| !matches!(claim, ClaimState::Unclaimed))
146 .count()
147 % 2
148 == 0;
149
150 let result = if blue_turn {
151 let mut winning_move_found = false;
152 for idx in 0..state.len() {
153 if !matches!(state[idx], ClaimState::Unclaimed) {
154 continue;
155 }
156 state[idx] = ClaimState::Blue;
157 if self.first_player_wins(state, vertex_to_state_index, memo) {
158 winning_move_found = true;
159 state[idx] = ClaimState::Unclaimed;
160 break;
161 }
162 state[idx] = ClaimState::Unclaimed;
163 }
164 winning_move_found
165 } else {
166 let mut all_red_moves_still_win = true;
167 for idx in 0..state.len() {
168 if !matches!(state[idx], ClaimState::Unclaimed) {
169 continue;
170 }
171 state[idx] = ClaimState::Red;
172 if !self.first_player_wins(state, vertex_to_state_index, memo) {
173 all_red_moves_still_win = false;
174 state[idx] = ClaimState::Unclaimed;
175 break;
176 }
177 state[idx] = ClaimState::Unclaimed;
178 }
179 all_red_moves_still_win
180 };
181
182 memo.insert(state.to_vec(), result);
183 result
184 }
185
186 fn has_path<F>(
187 &self,
188 state: &[ClaimState],
189 vertex_to_state_index: &[Option<usize>],
190 allow_claim: F,
191 ) -> bool
192 where
193 F: Fn(ClaimState) -> bool,
194 {
195 let mut visited = vec![false; self.graph.num_vertices()];
196 let mut queue = VecDeque::from([self.source]);
197 visited[self.source] = true;
198
199 while let Some(vertex) = queue.pop_front() {
200 if vertex == self.target {
201 return true;
202 }
203
204 for neighbor in self.graph.neighbors(vertex) {
205 if visited[neighbor]
206 || !self.vertex_is_allowed(neighbor, state, vertex_to_state_index, &allow_claim)
207 {
208 continue;
209 }
210 visited[neighbor] = true;
211 queue.push_back(neighbor);
212 }
213 }
214
215 false
216 }
217
218 fn vertex_is_allowed<F>(
219 &self,
220 vertex: usize,
221 state: &[ClaimState],
222 vertex_to_state_index: &[Option<usize>],
223 allow_claim: &F,
224 ) -> bool
225 where
226 F: Fn(ClaimState) -> bool,
227 {
228 if vertex == self.source || vertex == self.target {
229 return true;
230 }
231 vertex_to_state_index[vertex]
232 .and_then(|state_idx| state.get(state_idx).copied())
233 .is_some_and(allow_claim)
234 }
235}
236
237impl<G> Problem for GeneralizedHex<G>
238where
239 G: Graph + VariantParam,
240{
241 const NAME: &'static str = "GeneralizedHex";
242 type Value = crate::types::Or;
243
244 fn variant() -> Vec<(&'static str, &'static str)> {
245 crate::variant_params![G]
246 }
247
248 fn dims(&self) -> Vec<usize> {
249 vec![]
250 }
251
252 fn evaluate(&self, config: &[usize]) -> crate::types::Or {
253 crate::types::Or({
254 if !config.is_empty() {
255 return crate::types::Or(false);
256 }
257 let playable_vertices = self.playable_vertices();
258 let vertex_to_state_index = self.vertex_to_state_index(&playable_vertices);
259 let mut state = vec![ClaimState::Unclaimed; playable_vertices.len()];
260 let mut memo = HashMap::new();
261 self.first_player_wins(&mut state, &vertex_to_state_index, &mut memo)
262 })
263 }
264}
265
266crate::declare_variants! {
267 default GeneralizedHex<SimpleGraph> => "3^num_playable_vertices",
268}
269
270#[cfg(feature = "example-db")]
271pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
272 vec![crate::example_db::specs::ModelExampleSpec {
273 id: "generalized_hex_simplegraph",
274 instance: Box::new(GeneralizedHex::new(
275 SimpleGraph::new(
276 6,
277 vec![(0, 1), (0, 2), (0, 3), (1, 4), (2, 4), (3, 4), (4, 5)],
278 ),
279 0,
280 5,
281 )),
282 optimal_config: vec![],
283 optimal_value: serde_json::json!(true),
284 }]
285}
286
287#[cfg(test)]
288#[path = "../../unit_tests/models/graph/generalized_hex.rs"]
289mod tests;