Skip to content

Commit 1bafa02

Browse files
GiggleLiuisPANNclaude
authored
Fix #498: [Model] SequencingToMinimizeWeightedTardiness (#672)
* Add plan for #498: [Model] SequencingToMinimizeWeightedTardiness * Implement #498: [Model] SequencingToMinimizeWeightedTardiness * chore: remove plan file after implementation * Fix merge-with-main issues: update example_db API, fix paper Typst blocks, format - Update canonical_model_example_specs to use new ModelExampleSpec struct fields - Fix duplicate #{ block opener in reductions.typ from merge - Restore missing load-model-example and prefix-sums bindings for CumulativeCost - Run cargo fmt to fix import line wrapping Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix CLI flag mismatch: map schema field 'lengths' to --sizes The auto-generated Parameters section showed --lengths (from the schema field name) while the Example and actual handler used --sizes. Add a field-name override so both are consistent. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Xiwei Pan <xiwei.pan@connect.hkust-gz.edu.cn> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fa40ce7 commit 1bafa02

11 files changed

Lines changed: 591 additions & 9 deletions

File tree

docs/paper/reductions.typ

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@
134134
"MultiprocessorScheduling": [Multiprocessor Scheduling],
135135
"PrecedenceConstrainedScheduling": [Precedence Constrained Scheduling],
136136
"MinimumTardinessSequencing": [Minimum Tardiness Sequencing],
137+
"SequencingToMinimizeWeightedTardiness": [Sequencing to Minimize Weighted Tardiness],
137138
"SequencingToMinimizeMaximumCumulativeCost": [Sequencing to Minimize Maximum Cumulative Cost],
138139
"ConsecutiveOnesSubmatrix": [Consecutive Ones Submatrix],
139140
"SumOfSquaresPartition": [Sum of Squares Partition],
@@ -3518,6 +3519,86 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
35183519
]
35193520
}
35203521

3522+
#{
3523+
let x = load-model-example("SequencingToMinimizeWeightedTardiness")
3524+
let lengths = x.instance.lengths
3525+
let weights = x.instance.weights
3526+
let deadlines = x.instance.deadlines
3527+
let bound = x.instance.bound
3528+
let njobs = lengths.len()
3529+
let lehmer = x.optimal_config
3530+
let schedule = {
3531+
let avail = range(njobs)
3532+
let result = ()
3533+
for c in lehmer {
3534+
result.push(avail.at(c))
3535+
avail = avail.enumerate().filter(((i, v)) => i != c).map(((i, v)) => v)
3536+
}
3537+
result
3538+
}
3539+
let completions = {
3540+
let t = 0
3541+
let result = ()
3542+
for job in schedule {
3543+
t += lengths.at(job)
3544+
result.push(t)
3545+
}
3546+
result
3547+
}
3548+
let tardiness = schedule.enumerate().map(((pos, job)) => calc.max(0, completions.at(pos) - deadlines.at(job)))
3549+
let weighted = schedule.enumerate().map(((pos, job)) => tardiness.at(pos) * weights.at(job))
3550+
let total-weighted = weighted.fold(0, (acc, v) => acc + v)
3551+
let tardy-jobs = schedule.enumerate().filter(((pos, job)) => tardiness.at(pos) > 0).map(((pos, job)) => job)
3552+
[
3553+
#problem-def("SequencingToMinimizeWeightedTardiness")[
3554+
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
3555+
$sum_(j in J) w_j max(0, C_j - d_j)$
3556+
is at most $K$, where $C_j$ is the completion time of job $j$.
3557+
][
3558+
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.
3559+
3560+
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.
3561+
3562+
*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$.
3563+
3564+
#figure(
3565+
canvas(length: 1cm, {
3566+
import draw: *
3567+
let colors = (rgb("#4e79a7"), rgb("#e15759"), rgb("#76b7b2"), rgb("#f28e2b"), rgb("#59a14f"))
3568+
let scale = 0.34
3569+
let row-h = 0.7
3570+
let y = 0
3571+
3572+
for (pos, job) in schedule.enumerate() {
3573+
let start = if pos == 0 { 0 } else { completions.at(pos - 1) }
3574+
let end = completions.at(pos)
3575+
let is-tardy = tardiness.at(pos) > 0
3576+
let fill = colors.at(calc.rem(job, colors.len())).transparentize(if is-tardy { 70% } else { 30% })
3577+
let stroke = colors.at(calc.rem(job, colors.len()))
3578+
rect((start * scale, y - row-h / 2), (end * scale, y + row-h / 2),
3579+
fill: fill, stroke: 0.4pt + stroke)
3580+
content(((start + end) * scale / 2, y), text(7pt, $t_#(job + 1)$))
3581+
3582+
let dl = deadlines.at(job)
3583+
line((dl * scale, y + row-h / 2 + 0.05), (dl * scale, y + row-h / 2 + 0.2),
3584+
stroke: (paint: if is-tardy { red } else { green.darken(20%) }, thickness: 0.6pt))
3585+
}
3586+
3587+
let axis-y = -row-h / 2 - 0.25
3588+
line((0, axis-y), (completions.at(completions.len() - 1) * scale, axis-y), stroke: 0.4pt)
3589+
for t in range(completions.at(completions.len() - 1) + 1) {
3590+
let x = t * scale
3591+
line((x, axis-y), (x, axis-y - 0.08), stroke: 0.4pt)
3592+
content((x, axis-y - 0.22), text(6pt, str(t)))
3593+
}
3594+
content((completions.at(completions.len() - 1) * scale / 2, axis-y - 0.42), text(7pt)[time])
3595+
}),
3596+
caption: [Single-machine schedule for the canonical weighted-tardiness example. The faded job is tardy; colored ticks mark the individual deadlines $d_j$.],
3597+
) <fig:weighted-tardiness>
3598+
]
3599+
]
3600+
}
3601+
35213602
#{
35223603
let x = load-model-example("SequencingToMinimizeMaximumCumulativeCost")
35233604
let costs = x.instance.costs

