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
89 changes: 89 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
"ConsecutiveBlockMinimization": [Consecutive Block Minimization],
"ConsecutiveOnesSubmatrix": [Consecutive Ones Submatrix],
"SequencingToMinimizeMaximumCumulativeCost": [Sequencing to Minimize Maximum Cumulative Cost],
"SequencingToMinimizeWeightedCompletionTime": [Sequencing to Minimize Weighted Completion Time],
"SequencingToMinimizeWeightedTardiness": [Sequencing to Minimize Weighted Tardiness],
"SequencingWithinIntervals": [Sequencing Within Intervals],
"SumOfSquaresPartition": [Sum of Squares Partition],
Expand Down Expand Up @@ -3716,6 +3717,72 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
]
}

#{
let x = load-model-example("SequencingToMinimizeWeightedCompletionTime")
let lengths = x.instance.lengths
let weights = x.instance.weights
let precs = x.instance.precedences
let ntasks = lengths.len()
let sol = x.optimal.at(0)
let opt = sol.metric.Valid
let lehmer = sol.config
let schedule = {
let avail = range(ntasks)
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 starts = ()
let finishes = ()
let elapsed = 0
for task in schedule {
starts.push(elapsed)
elapsed += lengths.at(task)
finishes.push(elapsed)
}
let total-time = elapsed
[
#problem-def("SequencingToMinimizeWeightedCompletionTime")[
Given a set $T$ of $n$ tasks, a processing-time function $l: T -> ZZ^+$, a weight function $w: T -> ZZ^+$, and a partial order $prec.eq$ on $T$, find a one-machine schedule minimizing $sum_(t in T) w(t) C(t)$, where $C(t)$ is the completion time of task $t$ and every precedence relation $t_i prec.eq t_j$ requires task $t_i$ to complete before task $t_j$ starts.
][
Sequencing to Minimize Weighted Completion Time is the single-machine precedence-constrained scheduling problem catalogued as SS4 in Garey & Johnson @garey1979, usually written $1 | "prec" | sum w_j C_j$. Lawler showed that arbitrary precedence constraints make the problem NP-complete, while series-parallel precedence orders admit an $O(n log n)$ algorithm @lawler1978. Without precedence constraints, Smith's ratio rule orders jobs by non-increasing $w_j / l_j$ and is optimal @smith1956.

*Example.* Consider tasks with lengths $l = (#lengths.map(v => str(v)).join(", "))$, weights $w = (#weights.map(v => str(v)).join(", "))$, and precedence constraints #{precs.map(p => [$t_#(p.at(0)) prec.eq t_#(p.at(1))$]).join(", ")}. An optimal schedule is $(#schedule.map(t => $t_#t$).join(", "))$, with completion times $(#finishes.map(v => str(v)).join(", "))$ along the machine timeline and objective value $#opt$.

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

for (pos, task) in schedule.enumerate() {
let x0 = starts.at(pos) * scale
let x1 = finishes.at(pos) * scale
let color = colors.at(calc.rem(task, colors.len()))
rect((x0, -row-h / 2), (x1, row-h / 2),
fill: color.transparentize(30%), stroke: 0.4pt + color)
content(((x0 + x1) / 2, 0), text(7pt, $t_#task$))
}

let y-axis = -row-h / 2 - 0.22
line((0, y-axis), (total-time * scale, y-axis), stroke: 0.4pt)
for t in range(total-time + 1) {
let x = t * scale
line((x, y-axis), (x, y-axis - 0.08), stroke: 0.4pt)
content((x, y-axis - 0.22), text(6pt, str(t)))
}
content((total-time * scale / 2, y-axis - 0.45), text(7pt)[time])
}),
caption: [Optimal single-machine schedule for the canonical weighted-completion-time instance. Each block width equals the processing time $l_j$.],
) <fig:stmwct>
]
]
}

#{
let x = load-model-example("SequencingToMinimizeWeightedTardiness")
let lengths = x.instance.lengths
Expand Down Expand Up @@ -4898,6 +4965,28 @@ The following reductions to Integer Linear Programming are straightforward formu
_Solution extraction._ For each item $i$, find the unique $j$ with $x_(i j) = 1$; assign item $i$ to bin $j$.
]

