Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
"MultiprocessorScheduling": [Multiprocessor Scheduling],
"PrecedenceConstrainedScheduling": [Precedence Constrained Scheduling],
"MinimumTardinessSequencing": [Minimum Tardiness Sequencing],
"SequencingToMinimizeWeightedTardiness": [Sequencing to Minimize Weighted Tardiness],
"SequencingToMinimizeMaximumCumulativeCost": [Sequencing to Minimize Maximum Cumulative Cost],
"ConsecutiveOnesSubmatrix": [Consecutive Ones Submatrix],
"SumOfSquaresPartition": [Sum of Squares Partition],
Expand Down Expand Up @@ -3518,6 +3519,86 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
]
}

#{
let x = load-model-example("SequencingToMinimizeWeightedTardiness")
let lengths = x.instance.lengths
let weights = x.instance.weights
let deadlines = x.instance.deadlines
let bound = x.instance.bound
let njobs = lengths.len()
let lehmer = x.optimal_config
let schedule = {
let avail = range(njobs)
let result = ()
for c in lehmer {
result.push(avail.at(c))
avail = avail.enumerate().filter(((i, v)) => i != c).map(((i, v)) => v)
}
result
}
let completions = {
let t = 0
let result = ()
for job in schedule {
t += lengths.at(job)
result.push(t)
}
result
}
let tardiness = schedule.enumerate().map(((pos, job)) => calc.max(0, completions.at(pos) - deadlines.at(job)))
let weighted = schedule.enumerate().map(((pos, job)) => tardiness.at(pos) * weights.at(job))
let total-weighted = weighted.fold(0, (acc, v) => acc + v)
let tardy-jobs = schedule.enumerate().filter(((pos, job)) => tardiness.at(pos) > 0).map(((pos, job)) => job)
[
#problem-def("SequencingToMinimizeWeightedTardiness")[
Given a set $J$ of $n$ jobs, processing times $ell_j in ZZ^+$, tardiness weights $w_j in ZZ^+$, deadlines $d_j in ZZ^+$, and a bound $K in ZZ^+$, determine whether there exists a one-machine schedule whose total weighted tardiness
$sum_(j in J) w_j max(0, C_j - d_j)$
is at most $K$, where $C_j$ is the completion time of job $j$.
][
Sequencing to Minimize Weighted Tardiness is the classical single-machine scheduling problem $1 || sum w_j T_j$, where $T_j = max(0, C_j - d_j)$. It appears as SS5 in Garey & Johnson @garey1979 and is strongly NP-complete via transformation from 3-Partition, which rules out pseudo-polynomial algorithms in general. When all weights are equal, the special case reduces to ordinary total tardiness and admits a pseudo-polynomial dynamic program @lawler1977. Garey & Johnson also note that the equal-length case is polynomial-time solvable by bipartite matching @garey1979.

Exact algorithms remain exponential in the worst case. Brute-force over all $n!$ schedules evaluates the implementation's decision encoding in $O(n! dot n)$ time. More refined exact methods include the branch-and-bound algorithm of Potts and Van Wassenhove @potts1985 and the dynamic-programming style exact algorithm of Tanaka, Fujikuma, and Araki @tanaka2009.

*Example.* Consider the five jobs with processing times $ell = (#lengths.map(v => str(v)).join(", "))$, weights $w = (#weights.map(v => str(v)).join(", "))$, deadlines $d = (#deadlines.map(v => str(v)).join(", "))$, and bound $K = #bound$. The unique satisfying schedule is $(#schedule.map(job => $t_#(job + 1)$).join(", "))$, with completion times $(#completions.map(v => str(v)).join(", "))$. Only job $t_#(tardy-jobs.at(0) + 1)$ is tardy; the per-job weighted tardiness contributions are $(#weighted.map(v => str(v)).join(", "))$, so the total weighted tardiness is $#total-weighted <= K$.

#figure(
canvas(length: 1cm, {
import draw: *
let colors = (rgb("#4e79a7"), rgb("#e15759"), rgb("#76b7b2"), rgb("#f28e2b"), rgb("#59a14f"))
let scale = 0.34
let row-h = 0.7
let y = 0

for (pos, job) in schedule.enumerate() {
let start = if pos == 0 { 0 } else { completions.at(pos - 1) }
let end = completions.at(pos)
let is-tardy = tardiness.at(pos) > 0
let fill = colors.at(calc.rem(job, colors.len())).transparentize(if is-tardy { 70% } else { 30% })
let stroke = colors.at(calc.rem(job, colors.len()))
rect((start * scale, y - row-h / 2), (end * scale, y + row-h / 2),
fill: fill, stroke: 0.4pt + stroke)
content(((start + end) * scale / 2, y), text(7pt, $t_#(job + 1)$))

let dl = deadlines.at(job)
line((dl * scale, y + row-h / 2 + 0.05), (dl * scale, y + row-h / 2 + 0.2),
stroke: (paint: if is-tardy { red } else { green.darken(20%) }, thickness: 0.6pt))
}

let axis-y = -row-h / 2 - 0.25
line((0, axis-y), (completions.at(completions.len() - 1) * scale, axis-y), stroke: 0.4pt)
for t in range(completions.at(completions.len() - 1) + 1) {
let x = t * scale
line((x, axis-y), (x, axis-y - 0.08), stroke: 0.4pt)
content((x, axis-y - 0.22), text(6pt, str(t)))
}
content((completions.at(completions.len() - 1) * scale / 2, axis-y - 0.42), text(7pt)[time])
}),
caption: [Single-machine schedule for the canonical weighted-tardiness example. The faded job is tardy; colored ticks mark the individual deadlines $d_j$.],
) <fig:weighted-tardiness>
]
]
}

