diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index b5a352734..f74d8bd51 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -121,6 +121,7 @@ "FlowShopScheduling": [Flow Shop Scheduling], "StaffScheduling": [Staff Scheduling], "MultiprocessorScheduling": [Multiprocessor Scheduling], + "PrecedenceConstrainedScheduling": [Precedence Constrained Scheduling], "MinimumTardinessSequencing": [Minimum Tardiness Sequencing], "SumOfSquaresPartition": [Sum of Squares Partition], "SequencingWithinIntervals": [Sequencing Within Intervals], @@ -2809,6 +2810,28 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ] } +#{ + let x = load-model-example("PrecedenceConstrainedScheduling") + let n = x.instance.num_tasks + let m = x.instance.num_processors + let D = x.instance.deadline + let precs = x.instance.precedences + let sigma = x.optimal_config + // Group tasks by assigned slot + let tasks-by-slot = range(D).map(s => + range(n).filter(i => sigma.at(i) == s) + ) + [ + #problem-def("PrecedenceConstrainedScheduling")[ + Given a set $T$ of $n$ unit-length tasks, a partial order $prec$ on $T$, a number $m in ZZ^+$ of processors, and a deadline $D in ZZ^+$, determine whether there exists a schedule $sigma: T -> {0, dots, D-1}$ such that (i) for every time slot $t$, at most $m$ tasks are assigned to $t$, and (ii) for every precedence $t_i prec t_j$, we have $sigma(t_j) >= sigma(t_i) + 1$. + ][ + Precedence Constrained Scheduling is problem SS9 in Garey & Johnson @garey1979. NP-complete via reduction from 3SAT @ullman1975. Remains NP-complete even for $D = 3$ @lenstra1978. Solvable in polynomial time for $m = 2$ by the Coffman--Graham algorithm @coffman1972, for forest-structured precedences @hu1961, and for chordal complement precedences @papadimitriou1979. A subset dynamic programming approach solves the general case in $O(2^n dot n)$ time by enumerating subsets of completed tasks at each time step. + + *Example.* Let $n = #n$ tasks, $m = #m$ processors, $D = #D$. Precedences: #precs.map(p => $t_#(p.at(0)) prec t_#(p.at(1))$).join(", "). A feasible schedule assigns $sigma = (#sigma.map(s => str(s)).join(", "))$: #range(D).map(s => [slot #s has ${#tasks-by-slot.at(s).map(i => $t_#i$).join(", ")}$]).join(", "). All precedences are satisfied and no slot exceeds $m = #m$. + ] + ] +} + #{ let x = load-model-example("SequencingWithinIntervals") let ntasks = x.instance.lengths.len() diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 3aa8d155c..2d3eb8fac 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -851,3 +851,56 @@ @article{boothlueker1976 year = {1976}, doi = {10.1016/S0022-0000(76)80045-1} } + +@article{ullman1975, + author = {Jeffrey D. Ullman}, + title = {NP-Complete Scheduling Problems}, + journal = {Journal of Computer and System Sciences}, + volume = {10}, + number = {3}, + pages = {384--393}, + year = {1975}, + doi = {10.1016/S0022-0000(75)80008-0} +} + +@article{lenstra1978, + author = {Jan Karel Lenstra and Alexander H. G. Rinnooy Kan}, + title = {Complexity of Scheduling under Precedence Constraints}, + journal = {Operations Research}, + volume = {26}, + number = {1}, + pages = {22--35}, + year = {1978}, + doi = {10.1287/opre.26.1.22} +} + +@article{coffman1972, + author = {Edward G. Coffman and Ronald L. Graham}, + title = {Optimal Scheduling for Two-Processor Systems}, + journal = {Acta Informatica}, + volume = {1}, + number = {3}, + pages = {200--213}, + year = {1972}, + doi = {10.1007/BF00288685} +} + +@article{hu1961, + author = {Te Chiang Hu}, + title = {Parallel Sequencing and Assembly Line Problems}, + journal = {Operations Research}, + volume = {9}, + number = {6}, + pages = {841--848}, + year = {1961}, + doi = {10.1287/opre.9.6.841} +} + +@inproceedings{papadimitriou1979, + author = {Christos H. Papadimitriou and Mihalis Yannakakis}, + title = {Scheduling Interval-Ordered Tasks}, + booktitle = {Proceedings of the 10th Annual ACM Symposium on Theory of Computing (STOC)}, + pages = {1--7}, + year = {1979}, + doi = {10.1145/800135.804393} +} diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index d349bec48..b8b69b96d 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -9,6 +9,7 @@ //! - [`LongestCommonSubsequence`]: Longest Common Subsequence //! - [`MinimumTardinessSequencing`]: Minimize tardy tasks in single-machine scheduling //! - [`PaintShop`]: Minimize color switches in paint shop scheduling +//! - [`PrecedenceConstrainedScheduling`]: Schedule unit tasks on processors by deadline //! - [`SequencingWithReleaseTimesAndDeadlines`]: Single-machine scheduling feasibility //! - [`SequencingWithinIntervals`]: Schedule tasks within time windows //! - [`ShortestCommonSupersequence`]: Find a common supersequence of bounded length @@ -24,6 +25,7 @@ mod longest_common_subsequence; mod minimum_tardiness_sequencing; mod multiprocessor_scheduling; pub(crate) mod paintshop; +mod precedence_constrained_scheduling; mod sequencing_with_release_times_and_deadlines; mod sequencing_within_intervals; pub(crate) mod shortest_common_supersequence; @@ -40,6 +42,7 @@ pub use longest_common_subsequence::LongestCommonSubsequence; pub use minimum_tardiness_sequencing::MinimumTardinessSequencing; pub use multiprocessor_scheduling::MultiprocessorScheduling; pub use paintshop::PaintShop; +pub use precedence_constrained_scheduling::PrecedenceConstrainedScheduling; pub use sequencing_with_release_times_and_deadlines::SequencingWithReleaseTimesAndDeadlines; pub use sequencing_within_intervals::SequencingWithinIntervals; pub use shortest_common_supersequence::ShortestCommonSupersequence; @@ -61,6 +64,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec", description: "Precedence pairs (i, j) meaning task i must finish before task j starts" }, + ], + } +} + +/// The Precedence Constrained Scheduling problem. +/// +/// Given `n` unit-length tasks with precedence constraints (a partial order), +/// `m` processors, and a deadline `D`, determine whether there exists a schedule +/// assigning each task to a time slot in `{0, ..., D-1}` such that: +/// - At most `m` tasks are assigned to any single time slot +/// - For each precedence `(i, j)`: task `j` starts after task `i` completes, +/// i.e., `slot(j) >= slot(i) + 1` +/// +/// # Representation +/// +/// Each task has a variable in `{0, ..., D-1}` representing its assigned time slot. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::PrecedenceConstrainedScheduling; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // 4 tasks, 2 processors, deadline 3, with t0 < t2 and t1 < t3 +/// let problem = PrecedenceConstrainedScheduling::new(4, 2, 3, vec![(0, 2), (1, 3)]); +/// let solver = BruteForce::new(); +/// let solution = solver.find_satisfying(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PrecedenceConstrainedScheduling { + num_tasks: usize, + num_processors: usize, + deadline: usize, + precedences: Vec<(usize, usize)>, +} + +impl PrecedenceConstrainedScheduling { + /// Create a new Precedence Constrained Scheduling instance. + /// + /// # Panics + /// + /// Panics if `num_processors` or `deadline` is zero (when `num_tasks > 0`), + /// or if any precedence index is out of bounds (>= num_tasks). + pub fn new( + num_tasks: usize, + num_processors: usize, + deadline: usize, + precedences: Vec<(usize, usize)>, + ) -> Self { + if num_tasks > 0 { + assert!( + num_processors > 0, + "num_processors must be > 0 when there are tasks" + ); + assert!(deadline > 0, "deadline must be > 0 when there are tasks"); + } + for &(i, j) in &precedences { + assert!( + i < num_tasks && j < num_tasks, + "Precedence ({}, {}) out of bounds for {} tasks", + i, + j, + num_tasks + ); + } + Self { + num_tasks, + num_processors, + deadline, + precedences, + } + } + + /// Get the number of tasks. + pub fn num_tasks(&self) -> usize { + self.num_tasks + } + + /// Get the number of processors. + pub fn num_processors(&self) -> usize { + self.num_processors + } + + /// Get the deadline. + pub fn deadline(&self) -> usize { + self.deadline + } + + /// Get the precedence constraints. + pub fn precedences(&self) -> &[(usize, usize)] { + &self.precedences + } +} + +impl Problem for PrecedenceConstrainedScheduling { + const NAME: &'static str = "PrecedenceConstrainedScheduling"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + vec![self.deadline; self.num_tasks] + } + + fn evaluate(&self, config: &[usize]) -> bool { + if config.len() != self.num_tasks { + return false; + } + // Check all values are valid time slots + if config.iter().any(|&v| v >= self.deadline) { + return false; + } + // Check processor capacity: at most num_processors tasks per time slot + let mut slot_count = vec![0usize; self.deadline]; + for &slot in config { + slot_count[slot] += 1; + if slot_count[slot] > self.num_processors { + return false; + } + } + // Check precedence constraints: for (i, j), slot[j] >= slot[i] + 1 + for &(i, j) in &self.precedences { + if config[j] < config[i] + 1 { + return false; + } + } + true + } +} + +impl SatisfactionProblem for PrecedenceConstrainedScheduling {} + +crate::declare_variants! { + default sat PrecedenceConstrainedScheduling => "2^num_tasks", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "precedence_constrained_scheduling", + // Issue #501 example: 8 tasks, 3 processors, deadline 4 + instance: Box::new(PrecedenceConstrainedScheduling::new( + 8, + 3, + 4, + vec![ + (0, 2), + (0, 3), + (1, 3), + (1, 4), + (2, 5), + (3, 6), + (4, 6), + (5, 7), + (6, 7), + ], + )), + // Valid schedule: slot 0: {t0,t1}, slot 1: {t2,t3,t4}, slot 2: {t5,t6}, slot 3: {t7} + optimal_config: vec![0, 0, 1, 1, 1, 2, 2, 3], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/precedence_constrained_scheduling.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index 02506c079..15f9cc7a5 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -25,8 +25,9 @@ pub use graph::{ pub use misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, - SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence, - StaffScheduling, StringToStringCorrection, SubsetSum, SumOfSquaresPartition, + PrecedenceConstrainedScheduling, SequencingWithReleaseTimesAndDeadlines, + SequencingWithinIntervals, ShortestCommonSupersequence, StaffScheduling, + StringToStringCorrection, SubsetSum, SumOfSquaresPartition, }; pub use set::{ ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking, diff --git a/src/unit_tests/models/misc/precedence_constrained_scheduling.rs b/src/unit_tests/models/misc/precedence_constrained_scheduling.rs new file mode 100644 index 000000000..8a3ace315 --- /dev/null +++ b/src/unit_tests/models/misc/precedence_constrained_scheduling.rs @@ -0,0 +1,135 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; + +#[test] +fn test_precedence_constrained_scheduling_basic() { + let problem = PrecedenceConstrainedScheduling::new(4, 2, 3, vec![(0, 2), (1, 3)]); + assert_eq!(problem.num_tasks(), 4); + assert_eq!(problem.num_processors(), 2); + assert_eq!(problem.deadline(), 3); + assert_eq!(problem.precedences(), &[(0, 2), (1, 3)]); + assert_eq!(problem.dims(), vec![3; 4]); + assert_eq!( + ::NAME, + "PrecedenceConstrainedScheduling" + ); + assert_eq!( + ::variant(), + vec![] + ); +} + +#[test] +fn test_precedence_constrained_scheduling_evaluate_valid() { + // Issue example: 8 tasks, 3 processors, deadline 4 + // Precedences: 0<2, 0<3, 1<3, 1<4, 2<5, 3<6, 4<6, 5<7, 6<7 + let problem = PrecedenceConstrainedScheduling::new( + 8, + 3, + 4, + vec![ + (0, 2), + (0, 3), + (1, 3), + (1, 4), + (2, 5), + (3, 6), + (4, 6), + (5, 7), + (6, 7), + ], + ); + // Valid schedule: slot 0: {t0, t1}, slot 1: {t2, t3, t4}, slot 2: {t5, t6}, slot 3: {t7} + let config = vec![0, 0, 1, 1, 1, 2, 2, 3]; + assert!(problem.evaluate(&config)); +} + +#[test] +fn test_precedence_constrained_scheduling_evaluate_invalid_precedence() { + // t0 < t1, but we assign both to slot 0 + let problem = PrecedenceConstrainedScheduling::new(2, 2, 3, vec![(0, 1)]); + assert!(!problem.evaluate(&[0, 0])); // slot[1] = 0 < slot[0] + 1 = 1 +} + +#[test] +fn test_precedence_constrained_scheduling_evaluate_invalid_capacity() { + // 3 tasks, 2 processors, all in slot 0 + let problem = PrecedenceConstrainedScheduling::new(3, 2, 2, vec![]); + assert!(!problem.evaluate(&[0, 0, 0])); // 3 tasks in slot 0, capacity 2 +} + +#[test] +fn test_precedence_constrained_scheduling_evaluate_wrong_config_length() { + let problem = PrecedenceConstrainedScheduling::new(3, 2, 3, vec![]); + assert!(!problem.evaluate(&[0, 1])); + assert!(!problem.evaluate(&[0, 1, 2, 0])); +} + +#[test] +fn test_precedence_constrained_scheduling_evaluate_invalid_variable_value() { + let problem = PrecedenceConstrainedScheduling::new(2, 2, 3, vec![]); + assert!(!problem.evaluate(&[0, 3])); // 3 >= deadline=3 +} + +#[test] +fn test_precedence_constrained_scheduling_brute_force() { + // Small instance: 3 tasks, 2 processors, deadline 2, t0 < t2 + let problem = PrecedenceConstrainedScheduling::new(3, 2, 2, vec![(0, 2)]); + let solver = BruteForce::new(); + let solution = solver + .find_satisfying(&problem) + .expect("should find a solution"); + assert!(problem.evaluate(&solution)); +} + +#[test] +fn test_precedence_constrained_scheduling_brute_force_all() { + let problem = PrecedenceConstrainedScheduling::new(3, 2, 2, vec![(0, 2)]); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert!(!solutions.is_empty()); + for sol in &solutions { + assert!(problem.evaluate(sol)); + } +} + +#[test] +fn test_precedence_constrained_scheduling_unsatisfiable() { + // 3 tasks in a chain t0 < t1 < t2, but only deadline 2 (need 3 slots) + let problem = PrecedenceConstrainedScheduling::new(3, 1, 2, vec![(0, 1), (1, 2)]); + let solver = BruteForce::new(); + assert!(solver.find_satisfying(&problem).is_none()); +} + +#[test] +fn test_precedence_constrained_scheduling_serialization() { + let problem = PrecedenceConstrainedScheduling::new(4, 2, 3, vec![(0, 2), (1, 3)]); + let json = serde_json::to_value(&problem).unwrap(); + let restored: PrecedenceConstrainedScheduling = serde_json::from_value(json).unwrap(); + assert_eq!(restored.num_tasks(), problem.num_tasks()); + assert_eq!(restored.num_processors(), problem.num_processors()); + assert_eq!(restored.deadline(), problem.deadline()); + assert_eq!(restored.precedences(), problem.precedences()); +} + +#[test] +fn test_precedence_constrained_scheduling_empty() { + let problem = PrecedenceConstrainedScheduling::new(0, 1, 1, vec![]); + assert_eq!(problem.num_tasks(), 0); + assert_eq!(problem.dims(), Vec::::new()); + assert!(problem.evaluate(&[])); +} + +#[test] +fn test_precedence_constrained_scheduling_no_precedences() { + // 4 tasks, 2 processors, deadline 2, no precedences + let problem = PrecedenceConstrainedScheduling::new(4, 2, 2, vec![]); + // 2 tasks per slot, 2 slots = 4 tasks + assert!(problem.evaluate(&[0, 0, 1, 1])); + let solver = BruteForce::new(); + let solution = solver + .find_satisfying(&problem) + .expect("should find a solution"); + assert!(problem.evaluate(&solution)); +}