#reduction-rule("SequencingToMinimizeWeightedCompletionTime", "ILP")[
Completion times are natural integer variables, precedence constraints compare those completion times directly, and one binary order variable per task pair enforces that a single machine cannot overlap two jobs.
][
_Construction._ For each task $j$, introduce an integer completion-time variable $C_j$. For each unordered pair $i < j$, introduce a binary order variable $y_(i j)$ with $y_(i j) = 1$ meaning task $i$ finishes before task $j$. Let $M = sum_h l_h$.

_Bounds._ $l_j <= C_j <= M$ for every task $j$, and $y_(i j) in {0, 1}$.

_Precedence constraints._ If $i prec.eq j$, require $C_j - C_i >= l_j$.

_Single-machine disjunction._ For every pair $i < j$, require
$C_j - C_i + M (1 - y_(i j)) >= l_j$
and
$C_i - C_j + M y_(i j) >= l_i$.
Exactly one of the two orderings is therefore active.

_Objective._ Minimize $sum_j w_j C_j$.

_Correctness._ ($arrow.r.double$) Any feasible schedule defines completion times and pairwise order values satisfying the bounds, precedence inequalities, and disjunctive machine constraints; its weighted completion time is exactly the ILP objective. ($arrow.l.double$) Any feasible ILP solution assigns a strict order to every task pair and forbids overlap, so the completion times correspond to a valid single-machine schedule that respects all precedences. Minimizing the ILP objective therefore minimizes the original weighted completion-time objective.

_Solution extraction._ Sort tasks by their completion times $C_j$ and encode that order back into the source schedule representation.
]

#reduction-rule("TravelingSalesman", "ILP",
example: true,
example-caption: [Weighted $K_4$: the optimal tour $0 arrow 1 arrow 3 arrow 2 arrow 0$ with cost 80 is found by position-based ILP.],
Expand Down
21 changes: 21 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,27 @@ @article{moore1968
doi = {10.1287/mnsc.15.1.102}
}

@article{lawler1978,
author = {Eugene L. Lawler},
title = {Sequencing Jobs to Minimize Total Weighted Completion Time Subject to Precedence Constraints},
journal = {Annals of Discrete Mathematics},
volume = {2},
pages = {75--90},
year = {1978},
doi = {10.1016/S0167-5060(08)70356-7}
}

@article{smith1956,
author = {W. E. Smith},
title = {Various Optimizers for Single-Stage Production},
journal = {Naval Research Logistics Quarterly},
volume = {3},
number = {1--2},
pages = {59--66},
year = {1956},
doi = {10.1002/nav.3800030106}
}

