1use crate::registry::{ConstructionError, CreateSpec, ProblemSchemaEntry, VariantDimension};
28use crate::topology::{Graph, SimpleGraph};
29use crate::traits::Problem;
30use crate::types::{Min, WeightElement};
31use crate::variant::VariantParam;
32use num_traits::Zero;
33use serde::{Deserialize, Serialize};
34use std::collections::VecDeque;
35
36inventory::submit! {
37 ProblemSchemaEntry {
38 name: "PrizeCollectingSteinerForest",
39 display_name: "Prize-Collecting Steiner Forest",
40 aliases: &[],
41 dimensions: &[
42 VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]),
43 VariantDimension::new("weight", "i64", &["i64", "f64"]),
44 ],
45 category: crate::registry::ProblemCategory::Graph,
46 module_path: module_path!(),
47 description: "Find a forest minimizing omitted-prize plus edge-cost plus omega times the number of tree components",
48 fields: PrizeCollectingSteinerForestI64CreateSpec::FIELDS,
49 }
50}
51
52#[derive(Debug, Clone, Serialize)]
88pub struct PrizeCollectingSteinerForest<G, W> {
89 graph: G,
91 vertex_prizes: Vec<W>,
93 edge_costs: Vec<W>,
95 beta: W,
97 omega: W,
99}
100
101#[derive(Deserialize)]
102struct PrizeCollectingSteinerForestData<G, W> {
103 graph: G,
104 vertex_prizes: Vec<W>,
105 edge_costs: Vec<W>,
106 beta: W,
107 omega: W,
108}
109
110impl<'de, G, W> Deserialize<'de> for PrizeCollectingSteinerForest<G, W>
111where
112 G: Graph + Deserialize<'de>,
113 W: WeightElement + Deserialize<'de>,
114{
115 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
116 where
117 D: serde::Deserializer<'de>,
118 {
119 let data = PrizeCollectingSteinerForestData::deserialize(deserializer)?;
120 Self::new(
121 data.graph,
122 data.vertex_prizes,
123 data.edge_costs,
124 data.beta,
125 data.omega,
126 )
127 .map_err(serde::de::Error::custom)
128 }
129}
130
131macro_rules! prize_collecting_steiner_forest_create_spec {
132 ($name:ident, $weight:ty, $one:expr) => {
133 #[derive(Debug, Deserialize, crate::CreateSpec)]
134 struct $name {
135 #[create(codec = "edge-list")]
136 graph: Vec<(usize, usize)>,
137 num_vertices: Option<usize>,
138 #[create(codec = "comma-separated")]
139 vertex_prizes: Option<Vec<$weight>>,
140 #[create(codec = "comma-separated")]
141 edge_costs: Option<Vec<$weight>>,
142 beta: $weight,
143 omega: $weight,
144 }
145
146 impl TryFrom<$name> for PrizeCollectingSteinerForest<SimpleGraph, $weight> {
147 type Error = ConstructionError;
148
149 fn try_from(spec: $name) -> Result<Self, Self::Error> {
150 let graph = simple_graph_from_create(spec.graph, spec.num_vertices)?;
151 let vertex_prizes = spec
152 .vertex_prizes
153 .unwrap_or_else(|| vec![$one; graph.num_vertices()]);
154 let edge_costs = spec
155 .edge_costs
156 .unwrap_or_else(|| vec![$one; graph.num_edges()]);
157 Self::new(graph, vertex_prizes, edge_costs, spec.beta, spec.omega)
158 }
159 }
160 };
161}
162
163prize_collecting_steiner_forest_create_spec!(PrizeCollectingSteinerForestI64CreateSpec, i64, 1);
164prize_collecting_steiner_forest_create_spec!(PrizeCollectingSteinerForestF64CreateSpec, f64, 1.0);
165
166fn simple_graph_from_create(
167 edges: Vec<(usize, usize)>,
168 num_vertices: Option<usize>,
169) -> Result<SimpleGraph, ConstructionError> {
170 if edges.is_empty() && num_vertices.is_none() {
171 return Err(ConstructionError::Conversion(
172 "num_vertices is required for an empty graph".into(),
173 ));
174 }
175 for (index, &(u, v)) in edges.iter().enumerate() {
176 if u == v {
177 return Err(ConstructionError::Conversion(format!(
178 "graph edge {index} is a self-loop at vertex {u}"
179 )));
180 }
181 }
182 let inferred = edges
183 .iter()
184 .flat_map(|&(u, v)| [u, v])
185 .max()
186 .map(|vertex| {
187 vertex.checked_add(1).ok_or_else(|| {
188 ConstructionError::IntegerOverflow(
189 "inferring the PrizeCollectingSteinerForest vertex count".into(),
190 )
191 })
192 })
193 .transpose()?
194 .unwrap_or(0);
195 let num_vertices = num_vertices.unwrap_or(inferred);
196 if num_vertices < inferred {
197 return Err(ConstructionError::Conversion(format!(
198 "num_vertices {num_vertices} is too small for graph endpoints; need at least {inferred}"
199 )));
200 }
201 Ok(SimpleGraph::new(num_vertices, edges))
202}
203
204impl<G: Graph, W: WeightElement> PrizeCollectingSteinerForest<G, W> {
205 pub fn new(
208 graph: G,
209 vertex_prizes: Vec<W>,
210 edge_costs: Vec<W>,
211 beta: W,
212 omega: W,
213 ) -> Result<Self, ConstructionError> {
214 if vertex_prizes.len() != graph.num_vertices() {
215 return Err(ConstructionError::Conversion(
216 "vertex_prizes length must match graph num_vertices".into(),
217 ));
218 }
219 if edge_costs.len() != graph.num_edges() {
220 return Err(ConstructionError::Conversion(
221 "edge_costs length must match graph num_edges".into(),
222 ));
223 }
224 for (index, prize) in vertex_prizes.iter().enumerate() {
225 prize.validate_element(&format!("vertex prize at index {index}"))?;
226 }
227 for (index, cost) in edge_costs.iter().enumerate() {
228 cost.validate_element(&format!("edge cost at index {index}"))?;
229 }
230 beta.validate_element("beta")?;
231 omega.validate_element("omega")?;
232 Ok(Self {
233 graph,
234 vertex_prizes,
235 edge_costs,
236 beta,
237 omega,
238 })
239 }
240
241 pub fn graph(&self) -> &G {
243 &self.graph
244 }
245
246 pub fn vertex_prizes(&self) -> &[W] {
248 &self.vertex_prizes
249 }
250
251 pub fn edge_costs(&self) -> &[W] {
253 &self.edge_costs
254 }
255
256 pub fn beta(&self) -> &W {
258 &self.beta
259 }
260
261 pub fn omega(&self) -> &W {
263 &self.omega
264 }
265}
266
267impl<G: Graph, W: WeightElement> PrizeCollectingSteinerForest<G, W> {
268 pub fn num_vertices(&self) -> usize {
270 self.graph.num_vertices()
271 }
272
273 pub fn num_edges(&self) -> usize {
275 self.graph.num_edges()
276 }
277
278 pub fn num_vertices_with_prize(&self) -> usize {
281 let zero = <W::Sum as Zero>::zero();
282 self.vertex_prizes
283 .iter()
284 .filter(|prize| prize.to_sum() > zero)
285 .count()
286 }
287
288 pub fn is_valid_solution(&self, solution: &(Vec<bool>, Vec<bool>)) -> bool {
291 forest_components(&self.graph, &solution.0, &solution.1).is_some()
292 }
293}
294
295impl<G, W> Problem for PrizeCollectingSteinerForest<G, W>
296where
297 G: Graph + VariantParam,
298 W: WeightElement + VariantParam,
299{
300 const NAME: &'static str = "PrizeCollectingSteinerForest";
301 type Solution = (Vec<bool>, Vec<bool>);
302 type Value = Min<W::Sum>;
303
304 crate::problem_parameters![
305 ("num_edges", num_edges),
306 ("num_vertices", num_vertices),
307 ("num_vertices_with_prize", num_vertices_with_prize),
308 ];
309
310 fn variant() -> Vec<(&'static str, &'static str)> {
311 crate::variant_params![G, W]
312 }
313
314 fn evaluate(
315 &self,
316 solution: &Self::Solution,
317 ) -> Result<Min<W::Sum>, crate::traits::EvaluationError> {
318 let (vertices, edges) = solution;
319 if vertices.len() != self.graph.num_vertices() || edges.len() != self.graph.num_edges() {
320 return Err(crate::traits::EvaluationError::InvalidConfiguration(
321 "Steiner forest selection dimensions do not match the graph".into(),
322 ));
323 }
324 Ok({
325 let kappa = match forest_components(&self.graph, vertices, edges) {
326 Some(kappa) => kappa,
327 None => return Ok(Min(None)),
328 };
329
330 let mut omitted_prizes = W::Sum::zero();
335 for (v, prize) in self.vertex_prizes.iter().enumerate() {
336 if !vertices[v] {
337 omitted_prizes = W::checked_add_to_sum(
338 omitted_prizes,
339 prize.to_sum(),
340 "summing omitted Steiner forest prizes",
341 )?;
342 }
343 }
344 let omitted_term = W::checked_mul_sum(
345 self.beta.to_sum(),
346 omitted_prizes,
347 "multiplying omitted prizes by beta",
348 )?;
349
350 let mut edge_term = W::Sum::zero();
351 for (i, cost) in self.edge_costs.iter().enumerate() {
352 if edges[i] {
353 edge_term = W::checked_add_to_sum(
354 edge_term,
355 cost.to_sum(),
356 "summing Steiner forest edge costs",
357 )?;
358 }
359 }
360
361 let omega_sum = self.omega.to_sum();
363 let mut kappa_sum = W::Sum::zero();
364 for _ in 0..kappa {
365 kappa_sum = W::checked_add_to_sum(
366 kappa_sum,
367 omega_sum.clone(),
368 "multiplying Steiner forest component penalty",
369 )?;
370 }
371
372 let mut total = W::Sum::zero();
373 total = W::checked_add_to_sum(
374 total,
375 omitted_term,
376 "summing Steiner forest objective terms",
377 )?;
378 total =
379 W::checked_add_to_sum(total, edge_term, "summing Steiner forest objective terms")?;
380 total =
381 W::checked_add_to_sum(total, kappa_sum, "summing Steiner forest objective terms")?;
382 Min(Some(total))
383 })
384 }
385}
386
387impl<G, W> crate::solvers::BruteForceProblem for PrizeCollectingSteinerForest<G, W>
388where
389 G: Graph + VariantParam,
390 W: WeightElement + VariantParam,
391{
392 fn dimensions(&self) -> Vec<usize> {
393 vec![2; self.graph.num_vertices() + self.graph.num_edges()]
394 }
395}
396
397fn forest_components<G: Graph>(
402 graph: &G,
403 selected_vertices: &[bool],
404 selected_edges: &[bool],
405) -> Option<usize> {
406 let n = graph.num_vertices();
407 let m = graph.num_edges();
408 if selected_vertices.len() != n || selected_edges.len() != m {
409 return None;
410 }
411 let edges = graph.edges();
412 let mut adj: Vec<Vec<(usize, usize)>> = vec![Vec::new(); n];
413 for (i, &(u, v)) in edges.iter().enumerate() {
414 if !selected_edges[i] {
415 continue;
416 }
417 if !selected_vertices[u] || !selected_vertices[v] {
418 return None;
419 }
420 adj[u].push((v, i));
421 adj[v].push((u, i));
422 }
423 let mut visited = vec![false; n];
424 let mut kappa: usize = 0;
425 for start in 0..n {
426 if !selected_vertices[start] || visited[start] {
427 continue;
428 }
429 kappa += 1;
430 visited[start] = true;
431 let mut parent_edge: Vec<Option<usize>> = vec![None; n];
432 let mut queue: VecDeque<usize> = VecDeque::new();
433 queue.push_back(start);
434 while let Some(u) = queue.pop_front() {
435 for &(w, edge_idx) in &adj[u] {
436 if parent_edge[u] == Some(edge_idx) {
437 continue;
438 }
439 if visited[w] {
440 return None; }
442 visited[w] = true;
443 parent_edge[w] = Some(edge_idx);
444 queue.push_back(w);
445 }
446 }
447 }
448 Some(kappa)
449}
450
451crate::declare_variants! {
452 default PrizeCollectingSteinerForest<SimpleGraph, i64> => "2^(num_vertices + num_edges)" create PrizeCollectingSteinerForestI64CreateSpec,
453 PrizeCollectingSteinerForest<SimpleGraph, f64> => "2^(num_vertices + num_edges)" create PrizeCollectingSteinerForestF64CreateSpec,
454}
455
456crate::register_brute_force! {
457 PrizeCollectingSteinerForest<SimpleGraph, i64> decode |problem: &PrizeCollectingSteinerForest<SimpleGraph, i64>, indices: Vec<usize>| { let split = problem.num_vertices(); (crate::config::config_to_bits(&indices[..split]), crate::config::config_to_bits(&indices[split..])) },
458 PrizeCollectingSteinerForest<SimpleGraph, f64> decode |problem: &PrizeCollectingSteinerForest<SimpleGraph, f64>, indices: Vec<usize>| { let split = problem.num_vertices(); (crate::config::config_to_bits(&indices[..split]), crate::config::config_to_bits(&indices[split..])) },
459}
460
461#[cfg(feature = "example-db")]
462pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
463 vec![crate::example_db::specs::ModelExampleSpec {
468 id: "prize_collecting_steiner_forest_simplegraph",
469 instance: Box::new(
470 PrizeCollectingSteinerForest::<SimpleGraph, i64>::new(
471 SimpleGraph::new(3, vec![(0, 1), (1, 2)]),
472 vec![5, 2, 5],
473 vec![1, 6],
474 1,
475 2,
476 )
477 .unwrap(),
478 ),
479 optimal_config: serde_json::json!((vec![true, true, true], vec![true, false])),
480 optimal_value: serde_json::json!(5),
481 }]
482}
483
484#[cfg(test)]
485#[path = "../../unit_tests/models/graph/prize_collecting_steiner_forest.rs"]
486mod tests;