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)]
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(Deserialize)]
42#[serde(bound(deserialize = "W: WeightElement + Deserialize<'de>, W::Sum: Deserialize<'de>"))]
43struct AcyclicPartitionData<W: WeightElement> {
44    graph: DirectedGraph,
45    vertex_weights: Vec<W>,
46    arc_costs: Vec<W>,
47    weight_bound: W::Sum,
48    cost_bound: W::Sum,
49}
50
51impl<'de, W> Deserialize<'de> for AcyclicPartition<W>
52where
53    W: WeightElement + Deserialize<'de>,
54    W::Sum: Deserialize<'de>,
55{
56    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
57        let data = AcyclicPartitionData::<W>::deserialize(deserializer)?;
58        Self::try_new(
59            data.graph,
60            data.vertex_weights,
61            data.arc_costs,
62            data.weight_bound,
63            data.cost_bound,
64        )
65        .map_err(serde::de::Error::custom)
66    }
67}
68
69#[derive(Debug, Deserialize, crate::CreateSpec)]
70struct AcyclicPartitionCreateSpec {
71    #[create(codec = "arc-list")]
72    arcs: Vec<(usize, usize)>,
73    num_vertices: Option<usize>,
74    #[create(codec = "comma-separated")]
75    weights: Option<Vec<i64>>,
76    #[create(name = "arc_costs", codec = "comma-separated")]
77    arc_weights: Option<Vec<i64>>,
78    weight_bound: i64,
79    cost_bound: i64,
80}
81
82impl TryFrom<AcyclicPartitionCreateSpec> for AcyclicPartition<i64> {
83    type Error = crate::registry::ConstructionError;
84
85    fn try_from(spec: AcyclicPartitionCreateSpec) -> Result<Self, Self::Error> {
86        if spec.arcs.is_empty() && spec.num_vertices.is_none() {
87            return Err("num_vertices is required for an empty arc list"
88                .to_string()
89                .into());
90        }
91        let inferred = spec
92            .arcs
93            .iter()
94            .flat_map(|&(u, v)| [u, v])
95            .max()
96            .map(|vertex| vertex.checked_add(1).ok_or("vertex count overflows usize"))
97            .transpose()?
98            .unwrap_or(0);
99        let num_vertices = spec.num_vertices.unwrap_or(inferred);
100        if num_vertices < inferred {
101            return Err(format!(
102                "num_vertices {num_vertices} is too small for arc endpoints; need at least {inferred}"
103            ).into());
104        }
105        let graph = DirectedGraph::new(num_vertices, spec.arcs);
106        let vertex_weights = spec.weights.unwrap_or_else(|| vec![1; num_vertices]);
107        let arc_costs = spec
108            .arc_weights
109            .unwrap_or_else(|| vec![1; graph.num_arcs()]);
110        Self::try_new(
111            graph,
112            vertex_weights,
113            arc_costs,
114            spec.weight_bound,
115            spec.cost_bound,
116        )
117    }
118}
119
120impl<W: WeightElement> AcyclicPartition<W> {
121    /// Create a new Acyclic Partition instance.
122    pub fn new(
123        graph: DirectedGraph,
124        vertex_weights: Vec<W>,
125        arc_costs: Vec<W>,
126        weight_bound: W::Sum,
127        cost_bound: W::Sum,
128    ) -> Self {
129        Self::try_new(graph, vertex_weights, arc_costs, weight_bound, cost_bound)
130            .unwrap_or_else(|error| panic!("{error}"))
131    }
132
133    fn try_new(
134        graph: DirectedGraph,
135        vertex_weights: Vec<W>,
136        arc_costs: Vec<W>,
137        weight_bound: W::Sum,
138        cost_bound: W::Sum,
139    ) -> Result<Self, crate::registry::ConstructionError> {
140        Self::check_vertex_weights(&graph, &vertex_weights)?;
141        Self::check_arc_costs(&graph, &arc_costs)?;
142        Ok(Self {
143            graph,
144            vertex_weights,
145            arc_costs,
146            weight_bound,
147            cost_bound,
148        })
149    }
150
151    /// Get the underlying graph.
152    pub fn graph(&self) -> &DirectedGraph {
153        &self.graph
154    }
155
156    /// Get the vertex weights.
157    pub fn vertex_weights(&self) -> &[W] {
158        &self.vertex_weights
159    }
160
161    /// Get the arc costs.
162    pub fn arc_costs(&self) -> &[W] {
163        &self.arc_costs
164    }
165
166    /// Replace the vertex weights.
167    pub fn set_vertex_weights(&mut self, vertex_weights: Vec<W>) {
168        Self::check_vertex_weights(&self.graph, &vertex_weights)
169            .unwrap_or_else(|error| panic!("{error}"));
170        self.vertex_weights = vertex_weights;
171    }
172
173    fn check_vertex_weights(
174        graph: &DirectedGraph,
175        vertex_weights: &[W],
176    ) -> Result<(), crate::registry::ConstructionError> {
177        if vertex_weights.len() != graph.num_vertices() {
178            return Err(crate::registry::ConstructionError::length_mismatch(
179                "vertex_weights",
180                vertex_weights.len(),
181                graph.num_vertices(),
182            ));
183        }
184        Ok(())
185    }
186
187    /// Replace the arc costs.
188    pub fn set_arc_costs(&mut self, arc_costs: Vec<W>) {
189        Self::check_arc_costs(&self.graph, &arc_costs).unwrap_or_else(|error| panic!("{error}"));
190        self.arc_costs = arc_costs;
191    }
192
193    fn check_arc_costs(
194        graph: &DirectedGraph,
195        arc_costs: &[W],
196    ) -> Result<(), crate::registry::ConstructionError> {
197        if arc_costs.len() != graph.num_arcs() {
198            return Err(crate::registry::ConstructionError::length_mismatch(
199                "arc_costs",
200                arc_costs.len(),
201                graph.num_arcs(),
202            ));
203        }
204        Ok(())
205    }
206
207    /// Get the per-part weight bound.
208    pub fn weight_bound(&self) -> &W::Sum {
209        &self.weight_bound
210    }
211
212    /// Get the inter-partition cost bound.
213    pub fn cost_bound(&self) -> &W::Sum {
214        &self.cost_bound
215    }
216
217    /// Check whether this instance uses non-unit weights.
218    pub fn is_weighted(&self) -> bool {
219        !W::IS_UNIT
220    }
221
222    /// Smallest h >= 1 bounding vertex weights, arc costs, and both budgets in magnitude by 2^h.
223    pub fn max_numeric_magnitude_bits(&self) -> u64 {
224        crate::types::max_numeric_magnitude_bits(
225            self.vertex_weights
226                .iter()
227                .chain(&self.arc_costs)
228                .map(|value| value.to_sum())
229                .chain([self.weight_bound.clone(), self.cost_bound.clone()]),
230        )
231    }
232
233    /// Get the number of vertices.
234    pub fn num_vertices(&self) -> usize {
235        self.graph.num_vertices()
236    }
237
238    /// Get the number of arcs.
239    pub fn num_arcs(&self) -> usize {
240        self.graph.num_arcs()
241    }
242
243    /// Check whether a configuration is a valid solution.
244    pub fn is_valid_solution(
245        &self,
246        config: &[usize],
247    ) -> Result<bool, crate::traits::EvaluationError> {
248        is_valid_acyclic_partition(
249            &self.graph,
250            &self.vertex_weights,
251            &self.arc_costs,
252            &self.weight_bound,
253            &self.cost_bound,
254            config,
255        )
256    }
257}
258
259impl<W> Problem for AcyclicPartition<W>
260where
261    W: WeightElement + crate::variant::VariantParam,
262{
263    const NAME: &'static str = "AcyclicPartition";
264    type Solution = Vec<usize>;
265    type Value = crate::types::Or;
266
267    crate::problem_parameters![
268        ("max_numeric_magnitude_bits", max_numeric_magnitude_bits),
269        ("num_arcs", num_arcs),
270        ("num_vertices", num_vertices),
271    ];
272
273    fn variant() -> Vec<(&'static str, &'static str)> {
274        crate::variant_params![W]
275    }
276
277    fn evaluate(
278        &self,
279        config: &Self::Solution,
280    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
281        let n = self.graph.num_vertices();
282        if config.len() != n {
283            return Err(crate::traits::EvaluationError::InvalidConfiguration(
284                "partition assignment length does not match the graph vertices".into(),
285            ));
286        }
287        if config.iter().any(|&part| part >= n) {
288            return Err(crate::traits::EvaluationError::InvalidConfiguration(
289                "partition assignment contains an out-of-range part".into(),
290            ));
291        }
292        Ok({
293            crate::types::Or({
294                is_valid_acyclic_partition(
295                    &self.graph,
296                    &self.vertex_weights,
297                    &self.arc_costs,
298                    &self.weight_bound,
299                    &self.cost_bound,
300                    config,
301                )?
302            })
303        })
304    }
305}
306
307impl<W> crate::solvers::BruteForceProblem for AcyclicPartition<W>
308where
309    W: WeightElement + crate::variant::VariantParam,
310{
311    fn dimensions(&self) -> Vec<usize> {
312        vec![self.graph.num_vertices(); self.graph.num_vertices()]
313    }
314}
315
316fn is_valid_acyclic_partition<W: WeightElement>(
317    graph: &DirectedGraph,
318    vertex_weights: &[W],
319    arc_costs: &[W],
320    weight_bound: &W::Sum,
321    cost_bound: &W::Sum,
322    config: &[usize],
323) -> Result<bool, crate::traits::EvaluationError> {
324    let num_vertices = graph.num_vertices();
325    if config.len() != num_vertices {
326        return Ok(false);
327    }
328    if vertex_weights.len() != num_vertices || arc_costs.len() != graph.num_arcs() {
329        return Ok(false);
330    }
331    if config.iter().any(|&label| label >= num_vertices) {
332        return Ok(false);
333    }
334
335    let mut partition_weights = vec![W::Sum::zero(); num_vertices];
336    let mut used_labels = vec![false; num_vertices];
337    for (vertex, &label) in config.iter().enumerate() {
338        used_labels[label] = true;
339        partition_weights[label] = W::checked_add_to_sum(
340            partition_weights[label].clone(),
341            vertex_weights[vertex].to_sum(),
342            "summing acyclic partition vertex weights",
343        )?;
344    }
345    if partition_weights
346        .iter()
347        .zip(&used_labels)
348        .any(|(weight, used)| *used && weight > weight_bound)
349    {
350        return Ok(false);
351    }
352
353    let mut dense_label = vec![usize::MAX; num_vertices];
354    let mut next_dense = 0usize;
355    for (label, used) in used_labels.iter().enumerate() {
356        if *used {
357            dense_label[label] = next_dense;
358            next_dense += 1;
359        }
360    }
361
362    let mut total_cost = W::Sum::zero();
363    let mut quotient_arcs = BTreeSet::new();
364    for ((source, target), cost) in graph.arcs().iter().zip(arc_costs.iter()) {
365        let source_label = config[*source];
366        let target_label = config[*target];
367        if source_label == target_label {
368            continue;
369        }
370        total_cost = W::checked_add_to_sum(
371            total_cost,
372            cost.to_sum(),
373            "summing acyclic partition arc costs",
374        )?;
375        quotient_arcs.insert((dense_label[source_label], dense_label[target_label]));
376    }
377
378    Ok(total_cost <= *cost_bound
379        && DirectedGraph::new(next_dense, quotient_arcs.into_iter().collect()).is_dag())
380}
381
382crate::declare_variants! {
383    default AcyclicPartition<i64> => "num_vertices^num_vertices" create AcyclicPartitionCreateSpec,
384}
385
386crate::register_brute_force! {
387    AcyclicPartition<i64>,
388}
389
390#[cfg(feature = "example-db")]
391pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
392    vec![crate::example_db::specs::ModelExampleSpec {
393        id: "acyclic_partition",
394        instance: Box::new(AcyclicPartition::new(
395            DirectedGraph::new(
396                6,
397                vec![
398                    (0, 1),
399                    (0, 2),
400                    (1, 3),
401                    (1, 4),
402                    (2, 4),
403                    (2, 5),
404                    (3, 5),
405                    (4, 5),
406                ],
407            ),
408            vec![2, 3, 2, 1, 3, 1],
409            vec![1; 8],
410            5,
411            5,
412        )),
413        optimal_config: serde_json::json!(vec![0, 1, 0, 2, 2, 2]),
414        optimal_value: serde_json::json!(true),
415    }]
416}
417
418#[cfg(test)]
419#[path = "../../unit_tests/models/graph/acyclic_partition.rs"]
420mod tests;