Skip to content
158 changes: 158 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@
"IntegralFlowWithMultipliers": [Integral Flow With Multipliers],
"MinMaxMulticenter": [Min-Max Multicenter],
"FlowShopScheduling": [Flow Shop Scheduling],
"JobShopScheduling": [Job-Shop Scheduling],
"GroupingBySwapping": [Grouping by Swapping],
"MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets],
"MinimumDummyActivitiesPert": [Minimum Dummy Activities in PERT Networks],
Expand Down Expand Up @@ -5122,6 +5123,163 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
]
}

#{
let x = load-model-example("JobShopScheduling")
let jobs = x.instance.jobs
let m = x.instance.num_processors
let n = jobs.len()
let lehmer = x.optimal_config

// Flatten tasks: build per-machine task lists and lengths
let task-lengths = ()
let task-job = () // which job each flat task belongs to
let task-index = () // which task within the job
let machine-tasks = range(m).map(_ => ())
let tid = 0
for (ji, job) in jobs.enumerate() {
for (ki, op) in job.enumerate() {
let (mi, len) = op
task-lengths.push(len)
task-job.push(ji)
task-index.push(ki)
machine-tasks.at(mi).push(tid)
tid += 1
}
}
let T = task-lengths.len()

// Decode per-machine Lehmer codes into machine orders
let offset = 0
let machine-orders = ()
for mi in range(m) {
let mt = machine-tasks.at(mi)
let k = mt.len()
let seg = lehmer.slice(offset, offset + k)
let avail = range(k)
let order = ()
for c in seg {
order.push(mt.at(avail.at(c)))
avail = avail.enumerate().filter(((i, v)) => i != c).map(((i, v)) => v)
}
machine-orders.push(order)
offset += k
}

// Build DAG edges (job precedence + machine order)
let successors = range(T).map(_ => ())
let indegree = range(T).map(_ => 0)
// Job precedence edges
let job-task-start = 0
for job in jobs {
for i in range(job.len() - 1) {
let u = job-task-start + i
let v = job-task-start + i + 1
successors.at(u).push(v)
indegree.at(v) += 1
}
job-task-start += job.len()
}
// Machine order edges
for order in machine-orders {
for i in range(order.len() - 1) {
let u = order.at(i)
let v = order.at(i + 1)
successors.at(u).push(v)
indegree.at(v) += 1
}
}

// Topological sort + longest-path to compute start times
let start-times = range(T).map(_ => 0)
let queue = ()
for t in range(T) {
if indegree.at(t) == 0 { queue.push(t) }
}
while queue.len() > 0 {
let u = queue.remove(0)
let finish = start-times.at(u) + task-lengths.at(u)
for v in successors.at(u) {
if finish > start-times.at(v) { start-times.at(v) = finish }
indegree.at(v) -= 1
if indegree.at(v) == 0 { queue.push(v) }
}
}

// Build Gantt blocks: (machine, job, task-within-job, start, end)
let blocks = ()
for t in range(T) {
let (mi, _len) = jobs.at(task-job.at(t)).at(task-index.at(t))
blocks.push((mi, task-job.at(t), task-index.at(t), start-times.at(t), start-times.at(t) + task-lengths.at(t)))
}
let makespan = calc.max(..range(T).map(t => start-times.at(t) + task-lengths.at(t)))
[
#problem-def("JobShopScheduling")[
Given a positive integer $m$, a set $J$ of jobs, where each job $j in J$ consists of an ordered list of tasks $t_1[j], dots, t_(n_j)[j]$ with processor assignments $p(t_k[j]) in {1, dots, m}$, processing lengths $ell(t_k[j]) in ZZ^+_0$, and consecutive-processor constraint $p(t_k[j]) != p(t_(k+1)[j])$, find start times $sigma(t_k[j]) in ZZ^+_0$ such that tasks sharing a processor do not overlap, each job respects $sigma(t_(k+1)[j]) >= sigma(t_k[j]) + ell(t_k[j])$, and the makespan $max_(j in J) (sigma(t_(n_j)[j]) + ell(t_(n_j)[j]))$ is minimized.
][
Job-Shop Scheduling is the classical disjunctive scheduling problem SS18 in Garey & Johnson; Garey, Johnson, and Sethi proved it strongly NP-hard already for two machines @garey1976. Unlike Flow Shop Scheduling, each job carries its own machine route, so the difficulty lies in choosing a compatible relative order on every machine and then finding the schedule with minimum makespan. This implementation follows the original Garey-Johnson formulation, including the requirement that consecutive tasks of the same job use different processors, and evaluates a witness by orienting the machine-order edges and propagating longest paths through the resulting precedence DAG. The registered baseline therefore exposes a factorial upper bound over task orders#footnote[The auto-generated complexity table records the concrete upper bound used by the Rust implementation; no sharper exact bound is cited here.].

*Example.* The canonical fixture has #m machines and #n jobs
$
#for (ji, job) in jobs.enumerate() {
$J_#(ji+1) = (#job.map(((mi, len)) => $(M_#(mi+1), #len)$).join($,$))$
if ji < n - 1 [$,$] else [.]
}
$
The witness stored in the example DB orders the six tasks on $M_1$ as $(J_1^1, J_2^2, J_3^1, J_4^2, J_5^1, J_5^3)$ and the six tasks on $M_2$ as $(J_2^1, J_4^1, J_1^2, J_3^2, J_5^2, J_2^3)$. Taking the earliest schedule consistent with those machine orders yields the Gantt chart in @fig:jobshop, whose makespan is $#makespan$.

