Skip to content
Merged
75 changes: 75 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
"MultiprocessorScheduling": [Multiprocessor Scheduling],
"PrecedenceConstrainedScheduling": [Precedence Constrained Scheduling],
"MinimumTardinessSequencing": [Minimum Tardiness Sequencing],
"SequencingToMinimizeMaximumCumulativeCost": [Sequencing to Minimize Maximum Cumulative Cost],
"ConsecutiveOnesSubmatrix": [Consecutive Ones Submatrix],
"SumOfSquaresPartition": [Sum of Squares Partition],
"SequencingWithinIntervals": [Sequencing Within Intervals],
Expand Down Expand Up @@ -3498,6 +3499,80 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
]
}

#{
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 lehmer = x.optimal_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$.

#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 @@ -137,6 +137,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{evenTarjan1976,
author = {Shimon Even and Robert Endre Tarjan},
title = {A Combinatorial Problem Which Is Complete in Polynomial Space},
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 @@ -269,6 +269,7 @@ Flags by problem type:
FlowShopScheduling --task-lengths, --deadline [--num-processors]
StaffScheduling --schedules, --requirements, --num-workers, --k
MinimumTardinessSequencing --n, --deadlines [--precedence-pairs]
SequencingToMinimizeMaximumCumulativeCost --costs, --bound [--precedence-pairs]
RectilinearPictureCompression --matrix (0/1), --k
SCS --strings, --bound [--alphabet-size]
StringToStringCorrection --source-string, --target-string, --bound [--alphabet-size]
Expand Down Expand Up @@ -476,6 +477,9 @@ pub struct CreateArgs {
/// Input strings for LCS (e.g., "ABAC;BACA" or "0,1,0;1,0,1") 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
153 changes: 124 additions & 29 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ use problemreductions::models::misc::{
BinPacking, CbqRelation, ConjunctiveBooleanQuery, FlowShopScheduling, LongestCommonSubsequence,
MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, PartiallyOrderedKnapsack,
QueryArg, RectilinearPictureCompression, ResourceConstrainedScheduling,
SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence,
StringToStringCorrection, SubsetSum, SumOfSquaresPartition,
SequencingToMinimizeMaximumCumulativeCost, SequencingWithReleaseTimesAndDeadlines,
SequencingWithinIntervals, ShortestCommonSupersequence, StringToStringCorrection, SubsetSum,
SumOfSquaresPartition,
};
use problemreductions::models::BiconnectivityAugmentation;
use problemreductions::prelude::*;
Expand Down Expand Up @@ -85,6 +86,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.source.is_none()
&& args.sink.is_none()
Expand Down Expand Up @@ -218,6 +220,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 @@ -371,6 +417,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"--num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --k 2"
}
"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"
}
"ConjunctiveQueryFoldability" => "(use --example ConjunctiveQueryFoldability)",
"ConjunctiveBooleanQuery" => {
"--domain-size 6 --relations \"2:0,3|1,3|2,4;3:0,1,5|1,2,5\" --conjuncts-spec \"0:v0,c3;0:v1,c3;1:v0,v1,c5\""
Expand Down Expand Up @@ -470,6 +519,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 @@ -1800,39 +1884,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 @@ -1843,6 +1902,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 @@ -1858,6 +1944,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 Expand Up @@ -4266,6 +4358,9 @@ mod tests {
domain_size: None,
relations: None,
conjuncts_spec: None,
costs: None,
cut_bound: None,
size_bound: None,
}
}

Expand Down
Loading
Loading