@article{johnson1954,
author = {Selmer M. Johnson},
title = {Optimal two- and three-stage production schedules with setup times included},
Expand Down
1 change: 1 addition & 0 deletions docs/src/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ pred create MinimumCardinalityKey --num-attributes 6 --dependencies "0,1>2;0,2>3
pred create MinimumTardinessSequencing --n 5 --deadlines 5,5,5,3,3 --precedence-pairs "0>3,1>3,1>4,2>4" -o mts.json
pred create SchedulingWithIndividualDeadlines --n 7 --deadlines 2,1,2,2,3,3,2 --num-processors 3 --precedence-pairs "0>3,1>3,1>4,2>4,2>5" -o swid.json
pred solve swid.json --solver brute-force
pred create SequencingToMinimizeWeightedCompletionTime --lengths 2,1,3,1,2 --weights 3,5,1,4,2 --precedence-pairs "0>2,1>4" -o stmwct.json
pred create StringToStringCorrection --source-string "0,1,2,3,1,0" --target-string "0,1,3,2,1" --bound 2 | pred solve - --solver brute-force
pred create StrongConnectivityAugmentation --arcs "0>1,1>2,2>0,3>4,4>3,2>3,4>5,5>3" --candidate-arcs "3>0:5,3>1:3,3>2:4,4>0:6,4>1:2,4>2:7,5>0:4,5>1:3,5>2:1,0>3:8,0>4:3,0>5:2,1>3:6,1>4:4,1>5:5,2>4:3,2>5:7,1>0:2" --bound 1 -o sca.json
```
Expand Down
5 changes: 3 additions & 2 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ Flags by problem type:
RectilinearPictureCompression --matrix (0/1), --k
SchedulingWithIndividualDeadlines --n, --num-processors/--m, --deadlines [--precedence-pairs]
SequencingToMinimizeMaximumCumulativeCost --costs, --bound [--precedence-pairs]
SequencingToMinimizeWeightedCompletionTime --lengths, --weights [--precedence-pairs]
SequencingToMinimizeWeightedTardiness --sizes, --weights, --deadlines, --bound
SCS --strings, --bound [--alphabet-size]
StringToStringCorrection --source-string, --target-string, --bound [--alphabet-size]
Expand Down Expand Up @@ -524,7 +525,7 @@ pub struct CreateArgs {
/// Deadlines for MinimumTardinessSequencing or SchedulingWithIndividualDeadlines (comma-separated, e.g., "5,5,5,3,3")
#[arg(long)]
pub deadlines: Option<String>,
/// Precedence pairs for MinimumTardinessSequencing or SchedulingWithIndividualDeadlines (e.g., "0>3,1>3,1>4,2>4")
/// Precedence pairs for MinimumTardinessSequencing, SchedulingWithIndividualDeadlines, or SequencingToMinimizeWeightedCompletionTime (e.g., "0>3,1>3,1>4,2>4")
#[arg(long)]
pub precedence_pairs: Option<String>,
/// Resource bounds for ResourceConstrainedScheduling (comma-separated, e.g., "20,15")
Expand Down Expand Up @@ -735,7 +736,7 @@ mod tests {
"Deadlines for MinimumTardinessSequencing or SchedulingWithIndividualDeadlines"
));
assert!(help.contains(
"Precedence pairs for MinimumTardinessSequencing or SchedulingWithIndividualDeadlines"
"Precedence pairs for MinimumTardinessSequencing, SchedulingWithIndividualDeadlines, or SequencingToMinimizeWeightedCompletionTime"
));
assert!(
help.contains(
Expand Down
71 changes: 68 additions & 3 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ use problemreductions::models::misc::{
MultiprocessorScheduling, PaintShop, PartiallyOrderedKnapsack, QueryArg,
RectilinearPictureCompression, ResourceConstrainedScheduling,
SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost,
SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines,
SequencingWithinIntervals, ShortestCommonSupersequence, StringToStringCorrection, SubsetSum,
SumOfSquaresPartition,
SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness,
SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence,
StringToStringCorrection, SubsetSum, SumOfSquaresPartition,
};
use problemreductions::models::BiconnectivityAugmentation;
use problemreductions::prelude::*;
Expand Down Expand Up @@ -110,6 +110,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.potential_edges.is_none()
&& args.budget.is_none()
&& args.deadlines.is_none()
&& args.lengths.is_none()
&& args.precedence_pairs.is_none()
&& args.resource_bounds.is_none()
&& args.resource_requirements.is_none()
Expand Down Expand Up @@ -2214,6 +2215,70 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// SequencingToMinimizeWeightedCompletionTime
"SequencingToMinimizeWeightedCompletionTime" => {
let lengths_str = args.lengths.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"SequencingToMinimizeWeightedCompletionTime requires --lengths and --weights\n\n\
Usage: pred create SequencingToMinimizeWeightedCompletionTime --lengths 2,1,3,1,2 --weights 3,5,1,4,2 [--precedence-pairs \"0>2,1>4\"]"
)
})?;
let weights_str = args.weights.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"SequencingToMinimizeWeightedCompletionTime requires --weights\n\n\
Usage: pred create SequencingToMinimizeWeightedCompletionTime --lengths 2,1,3,1,2 --weights 3,5,1,4,2"
)
})?;
let lengths: Vec<u64> = util::parse_comma_list(lengths_str)?;
let weights: Vec<u64> = util::parse_comma_list(weights_str)?;
anyhow::ensure!(
lengths.len() == weights.len(),
"lengths length ({}) must equal weights length ({})",
lengths.len(),
weights.len()
);
anyhow::ensure!(
lengths.iter().all(|&length| length > 0),
"task lengths must be positive"
);
let num_tasks = lengths.len();
let precedences: Vec<(usize, usize)> = match args.precedence_pairs.as_deref() {
Some(s) if !s.is_empty() => s
.split(',')
.map(|pair| {
let parts: Vec<&str> = pair.trim().split('>').collect();
anyhow::ensure!(
parts.len() == 2,
"Invalid precedence format '{}', expected 'u>v'",
pair.trim()
);
Ok((
parts[0].trim().parse::<usize>()?,
parts[1].trim().parse::<usize>()?,
))
})
.collect::<Result<Vec<_>>>()?,
_ => vec![],
};
for &(pred, succ) in &precedences {
anyhow::ensure!(
pred < num_tasks && succ < num_tasks,
"precedence index out of range: ({}, {}) but num_tasks = {}",
pred,
succ,
num_tasks
);
}
(
ser(SequencingToMinimizeWeightedCompletionTime::new(
lengths,
weights,
precedences,
))?,
resolved_variant.clone(),
)
}

// SequencingToMinimizeWeightedTardiness
"SequencingToMinimizeWeightedTardiness" => {
let sizes_str = args.sizes.as_deref().ok_or_else(|| {
Expand Down
Loading
Loading