#pred-commands(
"pred create --example " + problem-spec(x) + " -o job-shop-scheduling.json",
"pred solve job-shop-scheduling.json --solver brute-force",
"pred evaluate job-shop-scheduling.json --config " + x.optimal_config.map(str).join(","),
)

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

for mi in range(m) {
let y = -mi * (row-h + gap)
content((-0.8, y), text(8pt, "M" + str(mi + 1)))
}

for block in blocks {
let (mi, ji, ti, s, e) = block
let x0 = s * scale
let x1 = e * scale
let y = -mi * (row-h + gap)
rect(
(x0, y - row-h / 2),
(x1, y + row-h / 2),
fill: colors.at(ji).transparentize(30%),
stroke: 0.4pt + colors.at(ji),
)
content(((x0 + x1) / 2, y), text(6pt, "j" + str(ji + 1) + "." + str(ti + 1)))
}

let y-axis = -(m - 1) * (row-h + gap) - row-h / 2 - 0.2
line((0, y-axis), (makespan * scale, y-axis), stroke: 0.4pt)
for t in range(calc.ceil(makespan / 5) + 1).map(i => calc.min(i * 5, makespan)) {
let x = t * scale
line((x, y-axis), (x, y-axis - 0.1), stroke: 0.4pt)
content((x, y-axis - 0.25), text(6pt, str(t)))
}
if calc.rem(makespan, 5) != 0 {
let x = makespan * scale
line((x, y-axis), (x, y-axis - 0.1), stroke: 0.4pt)
content((x, y-axis - 0.25), text(6pt, str(makespan)))
}
content((makespan * scale / 2, y-axis - 0.5), text(7pt)[$t$])
}),
caption: [Job-shop schedule induced by the canonical machine-order witness. The optimal makespan is #makespan.],
) <fig:jobshop>
]
]
}

#problem-def("StaffScheduling")[
Given a collection $C$ of binary schedule patterns of length $m$, where each pattern has exactly $k$ ones, a requirement vector $overline(R) in ZZ_(>= 0)^m$, and a worker budget $n in ZZ_(>= 0)$, determine whether there exists a function $f: C -> ZZ_(>= 0)$ such that $sum_(c in C) f(c) <= n$ and $sum_(c in C) f(c) dot c >= overline(R)$ component-wise.
][
Expand Down
8 changes: 6 additions & 2 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ Flags by problem type:
PartiallyOrderedKnapsack --sizes, --values, --capacity, --precedences
QAP --matrix (cost), --distance-matrix
StrongConnectivityAugmentation --arcs, --candidate-arcs, --bound [--num-vertices]
JobShopScheduling --job-tasks [--num-processors]
FlowShopScheduling --task-lengths, --deadline [--num-processors]
StaffScheduling --schedules, --requirements, --num-workers, --k
TimetableDesign --num-periods, --num-craftsmen, --num-tasks, --craftsman-avail, --task-avail, --requirements
Expand Down Expand Up @@ -643,10 +644,13 @@ pub struct CreateArgs {
/// Task lengths for FlowShopScheduling (semicolon-separated rows: "3,4,2;2,3,5;4,1,3")
#[arg(long)]
pub task_lengths: Option<String>,
/// Job tasks for JobShopScheduling (semicolon-separated jobs, comma-separated processor:length tasks, e.g., "0:3,1:4;1:2,0:3,1:2")
#[arg(long)]
pub job_tasks: Option<String>,
/// Deadline for FlowShopScheduling, MultiprocessorScheduling, or ResourceConstrainedScheduling
#[arg(long)]
pub deadline: Option<u64>,
/// Number of processors/machines for FlowShopScheduling, MultiprocessorScheduling, ResourceConstrainedScheduling, or SchedulingWithIndividualDeadlines
/// Number of processors/machines for FlowShopScheduling, JobShopScheduling, MultiprocessorScheduling, ResourceConstrainedScheduling, or SchedulingWithIndividualDeadlines
#[arg(long)]
pub num_processors: Option<usize>,
/// Binary schedule patterns for StaffScheduling (semicolon-separated rows, e.g., "1,1,0;0,1,1")
Expand Down Expand Up @@ -883,7 +887,7 @@ mod tests {
));
assert!(
help.contains(
"Number of processors/machines for FlowShopScheduling, MultiprocessorScheduling, ResourceConstrainedScheduling, or SchedulingWithIndividualDeadlines"
"Number of processors/machines for FlowShopScheduling, JobShopScheduling, MultiprocessorScheduling, ResourceConstrainedScheduling, or SchedulingWithIndividualDeadlines"
),
"create help should describe --num-processors for both scheduling models"
);
Expand Down
Loading
Loading