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(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    /// Reference to the underlying graph.
242    pub fn graph(&self) -> &G {
243        &self.graph
244    }
245
246    /// Vertex prizes in vertex-index order.
247    pub fn vertex_prizes(&self) -> &[W] {
248        &self.vertex_prizes
249    }
250
251    /// Edge costs in `graph.edges()` order.
252    pub fn edge_costs(&self) -> &[W] {
253        &self.edge_costs
254    }
255
256    /// Tradeoff coefficient on the omitted-prize term.
257    pub fn beta(&self) -> &W {
258        &self.beta
259    }
260
261    /// Per-component penalty.
262    pub fn omega(&self) -> &W {
263        &self.omega
264    }
265}
266
267impl<G: Graph, W: WeightElement> PrizeCollectingSteinerForest<G, W> {
268    /// Number of vertices in the underlying graph.
269    pub fn num_vertices(&self) -> usize {
270        self.graph.num_vertices()
271    }
272
273    /// Number of edges in the underlying graph.
274    pub fn num_edges(&self) -> usize {
275        self.graph.num_edges()
276    }
277
278    /// Number of vertices with a strictly positive prize, i.e.
279    /// `|{ v in V : p(v) > 0 }|`.
280    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    /// Whether this configuration is a feasible forest (selected edges only
289    /// touch selected vertices and induce an acyclic subgraph).
290    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            // Objective: beta * sum_{v notin V_F} p(v)
331            //          + sum_{e in E_F} c(e)
332            //          + omega * kappa(F).
333            //
334            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            // Represent `kappa` in `W::Sum` by summing `omega` `kappa` times.
362            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
397/// Validate a `(V_F, E_F)` configuration and, if feasible, return the number of
398/// tree components `kappa(F)` among the selected vertices. Feasible means every
399/// selected edge is incident only to selected vertices and the selected
400/// subgraph is acyclic. Returns `None` for any infeasible configuration.
401fn 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; // back-edge inside the component => cycle
441                }
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    // Issue #1026 canonical instance: path 0 - 1 - 2 with edge costs
464    // c(0,1)=1, c(1,2)=6, vertex prizes p = (5, 2, 5), beta = 1, omega = 2.
465    // Optimum: V_F = {0,1,2}, E_F = {(0,1)} (two components {0,1} and {2}),
466    // objective = 0 + 1 + 2*2 = 5.
467    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;