Skip to content

Commit 0592724

Browse files
GiggleLiuisPANNclaude
authored
Fix #500: [Model] SequencingToMinimizeMaximumCumulativeCost (#674)
* Add plan for #500: [Model] SequencingToMinimizeMaximumCumulativeCost * Implement #500: [Model] SequencingToMinimizeMaximumCumulativeCost * chore: remove plan file after implementation * fix: dedupe merged model fixtures * fix: harden sequencing model validation * fix: update paper Typst to use new example-db format after merge with main The paper's SequencingToMinimizeMaximumCumulativeCost section used the old x.samples / x.optimal format. Updated to x.optimal_config / x.optimal_value to match the current example-db JSON schema. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * revert: remove unrelated SequencingWithinIntervals changes from this PR Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Xiwei Pan <xiwei.pan@connect.hkust-gz.edu.cn> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c2450ab commit 0592724

10 files changed

Lines changed: 801 additions & 34 deletions

File tree

docs/paper/reductions.typ

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@
131131
"MultiprocessorScheduling": [Multiprocessor Scheduling],
132132
"PrecedenceConstrainedScheduling": [Precedence Constrained Scheduling],
133133
"MinimumTardinessSequencing": [Minimum Tardiness Sequencing],
134+
"SequencingToMinimizeMaximumCumulativeCost": [Sequencing to Minimize Maximum Cumulative Cost],
134135
"ConsecutiveOnesSubmatrix": [Consecutive Ones Submatrix],
135136
"SumOfSquaresPartition": [Sum of Squares Partition],
136137
"SequencingWithinIntervals": [Sequencing Within Intervals],
@@ -3498,6 +3499,80 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
34983499
]
34993500
}
35003501

