Skip to content
Open
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
77 changes: 77 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
"PartitionIntoTriangles": [Partition Into Triangles],
"FlowShopScheduling": [Flow Shop Scheduling],
"MinimumTardinessSequencing": [Minimum Tardiness Sequencing],
"SequencingToMinimizeMaximumCumulativeCost": [Sequencing to Minimize Maximum Cumulative Cost],
"SequencingWithinIntervals": [Sequencing Within Intervals],
"DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow],
)
Expand Down Expand Up @@ -2279,6 +2280,82 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS
]
}

#{
let x = load-model-example("SequencingToMinimizeMaximumCumulativeCost")
let costs = x.instance.costs
let precs = x.instance.precedences
let bound = x.instance.bound
let ntasks = costs.len()
let sample = x.samples.at(0)
let satisfying-count = x.optimal.len()
let lehmer = sample.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 prefix-sums = {
let running = 0
let result = ()
for task in schedule {
running += costs.at(task)
result.push(running)
}
result
}
[
#problem-def("SequencingToMinimizeMaximumCumulativeCost")[
Given a set $T$ of $n$ tasks, a precedence relation $prec.eq$ on $T$, an integer cost function $c: T -> ZZ$ (negative values represent profits), and a bound $K in ZZ$, determine whether there exists a one-machine schedule $sigma: T -> {1, 2, dots, n}$ that respects the precedence constraints and satisfies
$sum_(sigma(t') lt.eq sigma(t)) c(t') lt.eq K$
for every task $t in T$.
][
Sequencing to Minimize Maximum Cumulative Cost is the scheduling problem SS7 in Garey & Johnson @garey1979. It is NP-complete by transformation from Register Sufficiency, even when every task cost is in ${-1, 0, 1}$ @garey1979. The problem models precedence-constrained task systems with resource consumption and release, where a negative cost corresponds to a profit or resource refund accumulated as the schedule proceeds.

When the precedence constraints form a series-parallel digraph, #cite(<abdelWahabKameda1978>, form: "prose") gave a polynomial-time algorithm running in $O(n^2)$ time. #cite(<monmaSidney1979>, form: "prose") placed the problem in a broader family of sequencing objectives solvable efficiently on series-parallel precedence structures. The implementation here uses Lehmer-code enumeration of task orders, so the direct exact search induced by the model runs in $O(n!)$ time.

*Example.* Consider $n = #ntasks$ tasks with costs $(#costs.map(c => str(c)).join(", "))$, precedence constraints #{precs.map(p => [$t_#(p.at(0) + 1) prec.eq t_#(p.at(1) + 1)$]).join(", ")}, and bound $K = #bound$. The sample schedule $(#schedule.map(t => $t_#(t + 1)$).join(", "))$ has cumulative sums $(#prefix-sums.map(v => str(v)).join(", "))$, so every prefix stays at or below $K = #bound$. Our brute-force solver finds exactly #satisfying-count satisfying schedules for this instance.

#figure(
{
let pos = rgb("#f28e2b")
let neg = rgb("#76b7b2")
let zero = rgb("#bab0ab")
align(center, stack(dir: ttb, spacing: 0.35cm,
stack(dir: ltr, spacing: 0.08cm,
..schedule.enumerate().map(((i, task)) => {
let cost = costs.at(task)
let fill = if cost > 0 {
pos.transparentize(70%)
} else if cost < 0 {
neg.transparentize(65%)
} else {
zero.transparentize(65%)
}
stack(dir: ttb, spacing: 0.05cm,
box(width: 1.0cm, height: 0.6cm, fill: fill, stroke: 0.4pt + luma(120),
align(center + horizon, text(8pt, weight: "bold")[$t_#(task + 1)$])),
text(6pt, if cost >= 0 { $+ #cost$ } else { $#cost$ }),
)
}),
),
stack(dir: ltr, spacing: 0.08cm,
..prefix-sums.map(v => {
box(width: 1.0cm, align(center + horizon, text(7pt)[$#v$]))
}),
),
text(7pt, [prefix sums after each scheduled task]),
))
},
caption: [A satisfying schedule for Sequencing to Minimize Maximum Cumulative Cost. Orange boxes add cost, teal boxes release cost, and the displayed prefix sums $(#prefix-sums.map(v => str(v)).join(", "))$ never exceed $K = #bound$.],
) <fig:seq-max-cumulative>
]
]
}

#problem-def("DirectedTwoCommodityIntegralFlow")[
Given a directed graph $G = (V, A)$ with arc capacities $c: A -> ZZ^+$, two source-sink pairs $(s_1, t_1)$ and $(s_2, t_2)$, and requirements $R_1, R_2 in ZZ^+$, determine whether there exist two integral flow functions $f_1, f_2: A -> ZZ_(>= 0)$ such that (1) $f_1(a) + f_2(a) <= c(a)$ for all $a in A$, (2) flow $f_i$ is conserved at every vertex except $s_1, s_2, t_1, t_2$, and (3) the net flow into $t_i$ under $f_i$ is at least $R_i$ for $i in {1, 2}$.
][
Expand Down
22 changes: 22 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,28 @@ @article{evenItaiShamir1976
doi = {10.1137/0205048}
}

@article{abdelWahabKameda1978,
author = {H. M. Abdel-Wahab and T. Kameda},
title = {Scheduling to Minimize Maximum Cumulative Cost Subject to Series-Parallel Precedence Constraints},
journal = {Operations Research},
volume = {26},
number = {1},
pages = {141--158},
year = {1978},
doi = {10.1287/opre.26.1.141}
}

@article{monmaSidney1979,
author = {Clyde L. Monma and Jeffrey B. Sidney},
title = {Sequencing with Series-Parallel Precedence Constraints},
journal = {Mathematics of Operations Research},
volume = {4},
number = {3},
pages = {215--224},
year = {1979},
doi = {10.1287/moor.4.3.215}
}

@article{glover2019,
author = {Fred Glover and Gary Kochenberger and Yu Du},
title = {Quantum Bridge Analytics {I}: a tutorial on formulating and using {QUBO} models},
Expand Down
4 changes: 4 additions & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ Flags by problem type:
FVS --arcs [--weights] [--num-vertices]
FlowShopScheduling --task-lengths, --deadline [--num-processors]
MinimumTardinessSequencing --n, --deadlines [--precedence-pairs]
SequencingToMinimizeMaximumCumulativeCost --costs, --bound [--precedence-pairs]
SCS --strings, --bound [--alphabet-size]
D2CIF --arcs, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2
ILP, CircuitSAT (via reduction only)
Expand Down Expand Up @@ -435,6 +436,9 @@ pub struct CreateArgs {
/// Input strings for LCS (e.g., "ABAC;BACA") or SCS (e.g., "0,1,2;1,2,0")
#[arg(long)]
pub strings: Option<String>,
/// Task costs for SequencingToMinimizeMaximumCumulativeCost (comma-separated, e.g., "2,-1,3,-2,1,-3")
#[arg(long, allow_hyphen_values = true)]
pub costs: Option<String>,
/// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0)
#[arg(long)]
pub arcs: Option<String>,
Expand Down
149 changes: 120 additions & 29 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ use problemreductions::models::graph::{
};
use problemreductions::models::misc::{
BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing,
PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence, SubsetSum,
PaintShop, SequencingToMinimizeMaximumCumulativeCost, SequencingWithinIntervals,
ShortestCommonSupersequence, SubsetSum,
};
use problemreductions::prelude::*;
use problemreductions::registry::collect_schemas;
Expand Down Expand Up @@ -76,6 +77,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.bound.is_none()
&& args.pattern.is_none()
&& args.strings.is_none()
&& args.costs.is_none()
&& args.arcs.is_none()
&& args.deadlines.is_none()
&& args.precedence_pairs.is_none()
Expand Down Expand Up @@ -185,6 +187,50 @@ fn resolve_rule_example(
})
}

fn parse_precedence_pairs(raw: Option<&str>) -> Result<Vec<(usize, usize)>> {
raw.filter(|s| !s.is_empty())
.map(|s| {
s.split(',')
.map(|pair| {
let pair = pair.trim();
let (pred, succ) = pair.split_once('>').ok_or_else(|| {
anyhow::anyhow!(
"Invalid --precedence-pairs value '{}': expected 'u>v'",
pair
)
})?;
let pred = pred.trim().parse::<usize>().map_err(|_| {
anyhow::anyhow!(
"Invalid --precedence-pairs value '{}': expected 'u>v' with nonnegative integer indices",
pair
)
})?;
let succ = succ.trim().parse::<usize>().map_err(|_| {
anyhow::anyhow!(
"Invalid --precedence-pairs value '{}': expected 'u>v' with nonnegative integer indices",
pair
)
})?;
Ok((pred, succ))
})
.collect()
})
.unwrap_or_else(|| Ok(vec![]))
}

fn validate_precedence_pairs(precedences: &[(usize, usize)], num_tasks: usize) -> Result<()> {
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
);
}
Ok(())
}

fn create_from_example(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
let example_spec = args
.example
Expand Down Expand Up @@ -233,7 +279,6 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str {
"Vec<Vec<usize>>" => "semicolon-separated groups: \"0,1;2,3\"",
"usize" => "integer",
"u64" => "integer",
"Vec<u64>" => "comma-separated integers: 0,0,5",
"i64" => "integer",
"BigUint" => "nonnegative decimal integer",
"Vec<BigUint>" => "comma-separated nonnegative decimal integers: 3,7,1,8",
Expand Down Expand Up @@ -296,6 +341,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11",
"SetBasis" => "--universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3",
"ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\" --bound 4",
"SequencingToMinimizeMaximumCumulativeCost" => {
"--costs 2,-1,3,-2,1,-3 --precedence-pairs \"0>2,1>2,1>3,2>4,3>5,4>5\" --bound 4"
}
_ => "",
}
}
Expand Down Expand Up @@ -340,6 +388,41 @@ fn parse_nonnegative_usize_bound(bound: i64, problem_name: &str, usage: &str) ->
.map_err(|_| anyhow::anyhow!("{problem_name} requires nonnegative --bound\n\n{usage}"))
}

fn validate_sequencing_within_intervals_inputs(
release_times: &[u64],
deadlines: &[u64],
lengths: &[u64],
usage: &str,
) -> Result<()> {
if release_times.len() != deadlines.len() {
bail!("release_times and deadlines must have the same length\n\n{usage}");
}
if release_times.len() != lengths.len() {
bail!("release_times and lengths must have the same length\n\n{usage}");
}

for (i, ((&release_time, &deadline), &length)) in release_times
.iter()
.zip(deadlines.iter())
.zip(lengths.iter())
.enumerate()
{
let end = release_time.checked_add(length).ok_or_else(|| {
anyhow::anyhow!("Task {i}: overflow computing r(i) + l(i)\n\n{usage}")
})?;
if end > deadline {
bail!(
"Task {i}: r({}) + l({}) > d({}), time window is empty\n\n{usage}",
release_time,
length,
deadline
);
}
}

Ok(())
}

fn print_problem_help(canonical: &str, graph_type: Option<&str>) -> Result<()> {
let is_geometry = matches!(
graph_type,
Expand Down Expand Up @@ -1175,39 +1258,14 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
})?;
let deadlines: Vec<usize> = util::parse_comma_list(deadlines_str)?;
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![],
};
let precedences = parse_precedence_pairs(args.precedence_pairs.as_deref())?;
anyhow::ensure!(
deadlines.len() == num_tasks,
"deadlines length ({}) must equal num_tasks ({})",
deadlines.len(),
num_tasks
);
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
);
}
validate_precedence_pairs(&precedences, num_tasks)?;
(
ser(MinimumTardinessSequencing::new(
num_tasks,
Expand All @@ -1218,6 +1276,33 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// SequencingToMinimizeMaximumCumulativeCost
"SequencingToMinimizeMaximumCumulativeCost" => {
let costs_str = args.costs.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"SequencingToMinimizeMaximumCumulativeCost requires --costs\n\n\
Usage: pred create SequencingToMinimizeMaximumCumulativeCost --costs 2,-1,3,-2,1,-3 --precedence-pairs \"0>2,1>2,1>3,2>4,3>5,4>5\" --bound 4"
)
})?;
let bound = args.bound.ok_or_else(|| {
anyhow::anyhow!(
"SequencingToMinimizeMaximumCumulativeCost requires --bound\n\n\
Usage: pred create SequencingToMinimizeMaximumCumulativeCost --costs 2,-1,3,-2,1,-3 --precedence-pairs \"0>2,1>2,1>3,2>4,3>5,4>5\" --bound 4"
)
})?;
let costs: Vec<i64> = util::parse_comma_list(costs_str)?;
let precedences = parse_precedence_pairs(args.precedence_pairs.as_deref())?;
validate_precedence_pairs(&precedences, costs.len())?;
(
ser(SequencingToMinimizeMaximumCumulativeCost::new(
costs,
precedences,
bound,
))?,
resolved_variant.clone(),
)
}

// SequencingWithinIntervals
"SequencingWithinIntervals" => {
let usage = "Usage: pred create SequencingWithinIntervals --release-times 0,0,5 --deadlines 11,11,6 --lengths 3,1,1";
Expand All @@ -1233,6 +1318,12 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
let release_times: Vec<u64> = util::parse_comma_list(rt_str)?;
let deadlines: Vec<u64> = util::parse_comma_list(dl_str)?;
let lengths: Vec<u64> = util::parse_comma_list(len_str)?;
validate_sequencing_within_intervals_inputs(
&release_times,
&deadlines,
&lengths,
usage,
)?;
(
ser(SequencingWithinIntervals::new(
release_times,
Expand Down
Loading
Loading