#{
let x = load-model-example("SequencingToMinimizeMaximumCumulativeCost")
let costs = x.instance.costs
Expand Down
32 changes: 32 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,38 @@ @article{brucker1977
doi = {10.1016/S0167-5060(08)70743-X}
}

@article{lawler1977,
author = {Eugene L. Lawler},
title = {A pseudopolynomial algorithm for sequencing jobs to minimize total tardiness},
journal = {Annals of Discrete Mathematics},
volume = {1},
pages = {331--342},
year = {1977},
doi = {10.1016/S0167-5060(08)70742-8}
}

@article{potts1985,
author = {Chris N. Potts and Luk N. Van Wassenhove},
title = {A Branch and Bound Algorithm for the Total Weighted Tardiness Problem},
journal = {Operations Research},
volume = {33},
number = {2},
pages = {363--377},
year = {1985},
doi = {10.1287/opre.33.2.363}
}

@article{tanaka2009,
author = {Shunji Tanaka and Shuji Fujikuma and Mituhiko Araki},
title = {An exact algorithm for single-machine scheduling without machine idle time},
journal = {Journal of Scheduling},
volume = {12},
number = {6},
pages = {575--593},
year = {2009},
doi = {10.1007/s10951-008-0093-5}
}

@inproceedings{karp1972,
author = {Richard M. Karp},
title = {Reducibility among Combinatorial Problems},
Expand Down
1 change: 1 addition & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ Flags by problem type:
StaffScheduling --schedules, --requirements, --num-workers, --k
MinimumTardinessSequencing --n, --deadlines [--precedence-pairs]
SequencingToMinimizeMaximumCumulativeCost --costs, --bound [--precedence-pairs]
SequencingToMinimizeWeightedTardiness --sizes, --weights, --deadlines, --bound
RectilinearPictureCompression --matrix (0/1), --k
SCS --strings, --bound [--alphabet-size]
StringToStringCorrection --source-string, --target-string, --bound [--alphabet-size]
Expand Down
66 changes: 63 additions & 3 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ use problemreductions::models::misc::{
FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing,
MultiprocessorScheduling, PaintShop, PartiallyOrderedKnapsack, QueryArg,
RectilinearPictureCompression, ResourceConstrainedScheduling,
SequencingToMinimizeMaximumCumulativeCost, SequencingWithReleaseTimesAndDeadlines,
SequencingWithinIntervals, ShortestCommonSupersequence, StringToStringCorrection, SubsetSum,
SumOfSquaresPartition,
SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedTardiness,
SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence,
StringToStringCorrection, SubsetSum, SumOfSquaresPartition,
};
use problemreductions::models::BiconnectivityAugmentation;
use problemreductions::prelude::*;
Expand Down Expand Up @@ -421,6 +421,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"RectilinearPictureCompression" => {
"--matrix \"1,1,0,0;1,1,0,0;0,0,1,1;0,0,1,1\" --k 2"
}
"SequencingToMinimizeWeightedTardiness" => {
"--sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13"
}
"SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11",
"BoyceCoddNormalFormViolation" => {
"--n 6 --sets \"0,1:2;2:3;3,4:5\" --target 0,1,2,3,4,5"
Expand Down Expand Up @@ -493,6 +496,7 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String {
"num_tasks" => "n".to_string(),
"precedences" => "precedence-pairs".to_string(),
"threshold" => "bound".to_string(),
"lengths" => "sizes".to_string(),
_ => field_name.replace('_', "-"),
}
}
Expand Down Expand Up @@ -2022,6 +2026,62 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// SequencingToMinimizeWeightedTardiness
"SequencingToMinimizeWeightedTardiness" => {
let sizes_str = args.sizes.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"SequencingToMinimizeWeightedTardiness requires --sizes, --weights, --deadlines, and --bound\n\n\
Usage: pred create SequencingToMinimizeWeightedTardiness --sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13"
)
Comment on lines +2029 to +2035
})?;
let weights_str = args.weights.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"SequencingToMinimizeWeightedTardiness requires --weights (comma-separated tardiness weights)\n\n\
Usage: pred create SequencingToMinimizeWeightedTardiness --sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13"
)
})?;
let deadlines_str = args.deadlines.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"SequencingToMinimizeWeightedTardiness requires --deadlines (comma-separated job deadlines)\n\n\
Usage: pred create SequencingToMinimizeWeightedTardiness --sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13"
)
})?;
let bound = args.bound.ok_or_else(|| {
anyhow::anyhow!(
"SequencingToMinimizeWeightedTardiness requires --bound\n\n\
Usage: pred create SequencingToMinimizeWeightedTardiness --sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13"
)
})?;
anyhow::ensure!(bound >= 0, "--bound must be non-negative");

