Skip to main content

problemreductions/models/misc/
sequencing_to_minimize_weighted_completion_time.rs

1//! Sequencing to Minimize Weighted Completion Time problem implementation.
2//!
3//! A classical NP-hard single-machine scheduling problem (SS4 from
4//! Garey & Johnson, 1979) where tasks with processing times, weights,
5//! and precedence constraints must be scheduled to minimize the total
6//! weighted completion time.
7//!
8//! This model accepts zero-length tasks in addition to positive-length
9//! tasks. That choice matches the standard Lawler reduction from
10//! Optimal Linear Arrangement, which uses zero-length edge jobs instead
11//! of padding them to unit length.
12
13use crate::registry::{FieldInfo, ProblemSchemaEntry};
14use crate::traits::Problem;
15use crate::types::Min;
16use serde::{Deserialize, Serialize};
17
18inventory::submit! {
19    ProblemSchemaEntry {
20        name: "SequencingToMinimizeWeightedCompletionTime",
21        display_name: "Sequencing to Minimize Weighted Completion Time",
22        aliases: &[],
23        dimensions: &[],
24        module_path: module_path!(),
25        description: "Schedule tasks with lengths, weights, and precedence constraints to minimize total weighted completion time",
26        fields: &[
27            FieldInfo { name: "lengths", type_name: "Vec<u64>", description: "Processing time l(t) for each task" },
28            FieldInfo { name: "weights", type_name: "Vec<u64>", description: "Weight w(t) for each task" },
29            FieldInfo { name: "precedences", type_name: "Vec<(usize, usize)>", description: "Precedence pairs (predecessor, successor)" },
30        ],
31    }
32}
33
34/// Sequencing to Minimize Weighted Completion Time problem.
35///
36/// Given tasks with nonnegative processing times `l(t)`, weights `w(t)`, and precedence
37/// constraints, find a single-machine schedule that respects the precedences
38/// and minimizes `sum_t w(t) * C(t)`, where `C(t)` is the completion time of
39/// task `t`.
40///
41/// Configurations use Lehmer code with `dims() = [n, n-1, ..., 1]`.
42#[derive(Debug, Clone, Serialize)]
43pub struct SequencingToMinimizeWeightedCompletionTime {
44    lengths: Vec<u64>,
45    weights: Vec<u64>,
46    precedences: Vec<(usize, usize)>,
47}
48
49#[derive(Deserialize)]
50struct SequencingToMinimizeWeightedCompletionTimeSerde {
51    lengths: Vec<u64>,
52    weights: Vec<u64>,
53    precedences: Vec<(usize, usize)>,
54}
55
56impl SequencingToMinimizeWeightedCompletionTime {
57    fn validate(
58        lengths: &[u64],
59        weights: &[u64],
60        precedences: &[(usize, usize)],
61    ) -> Result<(), String> {
62        if lengths.len() != weights.len() {
63            return Err("lengths length must equal weights length".to_string());
64        }
65
66        let num_tasks = lengths.len();
67        for &(pred, succ) in precedences {
68            if pred >= num_tasks {
69                return Err(format!(
70                    "predecessor index {} out of range (num_tasks = {})",
71                    pred, num_tasks
72                ));
73            }
74            if succ >= num_tasks {
75                return Err(format!(
76                    "successor index {} out of range (num_tasks = {})",
77                    succ, num_tasks
78                ));
79            }
80        }
81
82        Ok(())
83    }
84
85    /// Create a new sequencing instance.
86    ///
87    /// # Panics
88    ///
89    /// Panics if `lengths.len() != weights.len()` or if any precedence
90    /// endpoint is out of range.
91    pub fn new(lengths: Vec<u64>, weights: Vec<u64>, precedences: Vec<(usize, usize)>) -> Self {
92        Self::validate(&lengths, &weights, &precedences).unwrap_or_else(|err| panic!("{err}"));
93
94        Self {
95            lengths,
96            weights,
97            precedences,
98        }
99    }
100
101    /// Returns the number of tasks.
102    pub fn num_tasks(&self) -> usize {
103        self.lengths.len()
104    }
105
106    /// Returns the processing times.
107    pub fn lengths(&self) -> &[u64] {
108        &self.lengths
109    }
110
111    /// Returns the task weights.
112    pub fn weights(&self) -> &[u64] {
113        &self.weights
114    }
115
116    /// Returns the precedence constraints.
117    pub fn precedences(&self) -> &[(usize, usize)] {
118        &self.precedences
119    }
120
121    /// Returns the number of precedence constraints.
122    pub fn num_precedences(&self) -> usize {
123        self.precedences.len()
124    }
125
126    /// Returns the sum of all processing times.
127    pub fn total_processing_time(&self) -> u64 {
128        self.lengths
129            .iter()
130            .try_fold(0u64, |acc, &length| acc.checked_add(length))
131            .expect("total processing time overflowed u64")
132    }
133
134    fn decode_schedule(&self, config: &[usize]) -> Option<Vec<usize>> {
135        super::decode_lehmer(config, self.num_tasks())
136    }
137
138    fn weighted_completion_time(&self, schedule: &[usize]) -> Min<u64> {
139        let n = self.num_tasks();
140        let mut positions = vec![0usize; n];
141        let mut completion_times = vec![0u64; n];
142        let mut elapsed = 0u64;
143
144        for (position, &task) in schedule.iter().enumerate() {
145            positions[task] = position;
146            elapsed = elapsed
147                .checked_add(self.lengths[task])
148                .expect("total processing time overflowed u64");
149            completion_times[task] = elapsed;
150        }
151
152        for &(pred, succ) in &self.precedences {
153            if positions[pred] >= positions[succ] {
154                return Min(None);
155            }
156        }
157
158        let total = completion_times
159            .iter()
160            .enumerate()
161            .try_fold(0u64, |acc, (task, &completion)| -> Option<u64> {
162                let weighted_completion = completion.checked_mul(self.weights[task])?;
163                acc.checked_add(weighted_completion)
164            })
165            .expect("weighted completion time overflowed u64");
166        Min(Some(total))
167    }
168}
169
170impl TryFrom<SequencingToMinimizeWeightedCompletionTimeSerde>
171    for SequencingToMinimizeWeightedCompletionTime
172{
173    type Error = String;
174
175    fn try_from(
176        value: SequencingToMinimizeWeightedCompletionTimeSerde,
177    ) -> Result<Self, Self::Error> {
178        Self::validate(&value.lengths, &value.weights, &value.precedences)?;
179        Ok(Self {
180            lengths: value.lengths,
181            weights: value.weights,
182            precedences: value.precedences,
183        })
184    }
185}
186
187impl<'de> Deserialize<'de> for SequencingToMinimizeWeightedCompletionTime {
188    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
189    where
190        D: serde::Deserializer<'de>,
191    {
192        let value = SequencingToMinimizeWeightedCompletionTimeSerde::deserialize(deserializer)?;
193        Self::try_from(value).map_err(serde::de::Error::custom)
194    }
195}
196
197impl Problem for SequencingToMinimizeWeightedCompletionTime {
198    const NAME: &'static str = "SequencingToMinimizeWeightedCompletionTime";
199    type Value = Min<u64>;
200
201    fn variant() -> Vec<(&'static str, &'static str)> {
202        crate::variant_params![]
203    }
204
205    fn dims(&self) -> Vec<usize> {
206        super::lehmer_dims(self.num_tasks())
207    }
208
209    fn evaluate(&self, config: &[usize]) -> Min<u64> {
210        let Some(schedule) = self.decode_schedule(config) else {
211            return Min(None);
212        };
213        self.weighted_completion_time(&schedule)
214    }
215}
216
217crate::declare_variants! {
218    default SequencingToMinimizeWeightedCompletionTime => "factorial(num_tasks)",
219}
220
221#[cfg(feature = "example-db")]
222pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
223    vec![crate::example_db::specs::ModelExampleSpec {
224        id: "sequencing_to_minimize_weighted_completion_time",
225        instance: Box::new(SequencingToMinimizeWeightedCompletionTime::new(
226            vec![2, 1, 3, 1, 2],
227            vec![3, 5, 1, 4, 2],
228            vec![(0, 2), (1, 4)],
229        )),
230        optimal_config: vec![1, 2, 0, 1, 0],
231        optimal_value: serde_json::json!(46),
232    }]
233}
234
235#[cfg(test)]
236#[path = "../../unit_tests/models/misc/sequencing_to_minimize_weighted_completion_time.rs"]
237mod tests;