Skip to main content

problemreductions/models/graph/
minimum_geometric_connected_dominating_set.rs

1//! Minimum Geometric Connected Dominating Set.
2//!
3//! Given a set of points P in the plane and a distance threshold B > 0,
4//! find a minimum subset P' ⊆ P such that:
5//! 1. Every point in P \ P' is within Euclidean distance B of some point in P' (domination).
6//! 2. The subgraph induced on P' (edges between points within distance B) is connected.
7
8use crate::registry::{FieldInfo, ProblemSchemaEntry};
9use crate::traits::Problem;
10use crate::types::Min;
11use serde::{Deserialize, Serialize};
12use std::collections::VecDeque;
13
14inventory::submit! {
15    ProblemSchemaEntry {
16        name: "MinimumGeometricConnectedDominatingSet",
17        display_name: "Minimum Geometric Connected Dominating Set",
18        aliases: &[],
19        dimensions: &[],
20        module_path: module_path!(),
21        description: "Find minimum connected dominating set in a geometric point set",
22        fields: &[
23            FieldInfo {
24                name: "points",
25                type_name: "Vec<(f64, f64)>",
26                description: "The set of points P in the plane",
27            },
28            FieldInfo {
29                name: "radius",
30                type_name: "f64",
31                description: "The distance threshold B",
32            },
33        ],
34    }
35}
36
37/// Minimum Geometric Connected Dominating Set.
38///
39/// Given points P in the plane and distance threshold B > 0,
40/// find a minimum subset P' ⊆ P such that every point in P \ P'
41/// is within distance B of some point in P', and the subgraph
42/// induced on P' (edges between points within distance B) is connected.
43///
44/// # Example
45///
46/// ```
47/// use problemreductions::models::graph::MinimumGeometricConnectedDominatingSet;
48/// use problemreductions::{Problem, Solver, BruteForce};
49///
50/// // Four collinear points with spacing 3 and radius 3.5:
51/// // each point reaches its immediate neighbor but not two steps away.
52/// let points = vec![(0.0, 0.0), (3.0, 0.0), (6.0, 0.0), (9.0, 0.0)];
53/// let problem = MinimumGeometricConnectedDominatingSet::new(points, 3.5);
54///
55/// let solver = BruteForce::new();
56/// let witness = solver.find_witness(&problem).unwrap();
57/// let value = problem.evaluate(&witness).unwrap();
58/// assert_eq!(value, 2); // Two interior points dominate all and form a connected pair
59/// ```
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct MinimumGeometricConnectedDominatingSet {
62    /// The set of points in the plane.
63    points: Vec<(f64, f64)>,
64    /// The distance threshold B.
65    radius: f64,
66}
67
68impl MinimumGeometricConnectedDominatingSet {
69    /// Create a new instance.
70    ///
71    /// # Panics
72    /// Panics if `radius <= 0.0` or if `points` is empty.
73    pub fn new(points: Vec<(f64, f64)>, radius: f64) -> Self {
74        assert!(radius > 0.0, "radius must be positive");
75        assert!(!points.is_empty(), "points must be non-empty");
76        Self { points, radius }
77    }
78
79    /// Fallible constructor used by CLI validation and deserialization.
80    pub fn try_new(points: Vec<(f64, f64)>, radius: f64) -> Result<Self, String> {
81        if radius <= 0.0 {
82            return Err("radius must be positive".into());
83        }
84        if points.is_empty() {
85            return Err("points must be non-empty".into());
86        }
87        Ok(Self { points, radius })
88    }
89
90    /// Get the number of points.
91    pub fn num_points(&self) -> usize {
92        self.points.len()
93    }
94
95    /// Get the distance threshold.
96    pub fn radius(&self) -> f64 {
97        self.radius
98    }
99
100    /// Get a reference to the points.
101    pub fn points(&self) -> &[(f64, f64)] {
102        &self.points
103    }
104
105    /// Squared Euclidean distance between two points.
106    fn dist_sq(a: (f64, f64), b: (f64, f64)) -> f64 {
107        let dx = a.0 - b.0;
108        let dy = a.1 - b.1;
109        dx * dx + dy * dy
110    }
111
112    /// Check if two points are within distance B.
113    fn within_radius(&self, i: usize, j: usize) -> bool {
114        Self::dist_sq(self.points[i], self.points[j]) <= self.radius * self.radius
115    }
116
117    /// Check if a configuration is a valid connected dominating set.
118    pub fn is_valid_solution(&self, config: &[usize]) -> bool {
119        let selected: Vec<usize> = config
120            .iter()
121            .enumerate()
122            .filter(|(_, &v)| v == 1)
123            .map(|(i, _)| i)
124            .collect();
125
126        if selected.is_empty() {
127            return false;
128        }
129
130        // Check domination: every unselected point must be within distance B
131        // of some selected point.
132        for (i, &v) in config.iter().enumerate() {
133            if v == 0 && !selected.iter().any(|&s| self.within_radius(i, s)) {
134                return false;
135            }
136        }
137
138        // Check connectivity: BFS on selected points using distance-B edges.
139        if selected.len() == 1 {
140            return true;
141        }
142        let mut visited = vec![false; selected.len()];
143        let mut queue = VecDeque::new();
144        visited[0] = true;
145        queue.push_back(0);
146        while let Some(u) = queue.pop_front() {
147            for (vi, &vj) in selected.iter().enumerate() {
148                if !visited[vi] && self.within_radius(selected[u], vj) {
149                    visited[vi] = true;
150                    queue.push_back(vi);
151                }
152            }
153        }
154        visited.iter().all(|&v| v)
155    }
156}
157
158impl Problem for MinimumGeometricConnectedDominatingSet {
159    const NAME: &'static str = "MinimumGeometricConnectedDominatingSet";
160    type Value = Min<usize>;
161
162    fn variant() -> Vec<(&'static str, &'static str)> {
163        crate::variant_params![]
164    }
165
166    fn dims(&self) -> Vec<usize> {
167        vec![2; self.num_points()]
168    }
169
170    fn evaluate(&self, config: &[usize]) -> Min<usize> {
171        if !self.is_valid_solution(config) {
172            return Min(None);
173        }
174        let count = config.iter().filter(|&&v| v == 1).count();
175        Min(Some(count))
176    }
177}
178
179crate::declare_variants! {
180    default MinimumGeometricConnectedDominatingSet => "2^num_points",
181}
182
183#[cfg(feature = "example-db")]
184pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
185    vec![crate::example_db::specs::ModelExampleSpec {
186        id: "minimum_geometric_connected_dominating_set",
187        instance: Box::new(MinimumGeometricConnectedDominatingSet::new(
188            vec![
189                (0.0, 0.0),
190                (3.0, 0.0),
191                (6.0, 0.0),
192                (9.0, 0.0),
193                (0.0, 3.0),
194                (3.0, 3.0),
195                (6.0, 3.0),
196                (9.0, 3.0),
197            ],
198            3.5,
199        )),
200        optimal_config: vec![1, 1, 1, 1, 0, 0, 0, 0],
201        optimal_value: serde_json::json!(4),
202    }]
203}
204
205#[cfg(test)]
206#[path = "../../unit_tests/models/graph/minimum_geometric_connected_dominating_set.rs"]
207mod tests;