docs/paper/references.bib

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,38 @@ @article{brucker1977
8181
doi = {10.1016/S0167-5060(08)70743-X}
8282
}
8383

84+
@article{lawler1977,
85+
author = {Eugene L. Lawler},
86+
title = {A pseudopolynomial algorithm for sequencing jobs to minimize total tardiness},
87+
journal = {Annals of Discrete Mathematics},
88+
volume = {1},
89+
pages = {331--342},
90+
year = {1977},
91+
doi = {10.1016/S0167-5060(08)70742-8}
92+
}
93+
94+
@article{potts1985,
95+
author = {Chris N. Potts and Luk N. Van Wassenhove},
96+
title = {A Branch and Bound Algorithm for the Total Weighted Tardiness Problem},
97+
journal = {Operations Research},
98+
volume = {33},
99+
number = {2},
100+
pages = {363--377},
101+
year = {1985},
102+
doi = {10.1287/opre.33.2.363}
103+
}
104+
105+
@article{tanaka2009,
106+
author = {Shunji Tanaka and Shuji Fujikuma and Mituhiko Araki},
107+
title = {An exact algorithm for single-machine scheduling without machine idle time},
108+
journal = {Journal of Scheduling},
109+
volume = {12},
110+
number = {6},
111+
pages = {575--593},
112+
year = {2009},
113+
doi = {10.1007/s10951-008-0093-5}
114+
}
115+
84116
@inproceedings{karp1972,
85117
author = {Richard M. Karp},
86118
title = {Reducibility among Combinatorial Problems},

problemreductions-cli/src/cli.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ Flags by problem type:
271271
StaffScheduling --schedules, --requirements, --num-workers, --k
272272
MinimumTardinessSequencing --n, --deadlines [--precedence-pairs]
273273
SequencingToMinimizeMaximumCumulativeCost --costs, --bound [--precedence-pairs]
274+
SequencingToMinimizeWeightedTardiness --sizes, --weights, --deadlines, --bound
274275
RectilinearPictureCompression --matrix (0/1), --k
275276
SCS --strings, --bound [--alphabet-size]
276277
StringToStringCorrection --source-string, --target-string, --bound [--alphabet-size]

problemreductions-cli/src/commands/create.rs

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ use problemreductions::models::misc::{
1818
FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing,
1919
MultiprocessorScheduling, PaintShop, PartiallyOrderedKnapsack, QueryArg,
2020
RectilinearPictureCompression, ResourceConstrainedScheduling,
21-
SequencingToMinimizeMaximumCumulativeCost, SequencingWithReleaseTimesAndDeadlines,
22-
SequencingWithinIntervals, ShortestCommonSupersequence, StringToStringCorrection, SubsetSum,
23-
SumOfSquaresPartition,
21+
SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedTardiness,
22+
SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence,
23+
StringToStringCorrection, SubsetSum, SumOfSquaresPartition,
2424
};
2525
use problemreductions::models::BiconnectivityAugmentation;
2626
use problemreductions::prelude::*;
@@ -421,6 +421,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
421421
"RectilinearPictureCompression" => {
422422
"--matrix \"1,1,0,0;1,1,0,0;0,0,1,1;0,0,1,1\" --k 2"
423423
}
424+
"SequencingToMinimizeWeightedTardiness" => {
425+
"--sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13"
426+
}
424427
"SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11",
425428
"BoyceCoddNormalFormViolation" => {
426429
"--n 6 --sets \"0,1:2;2:3;3,4:5\" --target 0,1,2,3,4,5"
@@ -493,6 +496,7 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String {
493496
"num_tasks" => "n".to_string(),
494497
"precedences" => "precedence-pairs".to_string(),
495498
"threshold" => "bound".to_string(),
499+
"lengths" => "sizes".to_string(),
496500
_ => field_name.replace('_', "-"),
497501
}
498502
}
@@ -2022,6 +2026,62 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
20222026
)
20232027
}
20242028

