Skip to content

Commit 3ce1685

Browse files
committed
2 parents 9c1603c + 7ae0232 commit 3ce1685

16 files changed

Lines changed: 776 additions & 49 deletions

File tree

docs/paper/reductions.typ

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
"SubgraphIsomorphism": [Subgraph Isomorphism],
115115
"PartitionIntoTriangles": [Partition Into Triangles],
116116
"FlowShopScheduling": [Flow Shop Scheduling],
117+
"MultiprocessorScheduling": [Multiprocessor Scheduling],
117118
"MinimumTardinessSequencing": [Minimum Tardiness Sequencing],
118119
"SequencingWithinIntervals": [Sequencing Within Intervals],
119120
"DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow],
@@ -2377,6 +2378,75 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS
23772378
) <fig:flowshop>
23782379
]
23792380

2381+
#{
2382+
let x = load-model-example("MultiprocessorScheduling")
2383+
let lengths = x.instance.lengths
2384+
let num-processors = x.instance.num_processors
2385+
let deadline = x.instance.deadline
2386+
let assignment = x.optimal.at(0).config
2387+
let tasks-by-processor = range(num-processors).map(p =>
2388+
range(lengths.len()).filter(i => assignment.at(i) == p)
2389+
)
2390+
let loads = tasks-by-processor.map(tasks => tasks.map(i => lengths.at(i)).sum())
2391+
let max-x = (num-processors - 1) * 1.8 + 1.0
2392+
[
2393+
#problem-def("MultiprocessorScheduling")[
2394+
Given a finite set $T$ of tasks with processing lengths $ell: T -> ZZ^+$, a number $m in ZZ^+$ of identical processors, and a deadline $D in ZZ^+$, determine whether there exists an assignment $p: T -> {1, dots, m}$ such that for every processor $i in {1, dots, m}$ we have $sum_(t in T: p(t) = i) ell(t) <= D$.
2395+
][
2396+
Multiprocessor Scheduling is problem SS8 in Garey & Johnson @garey1979. Their original formulation uses start times on identical processors, but because tasks are independent and non-preemptive, any feasible schedule can be packed contiguously on each processor. The model implemented here therefore uses processor-assignment variables, and feasibility reduces to checking that every processor's total load is at most $D$. For fixed $m$, dynamic programming over load vectors gives pseudo-polynomial algorithms; for general $m$, the best known exact algorithm runs in $O^*(2^n)$ time via inclusion-exclusion over set partitions @bjorklund2009.
2397+
2398+
*Example.* Let $T = {t_1, dots, t_5}$ with lengths $(4, 5, 3, 2, 6)$, $m = 2$, and $D = 10$. The satisfying assignment $(1, 2, 2, 2, 1)$ places $t_1$ and $t_5$ on processor 1 and $t_2, t_3, t_4$ on processor 2. The verifier computes the processor loads $4 + 6 = 10$ and $5 + 3 + 2 = 10$, so both meet the deadline exactly.
2399+
2400+
#figure({
2401+
canvas(length: 1cm, {
2402+
let scale = 0.25
2403+
let width = 1.0
2404+
let gap = 0.8
2405+
let colors = (
2406+
rgb("#4e79a7"),
2407+
rgb("#e15759"),
2408+
rgb("#76b7b2"),
2409+
rgb("#f28e2b"),
2410+
rgb("#59a14f"),
2411+
)
2412+
2413+
for p in range(num-processors) {
2414+
let x0 = p * (width + gap)
2415+
draw.rect((x0, 0), (x0 + width, deadline * scale), stroke: 0.8pt + black)
2416+
let y = 0
2417+
for task in tasks-by-processor.at(p) {
2418+
let len = lengths.at(task)
2419+
let col = colors.at(task)
2420+
draw.rect(
2421+
(x0, y),
2422+
(x0 + width, y + len * scale),
2423+
fill: col.transparentize(25%),
2424+
stroke: 0.4pt + col,
2425+
)
2426+
draw.content(
2427+
(x0 + width / 2, y + len * scale / 2),
2428+
text(7pt, fill: white)[$t_#(task + 1)$],
2429+
)
2430+
y += len * scale
2431+
}
2432+
draw.content((x0 + width / 2, -0.3), text(8pt)[$P_#(p + 1)$])
2433+
draw.content((x0 + width / 2, deadline * scale + 0.25), text(7pt)[$L_#(p + 1) = #loads.at(p)$])
2434+
}
2435+
2436+
draw.line(
2437+
(-0.15, deadline * scale),
2438+
(max-x + 0.15, deadline * scale),
2439+
stroke: (dash: "dashed", paint: luma(150), thickness: 0.5pt),
2440+
)
2441+
draw.content((-0.45, deadline * scale), text(7pt)[$D$])
2442+
})
2443+
},
2444+
caption: [Canonical Multiprocessor Scheduling instance with 5 tasks on 2 processors. Stacked blocks show the satisfying assignment $(1, 2, 2, 2, 1)$; both processor loads equal the deadline $D = 10$.],
2445+
) <fig:multiprocessor-scheduling>
2446+
]
2447+
]
2448+
}
2449+
23802450
#{
23812451
let x = load-model-example("SequencingWithinIntervals")
23822452
let ntasks = x.instance.lengths.len()

docs/paper/references.bib

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ @article{shang2018
4040
doi = {10.1016/j.cor.2017.10.015}
4141
}
4242

43+
@article{brucker1977,
44+
author = {Peter Brucker and Jan Karel Lenstra and Alexander H. G. Rinnooy Kan},
45+
title = {Complexity of Machine Scheduling Problems},
46+
journal = {Annals of Discrete Mathematics},
47+
volume = {1},
48+
pages = {343--362},
49+
year = {1977},
50+
doi = {10.1016/S0167-5060(08)70743-X}
51+
}
52+
4353
@inproceedings{karp1972,
4454
author = {Richard M. Karp},
4555
title = {Reducibility among Combinatorial Problems},

docs/src/cli.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -515,8 +515,9 @@ Source evaluation: Valid(2)
515515
```
516516
517517
> **Note:** The ILP solver requires a reduction path from the target problem to ILP.
518-
> Some problems (e.g., BoundedComponentSpanningForest, LengthBoundedDisjointPaths) do not currently have one, so use
519-
> `pred solve <file> --solver brute-force` for these.
518+
> Some problems do not currently have one. Examples include BoundedComponentSpanningForest,
519+
> LengthBoundedDisjointPaths, QUBO, SpinGlass, MaxCut, CircuitSAT, and MultiprocessorScheduling.
520+
> Use `pred solve <file> --solver brute-force` for these, or reduce to a problem that supports ILP first.
520521
> For other problems, use `pred path <PROBLEM> ILP` to check whether an ILP reduction path exists.
521522
522523
## Shell Completions

problemreductions-cli/src/cli.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ Flags by problem type:
243243
BMF --matrix (0/1), --rank
244244
SteinerTree --graph, --edge-weights, --terminals
245245
CVP --basis, --target-vec [--bounds]
246+
MultiprocessorScheduling --lengths, --num-processors, --deadline
246247
SequencingWithinIntervals --release-times, --deadlines, --lengths
247248
OptimalLinearArrangement --graph, --bound
248249
RuralPostman (RPP) --graph, --edge-weights, --required-edges, --bound
@@ -276,6 +277,7 @@ Examples:
276277
pred create MIS/KingsSubgraph --positions \"0,0;1,0;1,1;0,1\"
277278
pred create MIS/UnitDiskGraph --positions \"0,0;1,0;0.5,0.8\" --radius 1.5
278279
pred create MIS --random --num-vertices 10 --edge-prob 0.3
280+
pred create MultiprocessorScheduling --lengths 4,5,3,2,6 --num-processors 2 --deadline 10
279281
pred create BiconnectivityAugmentation --graph 0-1,1-2,2-3 --potential-edges 0-2:3,0-3:4,1-3:2 --budget 5
280282
pred create FVS --arcs \"0>1,1>2,2>0\" --weights 1,1,1
281283
pred create UndirectedTwoCommodityIntegralFlow --graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1
@@ -432,7 +434,7 @@ pub struct CreateArgs {
432434
/// Release times for SequencingWithinIntervals (comma-separated, e.g., "0,0,5")
433435
#[arg(long)]
434436
pub release_times: Option<String>,
435-
/// Processing lengths for SequencingWithinIntervals (comma-separated, e.g., "3,1,1")
437+
/// Processing lengths (comma-separated, e.g., "4,5,3,2,6")
436438
#[arg(long)]
437439
pub lengths: Option<String>,
438440
/// Terminal vertices for SteinerTree or MinimumMultiwayCut (comma-separated indices, e.g., "0,2,4")
@@ -474,10 +476,10 @@ pub struct CreateArgs {
474476
/// Task lengths for FlowShopScheduling (semicolon-separated rows: "3,4,2;2,3,5;4,1,3")
475477
#[arg(long)]
476478
pub task_lengths: Option<String>,
477-
/// Deadline for FlowShopScheduling
479+
/// Deadline for FlowShopScheduling or MultiprocessorScheduling
478480
#[arg(long)]
479481
pub deadline: Option<u64>,
480-
/// Number of processors/machines for FlowShopScheduling
482+
/// Number of processors/machines for FlowShopScheduling or MultiprocessorScheduling
481483
#[arg(long)]
482484
pub num_processors: Option<usize>,
483485
/// Alphabet size for SCS (optional; inferred from max symbol + 1 if omitted)

problemreductions-cli/src/commands/create.rs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ use problemreductions::models::graph::{
1414
};
1515
use problemreductions::models::misc::{
1616
BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing,
17-
PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence, SubsetSum,
17+
MultiprocessorScheduling, PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence,
18+
SubsetSum,
1819
};
1920
use problemreductions::models::BiconnectivityAugmentation;
2021
use problemreductions::prelude::*;
@@ -226,7 +227,7 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str {
226227
Some("UnitDiskGraph") => "float positions: \"0.0,0.0;1.0,0.0\"",
227228
_ => "edge list: 0-1,1-2,2-3",
228229
},
229-
"Vec<u64>" => "comma-separated integers: 1,2,3",
230+
"Vec<u64>" => "comma-separated integers: 4,5,3,2,6",
230231
"Vec<W>" => "comma-separated: 1,2,3",
231232
"Vec<usize>" => "comma-separated indices: 0,2,4",
232233
"Vec<(usize, usize, W)>" | "Vec<(usize,usize,W)>" => {
@@ -288,6 +289,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
288289
}
289290
"PartitionIntoTriangles" => "--graph 0-1,1-2,0-2",
290291
"Factoring" => "--target 15 --m 4 --n 4",
292+
"MultiprocessorScheduling" => "--lengths 4,5,3,2,6 --num-processors 2 --deadline 10",
291293
"MinimumMultiwayCut" => "--graph 0-1,1-2,2-3 --terminals 0,2 --edge-weights 1,1,1",
292294
"SequencingWithinIntervals" => "--release-times 0,0,5 --deadlines 11,11,6 --lengths 3,1,1",
293295
"SteinerTree" => "--graph 0-1,1-2,1-3,3-4 --edge-weights 2,2,1,1 --terminals 0,2,4",
@@ -1286,6 +1288,34 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
12861288
)
12871289
}
12881290

1291+
// MultiprocessorScheduling
1292+
"MultiprocessorScheduling" => {
1293+
let usage = "Usage: pred create MultiprocessorScheduling --lengths 4,5,3,2,6 --num-processors 2 --deadline 10";
1294+
let lengths_str = args.lengths.as_deref().ok_or_else(|| {
1295+
anyhow::anyhow!(
1296+
"MultiprocessorScheduling requires --lengths, --num-processors, and --deadline\n\n{usage}"
1297+
)
1298+
})?;
1299+
let num_processors = args.num_processors.ok_or_else(|| {
1300+
anyhow::anyhow!("MultiprocessorScheduling requires --num-processors\n\n{usage}")
1301+
})?;
1302+
if num_processors == 0 {
1303+
bail!("MultiprocessorScheduling requires --num-processors > 0\n\n{usage}");
1304+
}
1305+
let deadline = args.deadline.ok_or_else(|| {
1306+
anyhow::anyhow!("MultiprocessorScheduling requires --deadline\n\n{usage}")
1307+
})?;
1308+
let lengths: Vec<u64> = util::parse_comma_list(lengths_str)?;
1309+
(
1310+
ser(MultiprocessorScheduling::new(
1311+
lengths,
1312+
num_processors,
1313+
deadline,
1314+
))?,
1315+
resolved_variant.clone(),
1316+
)
1317+
}
1318+
12891319
// MinimumMultiwayCut
12901320
"MinimumMultiwayCut" => {
12911321
let (graph, _) = parse_graph(args).map_err(|e| {
@@ -1708,7 +1738,6 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
17081738
resolved_variant.clone(),
17091739
)
17101740
}
1711-
17121741
_ => bail!("{}", crate::problem_name::unknown_problem_error(canonical)),
17131742
};
17141743

problemreductions-cli/src/commands/inspect.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,17 @@ fn inspect_problem(pj: &ProblemJson, out: &OutputConfig) -> Result<()> {
4040
}
4141
text.push_str(&format!("Variables: {}\n", problem.num_variables_dyn()));
4242

43-
// Solvers
44-
text.push_str("Solvers: ilp (default), brute-force\n");
43+
let solvers = if problem.supports_ilp_solver() {
44+
vec!["ilp", "brute-force"]
45+
} else {
46+
vec!["brute-force"]
47+
};
48+
let solver_summary = if solvers.first() == Some(&"ilp") {
49+
"ilp (default), brute-force".to_string()
50+
} else {
51+
"brute-force".to_string()
52+
};
53+
text.push_str(&format!("Solvers: {solver_summary}\n"));
4554

4655
// Reductions
4756
let outgoing = graph.outgoing_reductions(name);
@@ -56,7 +65,7 @@ fn inspect_problem(pj: &ProblemJson, out: &OutputConfig) -> Result<()> {
5665
"variant": variant,
5766
"size_fields": size_fields,
5867
"num_variables": problem.num_variables_dyn(),
59-
"solvers": ["ilp", "brute-force"],
68+
"solvers": solvers,
6069
"reduces_to": targets,
6170
});
6271

problemreductions-cli/src/commands/solve.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ fn solve_problem(
9797
result
9898
}
9999
"ilp" => {
100-
let result = problem.solve_with_ilp()?;
100+
let result = problem.solve_with_ilp().map_err(add_ilp_solver_hint)?;
101101
let solver_desc = if name == "ILP" {
102102
"ilp".to_string()
103103
} else {
@@ -139,7 +139,7 @@ fn solve_bundle(bundle: ReductionBundle, solver_name: &str, out: &OutputConfig)
139139
// 2. Solve the target problem
140140
let target_result = match solver_name {
141141
"brute-force" => target.solve_brute_force()?,
142-
"ilp" => target.solve_with_ilp()?,
142+
"ilp" => target.solve_with_ilp().map_err(add_ilp_solver_hint)?,
143143
_ => unreachable!(),
144144
};
145145

@@ -200,3 +200,14 @@ fn solve_bundle(bundle: ReductionBundle, solver_name: &str, out: &OutputConfig)
200200
}
201201
result
202202
}
203+
204+
fn add_ilp_solver_hint(err: anyhow::Error) -> anyhow::Error {
205+
let message = err.to_string();
206+
if message.starts_with("No reduction path from ") && message.ends_with(" to ILP") {
207+
anyhow::anyhow!(
208+
"{message}\n\nHint: try `--solver brute-force` for direct exhaustive search on small instances."
209+
)
210+
} else {
211+
err
212+
}
213+
}

problemreductions-cli/src/dispatch.rs

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -47,53 +47,63 @@ impl LoadedProblem {
4747
Ok(SolveResult { config, evaluation })
4848
}
4949

50+
pub fn supports_ilp_solver(&self) -> bool {
51+
let name = self.problem_name();
52+
name == "ILP" || self.best_ilp_reduction_path().is_some()
53+
}
54+
5055
/// Solve using the ILP solver. If the problem is not ILP, auto-reduce to ILP first.
5156
pub fn solve_with_ilp(&self) -> Result<SolveResult> {
5257
let name = self.problem_name();
5358
if name == "ILP" {
5459
return solve_ilp(self.as_any());
5560
}
5661

57-
// Auto-reduce to ILP, solve, and map solution back
62+
let reduction_path = self.best_ilp_reduction_path().ok_or_else(|| {
63+
anyhow::anyhow!(
64+
"No reduction path from {} to ILP. Try `--solver brute-force`, or reduce to a problem that supports ILP.",
65+
name
66+
)
67+
})?;
68+
let graph = ReductionGraph::new();
69+
70+
let chain = graph
71+
.reduce_along_path(&reduction_path, self.as_any())
72+
.ok_or_else(|| anyhow::anyhow!("Failed to execute reduction chain to ILP"))?;
73+
74+
let ilp_result = solve_ilp(chain.target_problem_any())?;
75+
let config = chain.extract_solution(&ilp_result.config);
76+
let evaluation = self.evaluate_dyn(&config);
77+
Ok(SolveResult { config, evaluation })
78+
}
79+
80+
fn best_ilp_reduction_path(&self) -> Option<problemreductions::rules::ReductionPath> {
81+
let name = self.problem_name();
5882
let source_variant = self.variant_map();
5983
let graph = ReductionGraph::new();
6084
let ilp_variants = graph.variants_for("ILP");
6185
let input_size = ProblemSize::new(vec![]);
6286

6387
let mut best_path = None;
6488
for dv in &ilp_variants {
65-
if let Some(p) = graph.find_cheapest_path(
89+
if let Some(path) = graph.find_cheapest_path(
6690
name,
6791
&source_variant,
6892
"ILP",
6993
dv,
7094
&input_size,
7195
&MinimizeSteps,
7296
) {
73-
let is_better = best_path
74-
.as_ref()
75-
.is_none_or(|bp: &problemreductions::rules::ReductionPath| p.len() < bp.len());
97+
let is_better = best_path.as_ref().is_none_or(
98+
|current: &problemreductions::rules::ReductionPath| path.len() < current.len(),
99+
);
76100
if is_better {
77-
best_path = Some(p);
101+
best_path = Some(path);
78102
}
79103
}
80104
}
81105

82-
let reduction_path = best_path.ok_or_else(|| {
83-
anyhow::anyhow!(
84-
"No reduction path from {} to ILP. Try `--solver brute-force`, or reduce to a problem that supports ILP.",
85-
name
86-
)
87-
})?;
88-
89-
let chain = graph
90-
.reduce_along_path(&reduction_path, self.as_any())
91-
.ok_or_else(|| anyhow::anyhow!("Failed to execute reduction chain to ILP"))?;
92-
93-
let ilp_result = solve_ilp(chain.target_problem_any())?;
94-
let config = chain.extract_solution(&ilp_result.config);
95-
let evaluation = self.evaluate_dyn(&config);
96-
Ok(SolveResult { config, evaluation })
106+
best_path
97107
}
98108
}
99109

@@ -249,4 +259,26 @@ mod tests {
249259
let json = serialize_any_problem("BinPacking", &variant, &problem as &dyn Any).unwrap();
250260
assert_eq!(json, serde_json::to_value(&problem).unwrap());
251261
}
262+
263+
#[test]
264+
fn test_load_problem_rejects_zero_processor_multiprocessor_scheduling() {
265+
let loaded = load_problem(
266+
"MultiprocessorScheduling",
267+
&BTreeMap::new(),
268+
serde_json::json!({
269+
"lengths": [1, 2],
270+
"num_processors": 0,
271+
"deadline": 5
272+
}),
273+
);
274+
assert!(
275+
loaded.is_err(),
276+
"zero-processor instance should be rejected"
277+
);
278+
let err = loaded.err().unwrap();
279+
assert!(
280+
err.to_string().contains("expected positive integer, got 0"),
281+
"unexpected error: {err}"
282+
);
283+
}
252284
}

0 commit comments

Comments
 (0)