Skip to main content

problemreductions/models/graph/
acyclic_partition.rs

1//! Acyclic Partition problem implementation.
2//!
3//! Given a directed graph with vertex weights, arc costs, and bounds, determine
4//! whether the vertices can be partitioned into groups whose quotient graph is a
5//! DAG, each group's total vertex weight is bounded, and the total
6//! inter-partition arc cost is bounded.
7
8use crate::registry::{CreateSpec, ProblemSchemaEntry, VariantDimension};
9use crate::topology::DirectedGraph;
10use crate::traits::Problem;
11use crate::types::WeightElement;
12use num_traits::Zero;
13use serde::{Deserialize, Serialize};
14use std::collections::BTreeSet;
15
16inventory::submit! {
17    ProblemSchemaEntry {
18        name: "AcyclicPartition",
19        display_name: "Acyclic Partition",
20        aliases: &[],
21        dimensions: &[
22            VariantDimension::new("weight", "i64", &["i64"]),
23        ],
24        category: crate::registry::ProblemCategory::Graph,
25        module_path: module_path!(),
26        description: "Partition a directed graph into bounded-weight groups with an acyclic quotient graph and bounded inter-partition cost",
27        fields: AcyclicPartitionCreateSpec::FIELDS,
28    }
29}
30
31/// Acyclic Partition (Garey & Johnson ND15).
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct AcyclicPartition<W: WeightElement> {
34    graph: DirectedGraph,
35    vertex_weights: Vec<W>,
36    arc_costs: Vec<W>,
37    weight_bound: W::Sum,
38    cost_bound: W::Sum,
39}
40
41#[derive(Debug, Deserialize, crate::CreateSpec)]
42struct AcyclicPartitionCreateSpec {
43    #[create(codec = "arc-list")]
44    arcs: Vec<(usize, usize)>,
45    num_vertices: Option<usize>,
46    #[create(codec = "comma-separated")]
47    weights: Option<Vec<i64>>,
48    #[create(name = "arc_costs", codec = "comma-separated")]
49    arc_weights: Option<Vec<i64>>,
50    weight_bound: i64,
51    cost_bound: i64,
52}
53
54impl TryFrom<AcyclicPartitionCreateSpec> for AcyclicPartition<i64> {
55    type Error = crate::registry::ConstructionError;
56
57    fn try_from(spec: AcyclicPartitionCreateSpec) -> Result<Self, Self::Error> {
58        if spec.arcs.is_empty() && spec.num_vertices.is_none() {
59            return Err("num_vertices is required for an empty arc list"
60                .to_string()
61                .into());
62        }
63        let inferred = spec
64            .arcs
65            .iter()
66            .flat_map(|&(u, v)| [u, v])
67            .max()
68            .map(|vertex| vertex.checked_add(1).ok_or("vertex count overflows usize"))
69            .transpose()?
70            .unwrap_or(0);
71        let num_vertices = spec.num_vertices.unwrap_or(inferred);
72        if num_vertices < inferred {
73            return Err(format!(
74                "num_vertices {num_vertices} is too small for arc endpoints; need at least {inferred}"
75            ).into());
76        }
77        let graph = DirectedGraph::new(num_vertices, spec.arcs);
78        let vertex_weights = spec.weights.unwrap_or_else(|| vec![1; num_vertices]);
79        if vertex_weights.len() != num_vertices {
80            return Err(format!(
81                "weights has length {}, expected {num_vertices}",
82                vertex_weights.len()
83            )
84            .into());
85        }
86        let arc_costs = spec
87            .arc_weights
88            .unwrap_or_else(|| vec![1; graph.num_arcs()]);
89        if arc_costs.len() != graph.num_arcs() {
90            return Err(format!(
91                "arc_weights has length {}, expected {}",
92                arc_costs.len(),
93                graph.num_arcs()
94            )
95            .into());
96        }
97        Ok(Self::new(
98            graph,
99            vertex_weights,
100            arc_costs,
101            spec.weight_bound,
102            spec.cost_bound,
103        ))
104    }
105}
106
107impl<W: WeightElement> AcyclicPartition<W> {
108    /// Create a new Acyclic Partition instance.
109    pub fn new(
110        graph: DirectedGraph,
111        vertex_weights: Vec<W>,
112        arc_costs: Vec<W>,
113        weight_bound: W::Sum,
114        cost_bound: W::Sum,
115    ) -> Self {
116        assert_eq!(
117            vertex_weights.len(),
118            graph.num_vertices(),
119            "vertex_weights length must match graph num_vertices"
120        );
121        assert_eq!(
122            arc_costs.len(),
123            graph.num_arcs(),
124            "arc_costs length must match graph num_arcs"
125        );
126        Self {
127            graph,
128            vertex_weights,
129            arc_costs,
130            weight_bound,
131            cost_bound,
132        }
133    }
134
135    /// Get the underlying graph.
136    pub fn graph(&self) -> &DirectedGraph {
137        &self.graph
138    }
139
140    /// Get the vertex weights.
141    pub fn vertex_weights(&self) -> &[W] {
142        &self.vertex_weights
143    }
144
145    /// Get the arc costs.
146    pub fn arc_costs(&self) -> &[W] {
147        &self.arc_costs
148    }
149
150    /// Replace the vertex weights.
151    pub fn set_vertex_weights(&mut self, vertex_weights: Vec<W>) {
152        assert_eq!(
153            vertex_weights.len(),
154            self.graph.num_vertices(),
155            "vertex_weights length must match graph num_vertices"
156        );
157        self.vertex_weights = vertex_weights;
158    }
159
160    /// Replace the arc costs.
161    pub fn set_arc_costs(&mut self, arc_costs: Vec<W>) {
162        assert_eq!(
163            arc_costs.len(),
164            self.graph.num_arcs(),
165            "arc_costs length must match graph num_arcs"
166        );
167        self.arc_costs = arc_costs;
168    }
169
170    /// Get the per-part weight bound.
171    pub fn weight_bound(&self) -> &W::Sum {
172        &self.weight_bound
173    }
174
175    /// Get the inter-partition cost bound.
176    pub fn cost_bound(&self) -> &W::Sum {
177        &self.cost_bound
178    }
179
180    /// Check whether this instance uses non-unit weights.
181    pub fn is_weighted(&self) -> bool {
182        !W::IS_UNIT
183    }
184
185    /// Get the number of vertices.
186    pub fn num_vertices(&self) -> usize {
187        self.graph.num_vertices()
188    }
189
190    /// Get the number of arcs.
191    pub fn num_arcs(&self) -> usize {
192        self.graph.num_arcs()
193    }
194
195    /// Check whether a configuration is a valid solution.
196    pub fn is_valid_solution(
197        &self,
198        config: &[usize],
199    ) -> Result<bool, crate::traits::EvaluationError> {
200        is_valid_acyclic_partition(
201            &self.graph,
202            &self.vertex_weights,
203            &self.arc_costs,
204            &self.weight_bound,
205            &self.cost_bound,
206            config,
207        )
208    }
209}
210
211impl<W> Problem for AcyclicPartition<W>
212where
213    W: WeightElement + crate::variant::VariantParam,
214{
215    const NAME: &'static str = "AcyclicPartition";
216    type Solution = Vec<usize>;
217    type Value = crate::types::Or;
218
219    crate::problem_parameters![("num_arcs", num_arcs), ("num_vertices", num_vertices),];
220
221    fn variant() -> Vec<(&'static str, &'static str)> {
222        crate::variant_params![W]
223    }
224
225    fn evaluate(
226        &self,
227        config: &Self::Solution,
228    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
229        let n = self.graph.num_vertices();
230        if config.len() != n {
231            return Err(crate::traits::EvaluationError::InvalidConfiguration(
232                "partition assignment length does not match the graph vertices".into(),
233            ));
234        }
235        if config.iter().any(|&part| part >= n) {
236            return Err(crate::traits::EvaluationError::InvalidConfiguration(
237                "partition assignment contains an out-of-range part".into(),
238            ));
239        }
240        Ok({
241            crate::types::Or({
242                is_valid_acyclic_partition(
243                    &self.graph,
244                    &self.vertex_weights,
245                    &self.arc_costs,
246                    &self.weight_bound,
247                    &self.cost_bound,
248                    config,
249                )?
250            })
251        })
252    }
253}
254
255impl<W> crate::solvers::BruteForceProblem for AcyclicPartition<W>
256where
257    W: WeightElement + crate::variant::VariantParam,
258{
259    fn dimensions(&self) -> Vec<usize> {
260        vec![self.graph.num_vertices(); self.graph.num_vertices()]
261    }
262}
263
264fn is_valid_acyclic_partition<W: WeightElement>(
265    graph: &DirectedGraph,
266    vertex_weights: &[W],
267    arc_costs: &[W],
268    weight_bound: &W::Sum,
269    cost_bound: &W::Sum,
270    config: &[usize],
271) -> Result<bool, crate::traits::EvaluationError> {
272    let num_vertices = graph.num_vertices();
273    if config.len() != num_vertices {
274        return Ok(false);
275    }
276    if vertex_weights.len() != num_vertices || arc_costs.len() != graph.num_arcs() {
277        return Ok(false);
278    }
279    if config.iter().any(|&label| label >= num_vertices) {
280        return Ok(false);
281    }
282
283    let mut partition_weights = vec![W::Sum::zero(); num_vertices];
284    let mut used_labels = vec![false; num_vertices];
285    for (vertex, &label) in config.iter().enumerate() {
286        used_labels[label] = true;
287        partition_weights[label] = W::checked_add_to_sum(
288            partition_weights[label].clone(),
289            vertex_weights[vertex].to_sum(),
290            "summing acyclic partition vertex weights",
291        )?;
292        if partition_weights[label] > *weight_bound {
293            return Ok(false);
294        }
295    }
296
297    let mut dense_label = vec![usize::MAX; num_vertices];
298    let mut next_dense = 0usize;
299    for (label, used) in used_labels.iter().enumerate() {
300        if *used {
301            dense_label[label] = next_dense;
302            next_dense += 1;
303        }
304    }
305
306    let mut total_cost = W::Sum::zero();
307    let mut quotient_arcs = BTreeSet::new();
308    for ((source, target), cost) in graph.arcs().iter().zip(arc_costs.iter()) {
309        let source_label = config[*source];
310        let target_label = config[*target];
311        if source_label == target_label {
312            continue;
313        }
314        total_cost = W::checked_add_to_sum(
315            total_cost,
316            cost.to_sum(),
317            "summing acyclic partition arc costs",
318        )?;
319        if total_cost > *cost_bound {
320            return Ok(false);
321        }
322        quotient_arcs.insert((dense_label[source_label], dense_label[target_label]));
323    }
324
325    Ok(DirectedGraph::new(next_dense, quotient_arcs.into_iter().collect()).is_dag())
326}
327
328crate::declare_variants! {
329    default AcyclicPartition<i64> => "num_vertices^num_vertices" create AcyclicPartitionCreateSpec,
330}
331
332crate::register_brute_force! {
333    AcyclicPartition<i64>,
334}
335
336#[cfg(feature = "example-db")]
337pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
338    vec![crate::example_db::specs::ModelExampleSpec {
339        id: "acyclic_partition",
340        instance: Box::new(AcyclicPartition::new(
341            DirectedGraph::new(
342                6,
343                vec![
344                    (0, 1),
345                    (0, 2),
346                    (1, 3),
347                    (1, 4),
348                    (2, 4),
349                    (2, 5),
350                    (3, 5),
351                    (4, 5),
352                ],
353            ),
354            vec![2, 3, 2, 1, 3, 1],
355            vec![1; 8],
356            5,
357            5,
358        )),
359        optimal_config: serde_json::json!(vec![0, 1, 0, 2, 2, 2]),
360        optimal_value: serde_json::json!(true),
361    }]
362}
363
364#[cfg(test)]
365#[path = "../../unit_tests/models/graph/acyclic_partition.rs"]
366mod tests;