2029+
// SequencingToMinimizeWeightedTardiness
2030+
"SequencingToMinimizeWeightedTardiness" => {
2031+
let sizes_str = args.sizes.as_deref().ok_or_else(|| {
2032+
anyhow::anyhow!(
2033+
"SequencingToMinimizeWeightedTardiness requires --sizes, --weights, --deadlines, and --bound\n\n\
2034+
Usage: pred create SequencingToMinimizeWeightedTardiness --sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13"
2035+
)
2036+
})?;
2037+
let weights_str = args.weights.as_deref().ok_or_else(|| {
2038+
anyhow::anyhow!(
2039+
"SequencingToMinimizeWeightedTardiness requires --weights (comma-separated tardiness weights)\n\n\
2040+
Usage: pred create SequencingToMinimizeWeightedTardiness --sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13"
2041+
)
2042+
})?;
2043+
let deadlines_str = args.deadlines.as_deref().ok_or_else(|| {
2044+
anyhow::anyhow!(
2045+
"SequencingToMinimizeWeightedTardiness requires --deadlines (comma-separated job deadlines)\n\n\
2046+
Usage: pred create SequencingToMinimizeWeightedTardiness --sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13"
2047+
)
2048+
})?;
2049+
let bound = args.bound.ok_or_else(|| {
2050+
anyhow::anyhow!(
2051+
"SequencingToMinimizeWeightedTardiness requires --bound\n\n\
2052+
Usage: pred create SequencingToMinimizeWeightedTardiness --sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13"
2053+
)
2054+
})?;
2055+
anyhow::ensure!(bound >= 0, "--bound must be non-negative");
2056+
2057+
let lengths: Vec<u64> = util::parse_comma_list(sizes_str)?;
2058+
let weights: Vec<u64> = util::parse_comma_list(weights_str)?;
2059+
let deadlines: Vec<u64> = util::parse_comma_list(deadlines_str)?;
2060+
2061+
anyhow::ensure!(
2062+
lengths.len() == weights.len(),
2063+
"sizes length ({}) must equal weights length ({})",
2064+
lengths.len(),
2065+
weights.len()
2066+
);
2067+
anyhow::ensure!(
2068+
lengths.len() == deadlines.len(),
2069+
"sizes length ({}) must equal deadlines length ({})",
2070+
lengths.len(),
2071+
deadlines.len()
2072+
);
2073+
2074+
(
2075+
ser(SequencingToMinimizeWeightedTardiness::new(
2076+
lengths,
2077+
weights,
2078+
deadlines,
2079+
bound as u64,
2080+
))?,
2081+
resolved_variant.clone(),
2082+
)
2083+
}
2084+
20252085
// SequencingToMinimizeMaximumCumulativeCost
20262086
"SequencingToMinimizeMaximumCumulativeCost" => {
20272087
let costs_str = args.costs.as_deref().ok_or_else(|| {

problemreductions-cli/tests/cli_tests.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1402,6 +1402,72 @@ fn test_create_set_basis_rejects_out_of_range_elements() {
14021402
assert!(!stderr.contains("panicked at"), "stderr: {stderr}");
14031403
}
14041404

1405+
#[test]
1406+
fn test_create_sequencing_to_minimize_weighted_tardiness() {
1407+
let output_file =
1408+
std::env::temp_dir().join("pred_test_create_weighted_tardiness_sequencing.json");
1409+
let output = pred()
1410+
.args([
1411+
"-o",
1412+
output_file.to_str().unwrap(),
1413+
"create",
1414+
"SequencingToMinimizeWeightedTardiness",
1415+
"--sizes",
1416+
"3,4,2,5,3",
1417+
"--weights",
1418+
"2,3,1,4,2",
1419+
"--deadlines",
1420+
"5,8,4,15,10",
1421+
"--bound",
1422+
"13",
1423+
])
1424+
.output()
1425+
.unwrap();
1426+
assert!(
1427+
output.status.success(),
1428+
"stderr: {}",
1429+
String::from_utf8_lossy(&output.stderr)
1430+
);
1431+
1432+
let content = std::fs::read_to_string(&output_file).unwrap();
1433+
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
1434+
assert_eq!(json["type"], "SequencingToMinimizeWeightedTardiness");
1435+
assert_eq!(json["data"]["lengths"], serde_json::json!([3, 4, 2, 5, 3]));
1436+
assert_eq!(json["data"]["weights"], serde_json::json!([2, 3, 1, 4, 2]));
1437+
assert_eq!(
1438+
json["data"]["deadlines"],
1439+
serde_json::json!([5, 8, 4, 15, 10])
1440+
);
1441+
assert_eq!(json["data"]["bound"], 13);
1442+
1443+
std::fs::remove_file(&output_file).ok();
1444+
}
1445+
1446+
#[test]
1447+
fn test_create_sequencing_to_minimize_weighted_tardiness_rejects_mismatched_lengths() {
1448+
let output = pred()
1449+
.args([
1450+
"create",
1451+
"SequencingToMinimizeWeightedTardiness",
1452+
"--sizes",
1453+
"3,4,2",
1454+
"--weights",
1455+
"2,3",
1456+
"--deadlines",
1457+
"5,8,4",
1458+
"--bound",
1459+
"13",
1460+
])
1461+
.output()
1462+
.unwrap();
1463+
assert!(!output.status.success());
1464+
let stderr = String::from_utf8_lossy(&output.stderr);
1465+
assert!(
1466+
stderr.contains("sizes length (3) must equal weights length (2)"),
1467+
"stderr: {stderr}"
1468+
);
1469+
}
1470+
14051471
#[test]
14061472
fn test_create_sum_of_squares_partition_rejects_negative_bound_without_panicking() {
14071473
let output = pred()
@@ -2856,6 +2922,21 @@ fn test_create_no_flags_shows_help() {
28562922
);
28572923
}
28582924

2925+
#[test]
2926+
fn test_create_sequencing_to_minimize_weighted_tardiness_no_flags_shows_help() {
2927+
let output = pred()
2928+
.args(["create", "SequencingToMinimizeWeightedTardiness"])
2929+
.output()
2930+
.unwrap();
2931+
assert!(!output.status.success());
2932+
let stderr = String::from_utf8_lossy(&output.stderr);
2933+
assert!(stderr.contains("--sizes"));
2934+
assert!(stderr.contains("--weights"));
2935+
assert!(stderr.contains("--deadlines"));
2936+
assert!(stderr.contains("--bound"));
2937+
assert!(stderr.contains("pred create SequencingToMinimizeWeightedTardiness"));
2938+
}
2939+
28592940
#[test]
28602941
fn test_create_multiple_choice_branching_help_uses_bound_flag() {
28612942
let output = pred()

src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ pub mod prelude {
6565
Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling,
6666
PaintShop, Partition, QueryArg, RectilinearPictureCompression,
6767
ResourceConstrainedScheduling, SequencingToMinimizeMaximumCumulativeCost,
68-
SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals,
69-
ShortestCommonSupersequence, StaffScheduling, StringToStringCorrection, SubsetSum,
70-
SumOfSquaresPartition, Term,
68+
SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines,
69+
SequencingWithinIntervals, ShortestCommonSupersequence, StaffScheduling,
70+
StringToStringCorrection, SubsetSum, SumOfSquaresPartition, Term,
7171
};
7272
pub use crate::models::set::{
7373
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking,

src/models/misc/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
//! - [`RectilinearPictureCompression`]: Cover 1-entries with bounded rectangles
2020
//! - [`ResourceConstrainedScheduling`]: Schedule unit-length tasks on processors with resource constraints
2121
//! - [`SequencingToMinimizeMaximumCumulativeCost`]: Keep every cumulative schedule cost prefix under a bound
22+
//! - [`SequencingToMinimizeWeightedTardiness`]: Decide whether a schedule meets a weighted tardiness bound
2223
//! - [`SequencingWithReleaseTimesAndDeadlines`]: Single-machine scheduling feasibility
2324
//! - [`SequencingWithinIntervals`]: Schedule tasks within time windows
2425
//! - [`ShortestCommonSupersequence`]: Find a common supersequence of bounded length
@@ -44,6 +45,7 @@ mod precedence_constrained_scheduling;
4445
mod rectilinear_picture_compression;
4546
pub(crate) mod resource_constrained_scheduling;
4647
mod sequencing_to_minimize_maximum_cumulative_cost;
48+
mod sequencing_to_minimize_weighted_tardiness;
4749
mod sequencing_with_release_times_and_deadlines;
4850
mod sequencing_within_intervals;
4951
pub(crate) mod shortest_common_supersequence;
@@ -70,6 +72,7 @@ pub use precedence_constrained_scheduling::PrecedenceConstrainedScheduling;
7072
pub use rectilinear_picture_compression::RectilinearPictureCompression;
7173
pub use resource_constrained_scheduling::ResourceConstrainedScheduling;
7274
pub use sequencing_to_minimize_maximum_cumulative_cost::SequencingToMinimizeMaximumCumulativeCost;
75+
pub use sequencing_to_minimize_weighted_tardiness::SequencingToMinimizeWeightedTardiness;
7376
pub use sequencing_with_release_times_and_deadlines::SequencingWithReleaseTimesAndDeadlines;
7477
pub use sequencing_within_intervals::SequencingWithinIntervals;
7578
pub use shortest_common_supersequence::ShortestCommonSupersequence;
@@ -97,6 +100,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
97100
specs.extend(partially_ordered_knapsack::canonical_model_example_specs());
98101
specs.extend(string_to_string_correction::canonical_model_example_specs());
99102
specs.extend(minimum_tardiness_sequencing::canonical_model_example_specs());
103+
specs.extend(sequencing_to_minimize_weighted_tardiness::canonical_model_example_specs());
100104
specs.extend(additional_key::canonical_model_example_specs());
101105
specs.extend(sequencing_to_minimize_maximum_cumulative_cost::canonical_model_example_specs());
102106
specs.extend(sum_of_squares_partition::canonical_model_example_specs());

0 commit comments

Comments
 (0)