Skip to main content

problemreductions/models/graph/
prize_collecting_steiner_forest.rs

1//! Prize-Collecting Steiner Forest problem implementation.
2//!
3//! Given an undirected network `G = (V, E)` with nonnegative vertex prizes
4//! `p: V -> R_{>=0}`, nonnegative edge costs `c: E -> R_{>=0}`, and
5//! nonnegative tradeoff parameters `beta` and `omega`, find a forest
6//! `F = (V_F, E_F)` -- a subgraph that is a disjoint union of trees,
7//! including singleton-vertex trees -- minimizing
8//!
9//! ```text
10//! beta * sum_{v in V \ V_F} p(v) + sum_{e in E_F} c(e) + omega * kappa(F),
11//! ```
12//!
13//! where `kappa(F)` is the number of (tree) components of `F`. Singleton
14//! selected vertices are allowed and count as one-vertex tree components;
15//! unselected vertices are not part of any component.
16//!
17//! Reference:
18//! - Nurcan Tuncbag, Alfredo Braunstein, Andrea Pagnani, Shao-Shan Carol
19//!   Huang, Jennifer Chayes, Christian Borgs, Riccardo Zecchina, and Ernest
20//!   Fraenkel. "Simultaneous Reconstruction of Multiple Signaling Pathways
21//!   via the Prize-Collecting Steiner Forest Problem." Journal of
22//!   Computational Biology 20(2):124--136, 2013.
23//!   <https://doi.org/10.1089/cmb.2012.0092>
24//! - Earlier conference version, RECOMB 2012, LNBI 7262, pp. 287--301.
25//!   <https://doi.org/10.1007/978-3-642-29627-7_31>
26
27use 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/// The Prize-Collecting Steiner Forest problem (biology-paper variant).
53///
54/// Configuration layout (length `num_vertices + num_edges`):
55/// - the first `num_vertices` bits are vertex selectors `x_v` (1 iff
56///   `v in V_F`),
57/// - the next `num_edges` bits are edge selectors `y_e` (1 iff `e in E_F`),
58///   in `graph.edges()` order.
59///
60/// A configuration is feasible iff every selected edge has both endpoints
61/// selected and the resulting subgraph is acyclic. Singleton selected
62/// vertices are allowed.
63///
64/// # Type Parameters
65///
66/// * `G` - Graph type (currently `SimpleGraph`).
67/// * `W` - Weight / cost type (e.g., `i64`, `f64`).
68///
69/// # Example
70///
71/// ```
72/// use problemreductions::models::graph::PrizeCollectingSteinerForest;
73/// use problemreductions::topology::SimpleGraph;
74/// use problemreductions::types::Min;
75/// use problemreductions::{BruteForce, Problem};
76///
77/// // Path 0 - 1 - 2 with edge costs c(0,1)=1, c(1,2)=6 and vertex prizes
78/// // p = (5, 2, 5), beta = 1, omega = 2.
79/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]);
80/// let problem =
81///     PrizeCollectingSteinerForest::<_, i64>::new(graph, vec![5, 2, 5], vec![1, 6], 1, 2).unwrap();
82/// // V_F = {0,1,2}, E_F = {(0,1)} gives two components {0,1} and {2}:
83/// // objective = 0 + 1 + 2*2 = 5.
84/// let solution = BruteForce::new().solve(&problem).unwrap().unwrap();
85/// assert_eq!(problem.evaluate(&solution).unwrap(), Min(Some(5)));
86/// ```
87#[derive(Debug, Clone, Serialize)]
88pub struct PrizeCollectingSteinerForest<G, W> {
89    /// The underlying network.
90    graph: G,
91    /// Vertex prizes `p: V -> R_{>=0}` (in vertex-index order).
92    vertex_prizes: Vec<W>,
93    /// Edge costs `c: E -> R_{>=0}` (in `graph.edges()` order).
94    edge_costs: Vec<W>,
95    /// Tradeoff coefficient on the omitted-prize term.
96    beta: W,
97    /// Per-component penalty.
98    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    /// Create a new Prize-Collecting Steiner Forest instance.
206    ///
207    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(crate::registry::ConstructionError::length_mismatch(
216                "vertex_prizes",
217                vertex_prizes.len(),
218                graph.num_vertices(),
219            ));
220        }
221        if edge_costs.len() != graph.num_edges() {
222            return Err(crate::registry::ConstructionError::length_mismatch(
223                "edge_costs",
224                edge_costs.len(),
225                graph.num_edges(),
226            ));
227        }
228        for (index, prize) in vertex_prizes.iter().enumerate() {
229            prize.validate_element(&format!("vertex prize at index {index}"))?;
230        }
231        for (index, cost) in edge_costs.iter().enumerate() {
232            cost.validate_element(&format!("edge cost at index {index}"))?;
233        }
234        beta.validate_element("beta")?;
235        omega.validate_element("omega")?;
236        if vertex_prizes
237            .iter()
238            .chain(&edge_costs)
239            .chain([&beta, &omega])
240            .any(|value| value.to_sum() < W::Sum::zero())
241        {
242            return Err(ConstructionError::InvalidInput(
243                "vertex prizes, edge costs, beta, and omega must be nonnegative".into(),
244            ));
245        }
246        Ok(Self {
247            graph,
248            vertex_prizes,
249            edge_costs,
250            beta,
251            omega,
252        })
253    }
254
255    /// Reference to the underlying graph.
256    pub fn graph(&self) -> &G {
257        &self.graph
258    }
259
260    /// Vertex prizes in vertex-index order.
261    pub fn vertex_prizes(&self) -> &[W] {
262        &self.vertex_prizes
263    }
264
265    /// Edge costs in `graph.edges()` order.
266    pub fn edge_costs(&self) -> &[W] {
267        &self.edge_costs
268    }
269
270    /// Tradeoff coefficient on the omitted-prize term.
271    pub fn beta(&self) -> &W {
272        &self.beta
273    }
274
275    /// Per-component penalty.
276    pub fn omega(&self) -> &W {
277        &self.omega
278    }
279}
280
281impl<G: Graph, W: WeightElement> PrizeCollectingSteinerForest<G, W> {
282    /// Number of vertices in the underlying graph.
283    pub fn num_vertices(&self) -> usize {
284        self.graph.num_vertices()
285    }
286
287    /// Number of edges in the underlying graph.
288    pub fn num_edges(&self) -> usize {
289        self.graph.num_edges()
290    }
291
292    /// Number of vertices with a strictly positive prize, i.e.
293    /// `|{ v in V : p(v) > 0 }|`.
294    pub fn num_vertices_with_prize(&self) -> usize {
295        let zero = <W::Sum as Zero>::zero();
296        self.vertex_prizes
297            .iter()
298            .filter(|prize| prize.to_sum() > zero)
299            .count()
300    }
301
302    /// Whether this configuration is a feasible forest (selected edges only
303    /// touch selected vertices and induce an acyclic subgraph).
304    pub fn is_valid_solution(&self, solution: &(Vec<bool>, Vec<bool>)) -> bool {
305        forest_components(&self.graph, &solution.0, &solution.1).is_some()
306    }
307}
308
309impl<G, W> Problem for PrizeCollectingSteinerForest<G, W>
310where
311    G: Graph + VariantParam,
312    W: WeightElement + VariantParam,
313{
314    const NAME: &'static str = "PrizeCollectingSteinerForest";
315    type Solution = (Vec<bool>, Vec<bool>);
316    type Value = Min<W::Sum>;
317
318    crate::problem_parameters![
319        ("num_edges", num_edges),
320        ("num_vertices", num_vertices),
321        ("num_vertices_with_prize", num_vertices_with_prize),
322    ];
323
324    fn variant() -> Vec<(&'static str, &'static str)> {
325        crate::variant_params![G, W]
326    }
327
328    fn evaluate(
329        &self,
330        solution: &Self::Solution,
331    ) -> Result<Min<W::Sum>, crate::traits::EvaluationError> {
332        let (vertices, edges) = solution;
333        if vertices.len() != self.graph.num_vertices() || edges.len() != self.graph.num_edges() {
334            return Err(crate::traits::EvaluationError::InvalidConfiguration(
335                "Steiner forest selection dimensions do not match the graph".into(),
336            ));
337        }
338        Ok({
339            let kappa = match forest_components(&self.graph, vertices, edges) {
340                Some(kappa) => kappa,
341                None => return Ok(Min(None)),
342            };
343
344            // Objective: beta * sum_{v notin V_F} p(v)
345            //          + sum_{e in E_F} c(e)
346            //          + omega * kappa(F).
347            //
348            let mut omitted_prizes = W::Sum::zero();
349            for (v, prize) in self.vertex_prizes.iter().enumerate() {
350                if !vertices[v] {
351                    omitted_prizes = W::checked_add_to_sum(
352                        omitted_prizes,
353                        prize.to_sum(),
354                        "summing omitted Steiner forest prizes",
355                    )?;
356                }
357            }
358            let omitted_term = W::checked_mul_sum(
359                self.beta.to_sum(),
360                omitted_prizes,
361                "multiplying omitted prizes by beta",
362            )?;
363
364            let mut edge_term = W::Sum::zero();
365            for (i, cost) in self.edge_costs.iter().enumerate() {
366                if edges[i] {
367                    edge_term = W::checked_add_to_sum(
368                        edge_term,
369                        cost.to_sum(),
370                        "summing Steiner forest edge costs",
371                    )?;
372                }
373            }
374
375            // Represent `kappa` in `W::Sum` by summing `omega` `kappa` times.
376            let omega_sum = self.omega.to_sum();
377            let mut kappa_sum = W::Sum::zero();
378            for _ in 0..kappa {
379                kappa_sum = W::checked_add_to_sum(
380                    kappa_sum,
381                    omega_sum.clone(),
382                    "multiplying Steiner forest component penalty",
383                )?;
384            }
385
386            let mut total = W::Sum::zero();
387            total = W::checked_add_to_sum(
388                total,
389                omitted_term,
390                "summing Steiner forest objective terms",
391            )?;
392            total =
393                W::checked_add_to_sum(total, edge_term, "summing Steiner forest objective terms")?;
394            total =
395                W::checked_add_to_sum(total, kappa_sum, "summing Steiner forest objective terms")?;
396            Min(Some(total))
397        })
398    }
399}
400
401impl<G, W> crate::solvers::BruteForceProblem for PrizeCollectingSteinerForest<G, W>
402where
403    G: Graph + VariantParam,
404    W: WeightElement + VariantParam,
405{
406    fn dimensions(&self) -> Vec<usize> {
407        vec![2; self.graph.num_vertices() + self.graph.num_edges()]
408    }
409}
410
411/// Validate a `(V_F, E_F)` configuration and, if feasible, return the number of
412/// tree components `kappa(F)` among the selected vertices. Feasible means every
413/// selected edge is incident only to selected vertices and the selected
414/// subgraph is acyclic. Returns `None` for any infeasible configuration.
415fn forest_components<G: Graph>(
416    graph: &G,
417    selected_vertices: &[bool],
418    selected_edges: &[bool],
419) -> Option<usize> {
420    let n = graph.num_vertices();
421    let m = graph.num_edges();
422    if selected_vertices.len() != n || selected_edges.len() != m {
423        return None;
424    }
425    let edges = graph.edges();
426    let mut adj: Vec<Vec<(usize, usize)>> = vec![Vec::new(); n];
427    for (i, &(u, v)) in edges.iter().enumerate() {
428        if !selected_edges[i] {
429            continue;
430        }
431        if !selected_vertices[u] || !selected_vertices[v] {
432            return None;
433        }
434        adj[u].push((v, i));
435        adj[v].push((u, i));
436    }
437    let mut visited = vec![false; n];
438    let mut kappa: usize = 0;
439    for start in 0..n {
440        if !selected_vertices[start] || visited[start] {
441            continue;
442        }
443        kappa += 1;
444        visited[start] = true;
445        let mut parent_edge: Vec<Option<usize>> = vec![None; n];
446        let mut queue: VecDeque<usize> = VecDeque::new();
447        queue.push_back(start);
448        while let Some(u) = queue.pop_front() {
449            for &(w, edge_idx) in &adj[u] {
450                if parent_edge[u] == Some(edge_idx) {
451                    continue;
452                }
453                if visited[w] {
454                    return None; // back-edge inside the component => cycle
455                }
456                visited[w] = true;
457                parent_edge[w] = Some(edge_idx);
458                queue.push_back(w);
459            }
460        }
461    }
462    Some(kappa)
463}
464
465crate::declare_variants! {
466    default PrizeCollectingSteinerForest<SimpleGraph, i64> => "2^(num_vertices + num_edges)" create PrizeCollectingSteinerForestI64CreateSpec,
467    PrizeCollectingSteinerForest<SimpleGraph, f64> => "2^(num_vertices + num_edges)" create PrizeCollectingSteinerForestF64CreateSpec,
468}
469
470crate::register_brute_force! {
471    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..])) },
472    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..])) },
473}
474
475#[cfg(feature = "example-db")]
476pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
477    // Issue #1026 canonical instance: path 0 - 1 - 2 with edge costs
478    // c(0,1)=1, c(1,2)=6, vertex prizes p = (5, 2, 5), beta = 1, omega = 2.
479    // Optimum: V_F = {0,1,2}, E_F = {(0,1)} (two components {0,1} and {2}),
480    // objective = 0 + 1 + 2*2 = 5.
481    vec![crate::example_db::specs::ModelExampleSpec {
482        id: "prize_collecting_steiner_forest_simplegraph",
483        instance: Box::new(
484            PrizeCollectingSteinerForest::<SimpleGraph, i64>::new(
485                SimpleGraph::new(3, vec![(0, 1), (1, 2)]),
486                vec![5, 2, 5],
487                vec![1, 6],
488                1,
489                2,
490            )
491            .unwrap(),
492        ),
493        optimal_config: serde_json::json!((vec![true, true, true], vec![true, false])),
494        optimal_value: serde_json::json!(5),
495    }]
496}
497
498#[cfg(test)]
499#[path = "../../unit_tests/models/graph/prize_collecting_steiner_forest.rs"]
500mod tests;