let lengths: Vec<u64> = util::parse_comma_list(sizes_str)?;
let weights: Vec<u64> = util::parse_comma_list(weights_str)?;
let deadlines: Vec<u64> = util::parse_comma_list(deadlines_str)?;

anyhow::ensure!(
lengths.len() == weights.len(),
"sizes length ({}) must equal weights length ({})",
lengths.len(),
weights.len()
);
anyhow::ensure!(
lengths.len() == deadlines.len(),
"sizes length ({}) must equal deadlines length ({})",
lengths.len(),
deadlines.len()
);

(
ser(SequencingToMinimizeWeightedTardiness::new(
lengths,
weights,
deadlines,
bound as u64,
))?,
resolved_variant.clone(),
)
}

// SequencingToMinimizeMaximumCumulativeCost
"SequencingToMinimizeMaximumCumulativeCost" => {
let costs_str = args.costs.as_deref().ok_or_else(|| {
Expand Down
81 changes: 81 additions & 0 deletions problemreductions-cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1402,6 +1402,72 @@ fn test_create_set_basis_rejects_out_of_range_elements() {
assert!(!stderr.contains("panicked at"), "stderr: {stderr}");
}

#[test]
fn test_create_sequencing_to_minimize_weighted_tardiness() {
let output_file =
std::env::temp_dir().join("pred_test_create_weighted_tardiness_sequencing.json");
let output = pred()
.args([
"-o",
output_file.to_str().unwrap(),
"create",
"SequencingToMinimizeWeightedTardiness",
"--sizes",
"3,4,2,5,3",
"--weights",
"2,3,1,4,2",
"--deadlines",
"5,8,4,15,10",
"--bound",
"13",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);

let content = std::fs::read_to_string(&output_file).unwrap();
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(json["type"], "SequencingToMinimizeWeightedTardiness");
assert_eq!(json["data"]["lengths"], serde_json::json!([3, 4, 2, 5, 3]));
assert_eq!(json["data"]["weights"], serde_json::json!([2, 3, 1, 4, 2]));
assert_eq!(
json["data"]["deadlines"],
serde_json::json!([5, 8, 4, 15, 10])
);
assert_eq!(json["data"]["bound"], 13);

std::fs::remove_file(&output_file).ok();
}

#[test]
fn test_create_sequencing_to_minimize_weighted_tardiness_rejects_mismatched_lengths() {
let output = pred()
.args([
"create",
"SequencingToMinimizeWeightedTardiness",
"--sizes",
"3,4,2",
"--weights",
"2,3",
"--deadlines",
"5,8,4",
"--bound",
"13",
])
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("sizes length (3) must equal weights length (2)"),
"stderr: {stderr}"
);
}

#[test]
fn test_create_sum_of_squares_partition_rejects_negative_bound_without_panicking() {
let output = pred()
Expand Down Expand Up @@ -2856,6 +2922,21 @@ fn test_create_no_flags_shows_help() {
);
}

#[test]
fn test_create_sequencing_to_minimize_weighted_tardiness_no_flags_shows_help() {
let output = pred()
.args(["create", "SequencingToMinimizeWeightedTardiness"])
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("--sizes"));
assert!(stderr.contains("--weights"));
assert!(stderr.contains("--deadlines"));
assert!(stderr.contains("--bound"));
assert!(stderr.contains("pred create SequencingToMinimizeWeightedTardiness"));
}

#[test]
fn test_create_multiple_choice_branching_help_uses_bound_flag() {
let output = pred()
Expand Down
6 changes: 3 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ pub mod prelude {
Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling,
PaintShop, Partition, QueryArg, RectilinearPictureCompression,
ResourceConstrainedScheduling, SequencingToMinimizeMaximumCumulativeCost,
SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals,
ShortestCommonSupersequence, StaffScheduling, StringToStringCorrection, SubsetSum,
SumOfSquaresPartition, Term,
SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines,
SequencingWithinIntervals, ShortestCommonSupersequence, StaffScheduling,
StringToStringCorrection, SubsetSum, SumOfSquaresPartition, Term,
};
pub use crate::models::set::{
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking,
Expand Down
4 changes: 4 additions & 0 deletions src/models/misc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
//! - [`RectilinearPictureCompression`]: Cover 1-entries with bounded rectangles
//! - [`ResourceConstrainedScheduling`]: Schedule unit-length tasks on processors with resource constraints
//! - [`SequencingToMinimizeMaximumCumulativeCost`]: Keep every cumulative schedule cost prefix under a bound
//! - [`SequencingToMinimizeWeightedTardiness`]: Decide whether a schedule meets a weighted tardiness bound
//! - [`SequencingWithReleaseTimesAndDeadlines`]: Single-machine scheduling feasibility
//! - [`SequencingWithinIntervals`]: Schedule tasks within time windows
//! - [`ShortestCommonSupersequence`]: Find a common supersequence of bounded length
Expand All @@ -44,6 +45,7 @@ mod precedence_constrained_scheduling;
mod rectilinear_picture_compression;
pub(crate) mod resource_constrained_scheduling;
mod sequencing_to_minimize_maximum_cumulative_cost;
mod sequencing_to_minimize_weighted_tardiness;
mod sequencing_with_release_times_and_deadlines;
mod sequencing_within_intervals;
pub(crate) mod shortest_common_supersequence;
Expand All @@ -70,6 +72,7 @@ pub use precedence_constrained_scheduling::PrecedenceConstrainedScheduling;
pub use rectilinear_picture_compression::RectilinearPictureCompression;
pub use resource_constrained_scheduling::ResourceConstrainedScheduling;
pub use sequencing_to_minimize_maximum_cumulative_cost::SequencingToMinimizeMaximumCumulativeCost;
pub use sequencing_to_minimize_weighted_tardiness::SequencingToMinimizeWeightedTardiness;
pub use sequencing_with_release_times_and_deadlines::SequencingWithReleaseTimesAndDeadlines;
pub use sequencing_within_intervals::SequencingWithinIntervals;
pub use shortest_common_supersequence::ShortestCommonSupersequence;
Expand Down Expand Up @@ -97,6 +100,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
specs.extend(partially_ordered_knapsack::canonical_model_example_specs());
specs.extend(string_to_string_correction::canonical_model_example_specs());
specs.extend(minimum_tardiness_sequencing::canonical_model_example_specs());
specs.extend(sequencing_to_minimize_weighted_tardiness::canonical_model_example_specs());
specs.extend(additional_key::canonical_model_example_specs());
specs.extend(sequencing_to_minimize_maximum_cumulative_cost::canonical_model_example_specs());
specs.extend(sum_of_squares_partition::canonical_model_example_specs());
Expand Down
Loading
Loading