3502+
#{
3503+
let x = load-model-example("SequencingToMinimizeMaximumCumulativeCost")
3504+
let costs = x.instance.costs
3505+
let precs = x.instance.precedences
3506+
let bound = x.instance.bound
3507+
let ntasks = costs.len()
3508+
let lehmer = x.optimal_config
3509+
let schedule = {
3510+
let avail = range(ntasks)
3511+
let result = ()
3512+
for c in lehmer {
3513+
result.push(avail.at(c))
3514+
avail = avail.enumerate().filter(((i, v)) => i != c).map(((i, v)) => v)
3515+
}
3516+
result
3517+
}
3518+
let prefix-sums = {
3519+
let running = 0
3520+
let result = ()
3521+
for task in schedule {
3522+
running += costs.at(task)
3523+
result.push(running)
3524+
}
3525+
result
3526+
}
3527+
[
3528+
#problem-def("SequencingToMinimizeMaximumCumulativeCost")[
3529+
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
3530+
$sum_(sigma(t') lt.eq sigma(t)) c(t') lt.eq K$
3531+
for every task $t in T$.
3532+
][
3533+
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.
3534+
3535+
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.
3536+
3537+
*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$.
3538+
3539+
#figure(
3540+
{
3541+
let pos = rgb("#f28e2b")
3542+
let neg = rgb("#76b7b2")
3543+
let zero = rgb("#bab0ab")
3544+
align(center, stack(dir: ttb, spacing: 0.35cm,
3545+
stack(dir: ltr, spacing: 0.08cm,
3546+
..schedule.enumerate().map(((i, task)) => {
3547+
let cost = costs.at(task)
3548+
let fill = if cost > 0 {
3549+
pos.transparentize(70%)
3550+
} else if cost < 0 {
3551+
neg.transparentize(65%)
3552+
} else {
3553+
zero.transparentize(65%)
3554+
}
3555+
stack(dir: ttb, spacing: 0.05cm,
3556+
box(width: 1.0cm, height: 0.6cm, fill: fill, stroke: 0.4pt + luma(120),
3557+
align(center + horizon, text(8pt, weight: "bold")[$t_#(task + 1)$])),
3558+
text(6pt, if cost >= 0 { $+ #cost$ } else { $#cost$ }),
3559+
)
3560+
}),
3561+
),
3562+
stack(dir: ltr, spacing: 0.08cm,
3563+
..prefix-sums.map(v => {
3564+
box(width: 1.0cm, align(center + horizon, text(7pt)[$#v$]))
3565+
}),
3566+
),
3567+
text(7pt, [prefix sums after each scheduled task]),
3568+
))
3569+
},
3570+
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$.],
3571+
) <fig:seq-max-cumulative>
3572+
]
3573+
]
3574+
}
3575+
35013576
#problem-def("DirectedTwoCommodityIntegralFlow")[
35023577
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}$.
35033578
][

docs/paper/references.bib

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,28 @@ @article{evenItaiShamir1976
137137
doi = {10.1137/0205048}
138138
}
139139

140+
@article{abdelWahabKameda1978,
141+
author = {H. M. Abdel-Wahab and T. Kameda},
142+
title = {Scheduling to Minimize Maximum Cumulative Cost Subject to Series-Parallel Precedence Constraints},
143+
journal = {Operations Research},
144+
volume = {26},
145+
number = {1},
146+
pages = {141--158},
147+
year = {1978},
148+
doi = {10.1287/opre.26.1.141}
149+
}
150+
151+
@article{monmaSidney1979,
152+
author = {Clyde L. Monma and Jeffrey B. Sidney},
153+
title = {Sequencing with Series-Parallel Precedence Constraints},
154+
journal = {Mathematics of Operations Research},
155+
volume = {4},
156+
number = {3},
157+
pages = {215--224},
158+
year = {1979},
159+
doi = {10.1287/moor.4.3.215}
160+
}
161+
140162
@article{evenTarjan1976,
141163
author = {Shimon Even and Robert Endre Tarjan},
142164
title = {A Combinatorial Problem Which Is Complete in Polynomial Space},

problemreductions-cli/src/cli.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ Flags by problem type:
269269
FlowShopScheduling --task-lengths, --deadline [--num-processors]
270270
StaffScheduling --schedules, --requirements, --num-workers, --k
271271
MinimumTardinessSequencing --n, --deadlines [--precedence-pairs]
272+
SequencingToMinimizeMaximumCumulativeCost --costs, --bound [--precedence-pairs]
272273
RectilinearPictureCompression --matrix (0/1), --k
273274
SCS --strings, --bound [--alphabet-size]
274275
StringToStringCorrection --source-string, --target-string, --bound [--alphabet-size]
@@ -476,6 +477,9 @@ pub struct CreateArgs {
476477
/// 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")
477478
#[arg(long)]
478479
pub strings: Option<String>,
480+
/// Task costs for SequencingToMinimizeMaximumCumulativeCost (comma-separated, e.g., "2,-1,3,-2,1,-3")
481+
#[arg(long, allow_hyphen_values = true)]
482+
pub costs: Option<String>,
479483
/// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0)
480484
#[arg(long)]
481485
pub arcs: Option<String>,

problemreductions-cli/src/commands/create.rs

Lines changed: 124 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ use problemreductions::models::misc::{
1717
BinPacking, CbqRelation, ConjunctiveBooleanQuery, FlowShopScheduling, LongestCommonSubsequence,
1818
MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, PartiallyOrderedKnapsack,
1919
QueryArg, RectilinearPictureCompression, ResourceConstrainedScheduling,
20-
SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence,
21-
StringToStringCorrection, SubsetSum, SumOfSquaresPartition,
20+
SequencingToMinimizeMaximumCumulativeCost, SequencingWithReleaseTimesAndDeadlines,
21+
SequencingWithinIntervals, ShortestCommonSupersequence, StringToStringCorrection, SubsetSum,
22+
SumOfSquaresPartition,
2223
};
2324
use problemreductions::models::BiconnectivityAugmentation;
2425
use problemreductions::prelude::*;
@@ -85,6 +86,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
8586
&& args.bound.is_none()
8687
&& args.pattern.is_none()
8788
&& args.strings.is_none()
89+
&& args.costs.is_none()
8890
&& args.arcs.is_none()
8991
&& args.source.is_none()
9092
&& args.sink.is_none()
@@ -218,6 +220,50 @@ fn resolve_rule_example(
218220
})
219221
}
220222

223+
fn parse_precedence_pairs(raw: Option<&str>) -> Result<Vec<(usize, usize)>> {
224+
raw.filter(|s| !s.is_empty())
225+
.map(|s| {
226+
s.split(',')
227+
.map(|pair| {
228+
let pair = pair.trim();
229+
let (pred, succ) = pair.split_once('>').ok_or_else(|| {
230+
anyhow::anyhow!(
231+
"Invalid --precedence-pairs value '{}': expected 'u>v'",
232+
pair
233+
)
234+
})?;
235+
let pred = pred.trim().parse::<usize>().map_err(|_| {
236+
anyhow::anyhow!(
237+
"Invalid --precedence-pairs value '{}': expected 'u>v' with nonnegative integer indices",
238+
pair
239+
)
240+
})?;
241+
let succ = succ.trim().parse::<usize>().map_err(|_| {
242+
anyhow::anyhow!(
243+
"Invalid --precedence-pairs value '{}': expected 'u>v' with nonnegative integer indices",
244+
pair
245+
)
246+
})?;
247+
Ok((pred, succ))
248+
})
249+
.collect()
250+
})
251+
.unwrap_or_else(|| Ok(vec![]))
252+
}
253+
254+
fn validate_precedence_pairs(precedences: &[(usize, usize)], num_tasks: usize) -> Result<()> {
255+
for &(pred, succ) in precedences {
256+
anyhow::ensure!(
257+
pred < num_tasks && succ < num_tasks,
258+
"precedence index out of range: ({}, {}) but num_tasks = {}",
259+
pred,
260+
succ,
261+
num_tasks
262+
);
263+
}
264+
Ok(())
265+
}
266+
221267
fn create_from_example(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
222268
let example_spec = args
223269
.example
@@ -371,6 +417,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
371417
"--num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --k 2"
372418
}
373419
"ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\" --bound 4",
420+
"SequencingToMinimizeMaximumCumulativeCost" => {
421+
"--costs 2,-1,3,-2,1,-3 --precedence-pairs \"0>2,1>2,1>3,2>4,3>5,4>5\" --bound 4"
422+
}
374423
"ConjunctiveQueryFoldability" => "(use --example ConjunctiveQueryFoldability)",
375424
"ConjunctiveBooleanQuery" => {
376425
"--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\""
@@ -470,6 +519,41 @@ fn parse_nonnegative_usize_bound(bound: i64, problem_name: &str, usage: &str) ->
470519
.map_err(|_| anyhow::anyhow!("{problem_name} requires nonnegative --bound\n\n{usage}"))
471520
}
472521

522+
fn validate_sequencing_within_intervals_inputs(
523+
release_times: &[u64],
524+
deadlines: &[u64],
525+
lengths: &[u64],
526+
usage: &str,
527+
) -> Result<()> {
528+
if release_times.len() != deadlines.len() {
529+
bail!("release_times and deadlines must have the same length\n\n{usage}");
530+
}
531+
if release_times.len() != lengths.len() {
532+
bail!("release_times and lengths must have the same length\n\n{usage}");
533+
}
534+
535+
for (i, ((&release_time, &deadline), &length)) in release_times
536+
.iter()
537+
.zip(deadlines.iter())
538+
.zip(lengths.iter())
539+
.enumerate()
540+
{
541+
let end = release_time.checked_add(length).ok_or_else(|| {
542+
anyhow::anyhow!("Task {i}: overflow computing r(i) + l(i)\n\n{usage}")
543+
})?;
544+
if end > deadline {
545+
bail!(
546+
"Task {i}: r({}) + l({}) > d({}), time window is empty\n\n{usage}",
547+
release_time,
548+
length,
549+
deadline
550+
);
551+
}
552+
}
553+
554+
Ok(())
555+
}
556+
473557
fn print_problem_help(canonical: &str, graph_type: Option<&str>) -> Result<()> {
474558
let is_geometry = matches!(
475559
graph_type,
@@ -1800,39 +1884,14 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
18001884
)
18011885
})?;
18021886
let deadlines: Vec<usize> = util::parse_comma_list(deadlines_str)?;
1803-
let precedences: Vec<(usize, usize)> = match args.precedence_pairs.as_deref() {
1804-
Some(s) if !s.is_empty() => s
1805-
.split(',')
1806-
.map(|pair| {
1807-
let parts: Vec<&str> = pair.trim().split('>').collect();
1808-
anyhow::ensure!(
1809-
parts.len() == 2,
1810-
"Invalid precedence format '{}', expected 'u>v'",
1811-
pair.trim()
1812-
);
1813-
Ok((
1814-
parts[0].trim().parse::<usize>()?,
1815-
parts[1].trim().parse::<usize>()?,
1816-
))
1817-
})
1818-
.collect::<Result<Vec<_>>>()?,
1819-
_ => vec![],
1820-
};
1887+
let precedences = parse_precedence_pairs(args.precedence_pairs.as_deref())?;
18211888
anyhow::ensure!(
18221889
deadlines.len() == num_tasks,
18231890
"deadlines length ({}) must equal num_tasks ({})",
18241891
deadlines.len(),
18251892
num_tasks
18261893
);
1827-
for &(pred, succ) in &precedences {
1828-
anyhow::ensure!(
1829-
pred < num_tasks && succ < num_tasks,
1830-
"precedence index out of range: ({}, {}) but num_tasks = {}",
1831-
pred,
1832-
succ,
1833-
num_tasks
1834-
);
1835-
}
1894+
validate_precedence_pairs(&precedences, num_tasks)?;
18361895
(
18371896
ser(MinimumTardinessSequencing::new(
18381897
num_tasks,
@@ -1843,6 +1902,33 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
18431902
)
18441903
}
18451904

1905+
// SequencingToMinimizeMaximumCumulativeCost
1906+
"SequencingToMinimizeMaximumCumulativeCost" => {
1907+
let costs_str = args.costs.as_deref().ok_or_else(|| {
1908+
anyhow::anyhow!(
1909+
"SequencingToMinimizeMaximumCumulativeCost requires --costs\n\n\
1910+
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"
1911+
)
1912+
})?;
1913+
let bound = args.bound.ok_or_else(|| {
1914+
anyhow::anyhow!(
1915+
"SequencingToMinimizeMaximumCumulativeCost requires --bound\n\n\
1916+
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"
1917+
)
1918+
})?;
1919+
let costs: Vec<i64> = util::parse_comma_list(costs_str)?;
1920+
let precedences = parse_precedence_pairs(args.precedence_pairs.as_deref())?;
1921+
validate_precedence_pairs(&precedences, costs.len())?;
1922+
(
1923+
ser(SequencingToMinimizeMaximumCumulativeCost::new(
1924+
costs,
1925+
precedences,
1926+
bound,
1927+
))?,
1928+
resolved_variant.clone(),
1929+
)
1930+
}
1931+
18461932
// SequencingWithinIntervals
18471933
"SequencingWithinIntervals" => {
18481934
let usage = "Usage: pred create SequencingWithinIntervals --release-times 0,0,5 --deadlines 11,11,6 --lengths 3,1,1";
@@ -1858,6 +1944,12 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
18581944
let release_times: Vec<u64> = util::parse_comma_list(rt_str)?;
18591945
let deadlines: Vec<u64> = util::parse_comma_list(dl_str)?;
18601946
let lengths: Vec<u64> = util::parse_comma_list(len_str)?;
1947+
validate_sequencing_within_intervals_inputs(
1948+
&release_times,
1949+
&deadlines,
1950+
&lengths,
1951+
usage,
1952+
)?;
18611953
(
18621954
ser(SequencingWithinIntervals::new(
18631955
release_times,
@@ -4266,6 +4358,9 @@ mod tests {
42664358
domain_size: None,
42674359
relations: None,
42684360
conjuncts_spec: None,
4361+
costs: None,
4362+
cut_bound: None,
4363+
size_bound: None,
42694364
}
42704365
}
42714366

0 commit comments

Comments
 (0)