diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index f50c5ee5..ef72e939 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -91,6 +91,7 @@ "SubsetSum": [Subset Sum], "MinimumFeedbackArcSet": [Minimum Feedback Arc Set], "MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set], + "SequencingWithReleaseTimesAndDeadlines": [Sequencing with Release Times and Deadlines], "ShortestCommonSupersequence": [Shortest Common Supersequence], "MinimumSumMulticenter": [Minimum Sum Multicenter], "SubgraphIsomorphism": [Subgraph Isomorphism], @@ -1624,6 +1625,15 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS *Example.* Let $A = {3, 7, 1, 8, 2, 4}$ ($n = 6$) and target $B = 11$. Selecting $A' = {3, 8}$ gives sum $3 + 8 = 11 = B$. Another solution: $A' = {7, 4}$ with sum $7 + 4 = 11 = B$. ] +#problem-def("SequencingWithReleaseTimesAndDeadlines")[ + Given a set $T$ of $n$ tasks and, for each task $t in T$, a processing time $ell(t) in ZZ^+$, a release time $r(t) in ZZ^(>=0)$, and a deadline $d(t) in ZZ^+$, determine whether there exists a one-processor schedule $sigma: T -> ZZ^(>=0)$ such that for all $t in T$: $sigma(t) >= r(t)$, $sigma(t) + ell(t) <= d(t)$, and no two tasks overlap (i.e., $sigma(t) > sigma(t')$ implies $sigma(t) >= sigma(t') + ell(t')$). +][ + Problem SS1 in Garey and Johnson's appendix @garey1979, and a fundamental single-machine scheduling feasibility problem. It is strongly NP-complete by reduction from 3-Partition, so no pseudo-polynomial time algorithm exists unless P = NP. The problem becomes polynomial-time solvable when: (1) all task lengths equal 1, (2) preemption is allowed, or (3) all release times are zero. The best known exact algorithm for the general case runs in $O^*(2^n dot n)$ time via dynamic programming on task subsets. + + *Example.* Let $T = {t_1, t_2, t_3}$ with $ell(t_1) = 2$, $r(t_1) = 0$, $d(t_1) = 5$; $ell(t_2) = 3$, $r(t_2) = 1$, $d(t_2) = 6$; $ell(t_3) = 1$, $r(t_3) = 2$, $d(t_3) = 4$. A feasible schedule: $sigma(t_1) = 0$ (runs $[0, 2)$), $sigma(t_3) = 2$ (runs $[2, 3)$), $sigma(t_2) = 3$ (runs $[3, 6)$). All release and deadline constraints are satisfied with no overlap. +] + + #{ let x = load-model-example("ShortestCommonSupersequence") let alpha-size = x.instance.alphabet_size diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 3a6e9df8..a06e53e6 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -270,6 +270,17 @@ } ] }, + { + "name": "LongestCommonSubsequence", + "description": "Find the longest string that is a subsequence of every input string", + "fields": [ + { + "name": "strings", + "type_name": "Vec>", + "description": "The input strings" + } + ] + }, { "name": "MaxCut", "description": "Find maximum weight cut in a graph", @@ -594,6 +605,27 @@ } ] }, + { + "name": "SequencingWithReleaseTimesAndDeadlines", + "description": "Single-machine scheduling feasibility: can all tasks be scheduled within their release-deadline windows without overlap?", + "fields": [ + { + "name": "lengths", + "type_name": "Vec", + "description": "Processing time l(t) for each task (positive)" + }, + { + "name": "release_times", + "type_name": "Vec", + "description": "Release time r(t) for each task (non-negative)" + }, + { + "name": "deadlines", + "type_name": "Vec", + "description": "Deadline d(t) for each task (positive)" + } + ] + }, { "name": "ShortestCommonSupersequence", "description": "Find a common supersequence of bounded length for a set of strings", diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index c42f69c2..c731eb55 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -385,7 +385,13 @@ pub struct CreateArgs { /// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0) #[arg(long)] pub arcs: Option, - /// Deadlines for MinimumTardinessSequencing (comma-separated, e.g., "5,5,5,3,3") + /// Task processing times for scheduling problems (comma-separated, e.g., "3,2,4") + #[arg(long)] + pub lengths: Option, + /// Task release times for scheduling problems (comma-separated, e.g., "0,1,5") + #[arg(long)] + pub release_times: Option, + /// Deadlines for scheduling problems (comma-separated, e.g., "5,5,5,3,3") #[arg(long)] pub deadlines: Option, /// Precedence pairs for MinimumTardinessSequencing (e.g., "0>3,1>3,1>4,2>4") diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index a1444224..90f68990 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -9,7 +9,7 @@ use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; use problemreductions::models::graph::{GraphPartitioning, HamiltonianPath}; use problemreductions::models::misc::{ BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing, - PaintShop, ShortestCommonSupersequence, SubsetSum, + PaintShop, SequencingWithReleaseTimesAndDeadlines, ShortestCommonSupersequence, SubsetSum, }; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; @@ -57,6 +57,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.pattern.is_none() && args.strings.is_none() && args.arcs.is_none() + && args.lengths.is_none() + && args.release_times.is_none() && args.deadlines.is_none() && args.precedence_pairs.is_none() && args.task_lengths.is_none() @@ -1086,6 +1088,47 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // SequencingWithReleaseTimesAndDeadlines + "SequencingWithReleaseTimesAndDeadlines" => { + let lengths_str = args.lengths.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "SequencingWithReleaseTimesAndDeadlines requires --lengths, --release-times, and --deadlines\n\n\ + Usage: pred create SequencingWithReleaseTimesAndDeadlines --lengths 3,2,4 --release-times 0,1,5 --deadlines 5,6,10" + ) + })?; + let release_str = args.release_times.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "SequencingWithReleaseTimesAndDeadlines requires --release-times\n\n\ + Usage: pred create SequencingWithReleaseTimesAndDeadlines --lengths 3,2,4 --release-times 0,1,5 --deadlines 5,6,10" + ) + })?; + let deadlines_str = args.deadlines.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "SequencingWithReleaseTimesAndDeadlines requires --deadlines\n\n\ + Usage: pred create SequencingWithReleaseTimesAndDeadlines --lengths 3,2,4 --release-times 0,1,5 --deadlines 5,6,10" + ) + })?; + let lengths: Vec = util::parse_comma_list(lengths_str)?; + let release_times: Vec = util::parse_comma_list(release_str)?; + let deadlines: Vec = util::parse_comma_list(deadlines_str)?; + if lengths.len() != release_times.len() || lengths.len() != deadlines.len() { + bail!( + "All three lists must have the same length: lengths={}, release_times={}, deadlines={}", + lengths.len(), + release_times.len(), + deadlines.len() + ); + } + ( + ser(SequencingWithReleaseTimesAndDeadlines::new( + lengths, + release_times, + deadlines, + ))?, + resolved_variant.clone(), + ) + } + _ => bail!("{}", crate::problem_name::unknown_problem_error(canonical)), }; diff --git a/src/lib.rs b/src/lib.rs index bceccfe2..e08748d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,7 +56,8 @@ pub mod prelude { }; pub use crate::models::misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, - MinimumTardinessSequencing, PaintShop, ShortestCommonSupersequence, SubsetSum, + MinimumTardinessSequencing, PaintShop, SequencingWithReleaseTimesAndDeadlines, + ShortestCommonSupersequence, SubsetSum, }; pub use crate::models::set::{ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering}; diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index cc96aa83..8d4dd76b 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -8,6 +8,7 @@ //! - [`LongestCommonSubsequence`]: Longest Common Subsequence //! - [`MinimumTardinessSequencing`]: Minimize tardy tasks in single-machine scheduling //! - [`PaintShop`]: Minimize color switches in paint shop scheduling +//! - [`SequencingWithReleaseTimesAndDeadlines`]: Single-machine scheduling feasibility //! - [`ShortestCommonSupersequence`]: Find a common supersequence of bounded length //! - [`SubsetSum`]: Find a subset summing to exactly a target value @@ -18,6 +19,7 @@ mod knapsack; mod longest_common_subsequence; mod minimum_tardiness_sequencing; pub(crate) mod paintshop; +mod sequencing_with_release_times_and_deadlines; pub(crate) mod shortest_common_supersequence; mod subset_sum; @@ -28,6 +30,7 @@ pub use knapsack::Knapsack; pub use longest_common_subsequence::LongestCommonSubsequence; pub use minimum_tardiness_sequencing::MinimumTardinessSequencing; pub use paintshop::PaintShop; +pub use sequencing_with_release_times_and_deadlines::SequencingWithReleaseTimesAndDeadlines; pub use shortest_common_supersequence::ShortestCommonSupersequence; pub use subset_sum::SubsetSum; @@ -38,5 +41,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec", description: "Processing time l(t) for each task (positive)" }, + FieldInfo { name: "release_times", type_name: "Vec", description: "Release time r(t) for each task (non-negative)" }, + FieldInfo { name: "deadlines", type_name: "Vec", description: "Deadline d(t) for each task (positive)" }, + ], + } +} + +/// Sequencing with Release Times and Deadlines. +/// +/// Given a set of `n` tasks, each with a processing time `l(t)`, release time +/// `r(t)`, and deadline `d(t)`, determine whether there exists a one-processor +/// schedule where each task starts no earlier than its release time and finishes +/// by its deadline, with no two tasks overlapping. +/// +/// # Representation +/// +/// Uses a permutation encoding (Lehmer code), where `config[i]` selects which +/// remaining task to schedule next from the pool of unscheduled tasks. +/// `dims() = [n, n-1, ..., 2, 1]`. Tasks are scheduled left-to-right: each +/// task starts at `max(release_time, current_time)`. The schedule is feasible +/// iff every task finishes by its deadline. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::SequencingWithReleaseTimesAndDeadlines; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// let problem = SequencingWithReleaseTimesAndDeadlines::new( +/// vec![1, 2, 1], +/// vec![0, 0, 2], +/// vec![3, 3, 4], +/// ); +/// let solver = BruteForce::new(); +/// let solution = solver.find_satisfying(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SequencingWithReleaseTimesAndDeadlines { + lengths: Vec, + release_times: Vec, + deadlines: Vec, +} + +impl SequencingWithReleaseTimesAndDeadlines { + /// Create a new instance. + /// + /// # Panics + /// + /// Panics if the three vectors have different lengths. + pub fn new(lengths: Vec, release_times: Vec, deadlines: Vec) -> Self { + assert_eq!(lengths.len(), release_times.len()); + assert_eq!(lengths.len(), deadlines.len()); + Self { + lengths, + release_times, + deadlines, + } + } + + /// Returns the processing times. + pub fn lengths(&self) -> &[u64] { + &self.lengths + } + + /// Returns the release times. + pub fn release_times(&self) -> &[u64] { + &self.release_times + } + + /// Returns the deadlines. + pub fn deadlines(&self) -> &[u64] { + &self.deadlines + } + + /// Returns the number of tasks. + pub fn num_tasks(&self) -> usize { + self.lengths.len() + } + + /// Returns the time horizon (maximum deadline). + pub fn time_horizon(&self) -> u64 { + self.deadlines.iter().copied().max().unwrap_or(0) + } +} + +impl Problem for SequencingWithReleaseTimesAndDeadlines { + const NAME: &'static str = "SequencingWithReleaseTimesAndDeadlines"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + let n = self.num_tasks(); + (0..n).rev().map(|i| i + 1).collect() + } + + fn evaluate(&self, config: &[usize]) -> bool { + let n = self.num_tasks(); + if config.len() != n { + return false; + } + + // Decode Lehmer code into a permutation of task indices. + let mut available: Vec = (0..n).collect(); + let mut schedule = Vec::with_capacity(n); + for &c in config.iter() { + if c >= available.len() { + return false; + } + schedule.push(available.remove(c)); + } + + // Schedule tasks left-to-right: each task starts at max(release_time, current_time). + let mut current_time: u64 = 0; + for &task in &schedule { + let start = current_time.max(self.release_times[task]); + let finish = start + self.lengths[task]; + if finish > self.deadlines[task] { + return false; + } + current_time = finish; + } + + true + } +} + +impl SatisfactionProblem for SequencingWithReleaseTimesAndDeadlines {} + +crate::declare_variants! { + default sat SequencingWithReleaseTimesAndDeadlines => "2^num_tasks * num_tasks", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "sequencing_with_release_times_and_deadlines", + build: || { + // 5 tasks from issue example. + // Feasible schedule order: t3, t0, t1, t2, t4 + let problem = SequencingWithReleaseTimesAndDeadlines::new( + vec![3, 2, 4, 1, 2], + vec![0, 1, 5, 0, 8], + vec![5, 6, 10, 3, 12], + ); + // Lehmer code [3,0,0,0,0] = permutation [3,0,1,2,4] + crate::example_db::specs::satisfaction_example(problem, vec![vec![3, 0, 0, 0, 0]]) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index 8a9da4db..a9b01585 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -20,6 +20,7 @@ pub use graph::{ }; pub use misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, - MinimumTardinessSequencing, PaintShop, ShortestCommonSupersequence, SubsetSum, + MinimumTardinessSequencing, PaintShop, SequencingWithReleaseTimesAndDeadlines, + ShortestCommonSupersequence, SubsetSum, }; pub use set::{ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering}; diff --git a/src/unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs b/src/unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs new file mode 100644 index 00000000..f3449da4 --- /dev/null +++ b/src/unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs @@ -0,0 +1,144 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; + +#[test] +fn test_sequencing_rtd_basic() { + let problem = SequencingWithReleaseTimesAndDeadlines::new( + vec![3, 2, 4, 1, 2], + vec![0, 1, 5, 0, 8], + vec![5, 6, 10, 3, 12], + ); + assert_eq!(problem.num_tasks(), 5); + assert_eq!(problem.lengths(), &[3, 2, 4, 1, 2]); + assert_eq!(problem.release_times(), &[0, 1, 5, 0, 8]); + assert_eq!(problem.deadlines(), &[5, 6, 10, 3, 12]); + assert_eq!(problem.time_horizon(), 12); + // Lehmer code dims: [5, 4, 3, 2, 1] + assert_eq!(problem.dims(), vec![5, 4, 3, 2, 1]); + assert_eq!( + ::NAME, + "SequencingWithReleaseTimesAndDeadlines" + ); + assert_eq!( + ::variant(), + vec![] + ); +} + +#[test] +fn test_sequencing_rtd_evaluate_feasible() { + // 5 tasks: schedule order t3, t0, t1, t2, t4 + // t3: start=max(0,0)=0, finish=1 <= 3 ✓ + // t0: start=max(0,1)=1, finish=4 <= 5 ✓ + // t1: start=max(1,4)=4, finish=6 <= 6 ✓ + // t2: start=max(5,6)=6, finish=10 <= 10 ✓ + // t4: start=max(8,10)=10, finish=12 <= 12 ✓ + let problem = SequencingWithReleaseTimesAndDeadlines::new( + vec![3, 2, 4, 1, 2], + vec![0, 1, 5, 0, 8], + vec![5, 6, 10, 3, 12], + ); + // Lehmer code for permutation [3, 0, 1, 2, 4]: + // available=[0,1,2,3,4], pick 3 -> index 3; available=[0,1,2,4], pick 0 -> index 0; + // available=[1,2,4], pick 1 -> index 0; available=[2,4], pick 2 -> index 0; + // available=[4], pick 4 -> index 0 + assert!(problem.evaluate(&[3, 0, 0, 0, 0])); +} + +#[test] +fn test_sequencing_rtd_evaluate_infeasible_deadline() { + let problem = SequencingWithReleaseTimesAndDeadlines::new( + vec![3, 2], + vec![0, 0], + vec![2, 4], // task 0 needs 3 time units but deadline is 2 + ); + // Order [0, 1]: t0 start=0, finish=3 > 2 -> infeasible + assert!(!problem.evaluate(&[0, 0])); + // Order [1, 0]: t1 start=0, finish=2; t0 start=2, finish=5 > 2 -> infeasible + assert!(!problem.evaluate(&[1, 0])); +} + +#[test] +fn test_sequencing_rtd_evaluate_wrong_config_length() { + let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![1, 1], vec![0, 0], vec![2, 2]); + assert!(!problem.evaluate(&[0])); + assert!(!problem.evaluate(&[0, 0, 0])); +} + +#[test] +fn test_sequencing_rtd_empty_instance() { + let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![], vec![], vec![]); + assert_eq!(problem.num_tasks(), 0); + assert_eq!(problem.time_horizon(), 0); + assert_eq!(problem.dims(), Vec::::new()); + assert!(problem.evaluate(&[])); +} + +#[test] +fn test_sequencing_rtd_single_task() { + let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![2], vec![1], vec![5]); + assert_eq!(problem.dims(), vec![1]); + // Only one permutation: task 0 starts at max(1,0)=1, finish=3 <= 5 + assert!(problem.evaluate(&[0])); +} + +#[test] +fn test_sequencing_rtd_brute_force() { + // Small instance: 3 tasks that fit tightly + let problem = + SequencingWithReleaseTimesAndDeadlines::new(vec![1, 2, 1], vec![0, 0, 2], vec![3, 3, 4]); + let solver = BruteForce::new(); + let solution = solver + .find_satisfying(&problem) + .expect("should find a solution"); + assert!(problem.evaluate(&solution)); +} + +#[test] +fn test_sequencing_rtd_brute_force_all() { + let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![1, 1], vec![0, 0], vec![3, 3]); + 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_sequencing_rtd_unsatisfiable() { + // Two tasks each need 2 time units but only 3 total time available + let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![2, 2], vec![0, 0], vec![3, 3]); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_none()); +} + +#[test] +fn test_sequencing_rtd_serialization() { + let problem = + SequencingWithReleaseTimesAndDeadlines::new(vec![3, 2, 4], vec![0, 1, 5], vec![5, 6, 10]); + let json = serde_json::to_value(&problem).unwrap(); + let restored: SequencingWithReleaseTimesAndDeadlines = serde_json::from_value(json).unwrap(); + assert_eq!(restored.lengths(), problem.lengths()); + assert_eq!(restored.release_times(), problem.release_times()); + assert_eq!(restored.deadlines(), problem.deadlines()); +} + +#[test] +fn test_sequencing_rtd_tight_schedule() { + // Tasks that can only be scheduled in one specific order + let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![2, 2], vec![0, 2], vec![2, 4]); + // Order [0, 1]: t0 start=max(0,0)=0, finish=2<=2; t1 start=max(2,2)=2, finish=4<=4 ✓ + assert!(problem.evaluate(&[0, 0])); + // Order [1, 0]: t1 start=max(2,0)=2, finish=4<=4; t0 start=max(0,4)=4, finish=6>2 ✗ + assert!(!problem.evaluate(&[1, 0])); +} + +#[test] +fn test_sequencing_rtd_invalid_lehmer_index() { + let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![1, 1], vec![0, 0], vec![2, 2]); + // config[0]=2 is out of range for available.len()=2 + assert!(!problem.evaluate(&[2, 0])); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index eee5dc64..cba05e0d 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -130,6 +130,10 @@ fn test_all_problems_implement_trait_correctly() { &MinimumTardinessSequencing::new(3, vec![2, 3, 1], vec![(0, 2)]), "MinimumTardinessSequencing", ); + check_problem_trait( + &SequencingWithReleaseTimesAndDeadlines::new(vec![1, 2, 1], vec![0, 0, 2], vec![3, 3, 4]), + "SequencingWithReleaseTimesAndDeadlines", + ); } #[test]