Skip to main content

problemreductions/models/graph/
generalized_hex.rs

1//! Generalized Hex problem implementation.
2//!
3//! Generalized Hex asks whether the first player has a forced win in the
4//! vertex-claiming Shannon switching game on an undirected graph.
5
6use 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/// Generalized Hex on an undirected graph.
34///
35/// The problem is represented as a zero-variable decision problem: the graph
36/// instance fully determines the question, so `evaluate([])` runs a memoized
37/// game-tree search from the initial empty board.
38#[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    /// Create a new Generalized Hex instance.
55    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    /// Get a reference to the underlying graph.
68    pub fn graph(&self) -> &G {
69        &self.graph
70    }
71
72    /// Get the source terminal.
73    pub fn source(&self) -> usize {
74        self.source
75    }
76
77    /// Get the target terminal.
78    pub fn target(&self) -> usize {
79        self.target
80    }
81
82    /// Get the number of vertices in the graph.
83    pub fn num_vertices(&self) -> usize {
84        self.graph.num_vertices()
85    }
86
87    /// Get the number of edges in the graph.
88    pub fn num_edges(&self) -> usize {
89        self.graph.num_edges()
90    }
91
92    /// Get the number of playable non-terminal vertices.
93    pub fn num_playable_vertices(&self) -> usize {
94        self.graph.num_vertices().saturating_sub(2)
95    }
96
97    /// Check whether the first player has a forced win from the initial state.
98    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;