From d971d919eec934b0795ada20438a7a2c82648b24 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Thu, 19 Mar 2026 18:24:17 +0800 Subject: [PATCH 1/3] Fix #701: Add find_all_satisfying tests and register all canonical examples - Add find_all_satisfying + empty tests for 5 satisfaction models: SequencingWithinIntervals (41 solutions), ShortestCommonSupersequence (42), MultiprocessorScheduling (2), FlowShopScheduling (99), RuralPostman (1) - Update canonical examples for SequencingWithinIntervals and ShortestCommonSupersequence to use natural instances matching their issues - Register canonical_model_example_specs for 7 missing models: BinPacking, GraphPartitioning, Knapsack, MinimumFeedbackArcSet, OptimalLinearArrangement, SubgraphIsomorphism, SubsetSum (70/70 now) - Convert all 7 paper entries from hardcoded to data-driven load-model-example Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/reductions.typ | 603 +++++++++++------- src/models/graph/graph_partitioning.rs | 25 + src/models/graph/minimum_feedback_arc_set.rs | 15 + src/models/graph/mod.rs | 5 + .../graph/optimal_linear_arrangement.rs | 19 + src/models/graph/rural_postman.rs | 31 + src/models/graph/subgraph_isomorphism.rs | 15 + src/models/misc/bin_packing.rs | 11 + src/models/misc/flow_shop_scheduling.rs | 21 + src/models/misc/knapsack.rs | 12 + src/models/misc/mod.rs | 4 + .../misc/sequencing_within_intervals.rs | 8 +- .../misc/shortest_common_supersequence.rs | 6 +- src/models/misc/subset_sum.rs | 11 + src/models/mod.rs | 6 +- src/unit_tests/models/graph/rural_postman.rs | 33 + .../models/misc/flow_shop_scheduling.rs | 36 ++ .../models/misc/multiprocessor_scheduling.rs | 25 + .../misc/sequencing_within_intervals.rs | 92 ++- .../misc/shortest_common_supersequence.rs | 39 ++ 20 files changed, 739 insertions(+), 278 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 930aef48a..0169f8aba 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -508,57 +508,64 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ] ] } -#problem-def("GraphPartitioning")[ - Given an undirected graph $G = (V, E)$ with $|V| = n$ (even), find a partition of $V$ into two disjoint sets $A$ and $B$ with $|A| = |B| = n slash 2$ that minimizes the number of edges crossing the partition: - $ "cut"(A, B) = |{(u, v) in E : u in A, v in B}|. $ -][ -Graph Partitioning is a core NP-hard problem arising in VLSI design, parallel computing, and scientific simulation, where balanced workload distribution with minimal communication is essential. Closely related to Max-Cut (which _maximizes_ rather than _minimizes_ the cut) and to the Ising Spin Glass model. NP-completeness was proved by Garey, Johnson and Stockmeyer @garey1976. Arora, Rao and Vazirani @arora2009 gave an $O(sqrt(log n))$-approximation algorithm. The best known unconditional exact algorithm is brute-force enumeration of all $binom(n, n slash 2) = O^*(2^n)$ balanced partitions; no faster worst-case algorithm is known. Cygan et al. @cygan2014 showed that Minimum Bisection is fixed-parameter tractable in $O(2^(O(k^3)) dot n^3 log^3 n)$ time parameterized by bisection width $k$. Standard partitioning tools include METIS, KaHIP, and Scotch. +#{ + let x = load-model-example("GraphPartitioning") + let nv = graph-num-vertices(x.instance) + let ne = graph-num-edges(x.instance) + let edges = x.instance.graph.edges.map(e => (e.at(0), e.at(1))) + let config = x.optimal_config + let cut-val = x.optimal_value.Valid + let side-a = range(nv).filter(i => config.at(i) == 0) + let side-b = range(nv).filter(i => config.at(i) == 1) + let cut-edges = edges.filter(e => config.at(e.at(0)) != config.at(e.at(1))) + [ + #problem-def("GraphPartitioning")[ + Given an undirected graph $G = (V, E)$ with $|V| = n$ (even), find a partition of $V$ into two disjoint sets $A$ and $B$ with $|A| = |B| = n slash 2$ that minimizes the number of edges crossing the partition: + $ "cut"(A, B) = |{(u, v) in E : u in A, v in B}|. $ + ][ + Graph Partitioning is a core NP-hard problem arising in VLSI design, parallel computing, and scientific simulation, where balanced workload distribution with minimal communication is essential. Closely related to Max-Cut (which _maximizes_ rather than _minimizes_ the cut) and to the Ising Spin Glass model. NP-completeness was proved by Garey, Johnson and Stockmeyer @garey1976. Arora, Rao and Vazirani @arora2009 gave an $O(sqrt(log n))$-approximation algorithm. The best known unconditional exact algorithm is brute-force enumeration of all $binom(n, n slash 2) = O^*(2^n)$ balanced partitions; no faster worst-case algorithm is known. Cygan et al. @cygan2014 showed that Minimum Bisection is fixed-parameter tractable in $O(2^(O(k^3)) dot n^3 log^3 n)$ time parameterized by bisection width $k$. Standard partitioning tools include METIS, KaHIP, and Scotch. -*Example.* Consider the graph $G$ with $n = 6$ vertices and 9 edges: $(v_0, v_1)$, $(v_0, v_2)$, $(v_1, v_2)$, $(v_1, v_3)$, $(v_2, v_3)$, $(v_2, v_4)$, $(v_3, v_4)$, $(v_3, v_5)$, $(v_4, v_5)$. The optimal balanced partition is $A = {v_0, v_1, v_2}$, $B = {v_3, v_4, v_5}$, with cut value 3: the crossing edges are $(v_1, v_3)$, $(v_2, v_3)$, $(v_2, v_4)$. All other balanced partitions yield a cut of at least 3. + *Example.* Consider the graph $G$ with $n = #nv$ vertices and #ne edges. The optimal balanced partition is $A = {#side-a.map(i => $v_#i$).join($,$)}$, $B = {#side-b.map(i => $v_#i$).join($,$)}$, with cut value #cut-val. -#figure( - canvas(length: 1cm, { - // 6-vertex layout: two columns of 3 - let verts = ( - (0, 2), // v0: top-left - (0, 1), // v1: mid-left - (0, 0), // v2: bottom-left - (2.5, 2), // v3: top-right - (2.5, 1), // v4: mid-right - (2.5, 0), // v5: bottom-right - ) - let edges = ((0,1),(0,2),(1,2),(1,3),(2,3),(2,4),(3,4),(3,5),(4,5)) - let side-a = (0, 1, 2) - let cut-edges = edges.filter(e => side-a.contains(e.at(0)) != side-a.contains(e.at(1))) - // Draw edges - for (u, v) in edges { - let crossing = cut-edges.any(e => (e.at(0) == u and e.at(1) == v) or (e.at(0) == v and e.at(1) == u)) - g-edge(verts.at(u), verts.at(v), - stroke: if crossing { 2pt + graph-colors.at(1) } else { 1pt + luma(180) }) - } - // Draw partition regions - import draw: * - on-layer(-1, { - rect((-0.5, -0.5), (0.5, 2.5), - fill: graph-colors.at(0).transparentize(90%), - stroke: (dash: "dashed", paint: graph-colors.at(0), thickness: 0.8pt)) - content((0, 2.8), text(8pt, fill: graph-colors.at(0))[$A$]) - rect((2.0, -0.5), (3.0, 2.5), - fill: graph-colors.at(1).transparentize(90%), - stroke: (dash: "dashed", paint: graph-colors.at(1), thickness: 0.8pt)) - content((2.5, 2.8), text(8pt, fill: graph-colors.at(1))[$B$]) - }) - // Draw nodes - for (k, pos) in verts.enumerate() { - let in-a = side-a.contains(k) - g-node(pos, name: "v" + str(k), - fill: if in-a { graph-colors.at(0) } else { graph-colors.at(1) }, - label: text(fill: white)[$v_#k$]) - } - }), - caption: [Graph with $n = 6$ vertices partitioned into $A = {v_0, v_1, v_2}$ (blue) and $B = {v_3, v_4, v_5}$ (red). The 3 crossing edges $(v_1, v_3)$, $(v_2, v_3)$, $(v_2, v_4)$ are shown in bold red; internal edges are gray.], -) -] + #figure( + canvas(length: 1cm, { + // Two-column layout for balanced partition + let half = int(nv / 2) + let verts = ( + ..range(half).map(i => (0, (half - 1 - i))), + ..range(half).map(i => (2.5, (half - 1 - i))), + ) + // Draw edges + for (u, v) in edges { + let crossing = config.at(u) != config.at(v) + g-edge(verts.at(u), verts.at(v), + stroke: if crossing { 2pt + graph-colors.at(1) } else { 1pt + luma(180) }) + } + // Draw partition regions + import draw: * + on-layer(-1, { + rect((-0.5, -0.5), (0.5, half - 0.5), + fill: graph-colors.at(0).transparentize(90%), + stroke: (dash: "dashed", paint: graph-colors.at(0), thickness: 0.8pt)) + content((0, half - 0.2), text(8pt, fill: graph-colors.at(0))[$A$]) + rect((2.0, -0.5), (3.0, half - 0.5), + fill: graph-colors.at(1).transparentize(90%), + stroke: (dash: "dashed", paint: graph-colors.at(1), thickness: 0.8pt)) + content((2.5, half - 0.2), text(8pt, fill: graph-colors.at(1))[$B$]) + }) + // Draw nodes + for (k, pos) in verts.enumerate() { + let in-a = config.at(k) == 0 + g-node(pos, name: "v" + str(k), + fill: if in-a { graph-colors.at(0) } else { graph-colors.at(1) }, + label: text(fill: white)[$v_#k$]) + } + }), + caption: [Graph with $n = #nv$ vertices partitioned into $A$ (blue) and $B$ (red). The #cut-val crossing edges are shown in bold red; internal edges are gray.], + ) + ] + ] +} #problem-def("BiconnectivityAugmentation")[ Given an undirected graph $G = (V, E)$, a set $F$ of candidate edges on $V$ with $F inter E = emptyset$, weights $w: F -> RR$, and a budget $B in RR$, find $F' subset.eq F$ such that $sum_(e in F') w(e) <= B$ and the augmented graph $G' = (V, E union F')$ is biconnected, meaning $G'$ is connected and deleting any single vertex leaves it connected. ][ @@ -1304,15 +1311,27 @@ is feasible: each set induces a connected subgraph, the component weights are $2 ] ] } -#problem-def("OptimalLinearArrangement")[ - Given an undirected graph $G=(V,E)$ and a non-negative integer $K$, is there a bijection $f: V -> {0, 1, dots, |V|-1}$ such that $sum_({u,v} in E) |f(u) - f(v)| <= K$? -][ -A classical NP-complete decision problem from Garey & Johnson (GT42) @garey1979, with applications in VLSI design, graph drawing, and sparse matrix reordering. The problem asks whether vertices can be placed on a line so that the total "stretch" of all edges is at most $K$. +#{ + let x = load-model-example("OptimalLinearArrangement") + let nv = graph-num-vertices(x.instance) + let ne = graph-num-edges(x.instance) + let edges = x.instance.graph.edges.map(e => (e.at(0), e.at(1))) + let K = x.instance.bound + let config = x.optimal_config + // Compute total cost + let total-cost = edges.map(e => calc.abs(config.at(e.at(0)) - config.at(e.at(1)))).sum() + [ + #problem-def("OptimalLinearArrangement")[ + Given an undirected graph $G=(V,E)$ and a non-negative integer $K$, is there a bijection $f: V -> {0, 1, dots, |V|-1}$ such that $sum_({u,v} in E) |f(u) - f(v)| <= K$? + ][ + A classical NP-complete decision problem from Garey & Johnson (GT42) @garey1979, with applications in VLSI design, graph drawing, and sparse matrix reordering. The problem asks whether vertices can be placed on a line so that the total "stretch" of all edges is at most $K$. -NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonStockmeyer1976, via reduction from Simple Max Cut. The problem remains NP-complete on bipartite graphs, but is solvable in polynomial time on trees. The best known exact algorithm for general graphs uses dynamic programming over subsets in $O^*(2^n)$ time and space (Held-Karp style), analogous to TSP. + NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonStockmeyer1976, via reduction from Simple Max Cut. The problem remains NP-complete on bipartite graphs, but is solvable in polynomial time on trees. The best known exact algorithm for general graphs uses dynamic programming over subsets in $O^*(2^n)$ time and space (Held-Karp style), analogous to TSP. -*Example.* Consider the path graph $P_3$: vertices ${v_0, v_1, v_2}$ with edges ${v_0, v_1}$ and ${v_1, v_2}$. The identity arrangement $f(v_i) = i$ gives cost $|0-1| + |1-2| = 2$. With bound $K = 2$, this is a YES instance. For a triangle $K_3$ with the same vertex set plus edge ${v_0, v_2}$, any arrangement yields cost 4, so a bound of $K = 3$ gives a NO instance. -] + *Example.* Consider a graph with #nv vertices and #ne edges, with bound $K = #K$. The arrangement $f = (#config.map(c => str(c)).join(", "))$ gives total cost $#edges.map(e => $|#config.at(e.at(0)) - #config.at(e.at(1))|$).join($+$) = #total-cost lt.eq #K$, so this is a YES instance. + ] + ] +} #{ let x = load-model-example("MaximumClique") let nv = graph-num-vertices(x.instance) @@ -2432,53 +2451,78 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ] } -#problem-def("BinPacking")[ - Given $n$ items with sizes $s_1, dots, s_n in RR^+$ and bin capacity $C > 0$, find an assignment $x: {1, dots, n} -> NN$ minimizing $|{x(i) : i = 1, dots, n}|$ (the number of distinct bins used) subject to $forall j: sum_(i: x(i) = j) s_i lt.eq C$. -][ - Bin Packing is one of the classical NP-hard optimization problems @garey1979, with applications in logistics, cutting stock, and cloud resource allocation. The best known exact algorithm runs in $O^*(2^n)$ time via inclusion-exclusion over set partitions @bjorklund2009. +#{ + let x = load-model-example("BinPacking") + let sizes = x.instance.sizes + let n = sizes.len() + let C = x.instance.capacity + let config = x.optimal_config + let num-bins = x.optimal_value.Valid + // Group items by bin + let bins-contents = range(num-bins).map(b => + range(n).filter(i => config.at(i) == b) + ) + let bin-loads = bins-contents.map(items => items.map(i => sizes.at(i)).sum()) + [ + #problem-def("BinPacking")[ + Given $n$ items with sizes $s_1, dots, s_n in RR^+$ and bin capacity $C > 0$, find an assignment $x: {1, dots, n} -> NN$ minimizing $|{x(i) : i = 1, dots, n}|$ (the number of distinct bins used) subject to $forall j: sum_(i: x(i) = j) s_i lt.eq C$. + ][ + Bin Packing is one of the classical NP-hard optimization problems @garey1979, with applications in logistics, cutting stock, and cloud resource allocation. The best known exact algorithm runs in $O^*(2^n)$ time via inclusion-exclusion over set partitions @bjorklund2009. - *Example.* Consider $n = 6$ items with sizes $(6, 6, 5, 5, 4, 4)$ and capacity $C = 10$. The lower bound is $ceil(30 slash 10) = 3$ bins. An optimal packing uses exactly 3 bins: $B_1 = {6, 4}$, $B_2 = {6, 4}$, $B_3 = {5, 5}$, each with total load $10 = C$. + *Example.* Consider $n = #n$ items with sizes $(#sizes.map(s => str(s)).join(", "))$ and capacity $C = #C$. An optimal packing uses #num-bins bins. - #figure({ - canvas(length: 1cm, { - let s = 0.28 - let w = 1.0 - let gap = 0.6 - let bins = ((6, 4), (6, 4), (5, 5)) - let fills = ( - (graph-colors.at(0), graph-colors.at(1)), - (graph-colors.at(0), graph-colors.at(1)), - (graph-colors.at(2), graph-colors.at(2)), - ) - for i in range(3) { - let x = i * (w + gap) - draw.rect((x, 0), (x + w, 10 * s), stroke: 0.8pt + black) - let y = 0 - for j in range(bins.at(i).len()) { - let sz = bins.at(i).at(j) - let c = fills.at(i).at(j) - draw.rect((x, y), (x + w, y + sz * s), stroke: 0.4pt, fill: c) - draw.content((x + w / 2, y + sz * s / 2), text(8pt, fill: white)[#sz]) - y += sz * s - } - draw.content((x + w / 2, -0.3), text(8pt)[$B_#(i + 1)$]) - } - draw.line((-0.15, 10 * s), (2 * (w + gap) + w + 0.15, 10 * s), - stroke: (dash: "dashed", paint: luma(150), thickness: 0.5pt)) - draw.content((-0.5, 10 * s), text(7pt)[$C$]) - }) - }, - caption: [Optimal packing of items with sizes $(6, 6, 5, 5, 4, 4)$ into 3 bins of capacity $C = 10$. Numbers indicate item sizes; all bins are fully utilized.], - ) -] + #figure({ + canvas(length: 1cm, { + let s = 0.35 + let w = 1.0 + let gap = 0.6 + let item-colors = (rgb("#4e79a7"), rgb("#e15759"), rgb("#76b7b2"), rgb("#f28e2b"), rgb("#59a14f"), rgb("#b07aa1")) + for bi in range(num-bins) { + let bx = bi * (w + gap) + draw.rect((bx, 0), (bx + w, C * s), stroke: 0.8pt + black) + let y = 0 + for item-idx in bins-contents.at(bi) { + let sz = sizes.at(item-idx) + let c = item-colors.at(calc.rem(item-idx, item-colors.len())) + draw.rect((bx, y), (bx + w, y + sz * s), stroke: 0.4pt, fill: c) + draw.content((bx + w / 2, y + sz * s / 2), text(8pt, fill: white)[#sz]) + y += sz * s + } + draw.content((bx + w / 2, -0.3), text(8pt)[$B_#(bi + 1)$]) + } + let total-w = (num-bins - 1) * (w + gap) + w + draw.line((-0.15, C * s), (total-w + 0.15, C * s), + stroke: (dash: "dashed", paint: luma(150), thickness: 0.5pt)) + draw.content((-0.5, C * s), text(7pt)[$C$]) + }) + }, + caption: [Optimal packing of #n items into #num-bins bins of capacity $C = #C$. Numbers indicate item sizes.], + ) + ] + ] +} -#problem-def("Knapsack")[ - Given $n$ items with weights $w_0, dots, w_(n-1) in NN$ and values $v_0, dots, v_(n-1) in NN$, and a capacity $C in NN$, find $S subset.eq {0, dots, n - 1}$ maximizing $sum_(i in S) v_i$ subject to $sum_(i in S) w_i lt.eq C$. -][ - One of Karp's 21 NP-complete problems @karp1972. Knapsack is only _weakly_ NP-hard: a classical dynamic-programming algorithm runs in $O(n C)$ pseudo-polynomial time, and a fully polynomial-time approximation scheme (FPTAS) achieves $(1 - epsilon)$-optimal value in $O(n^2 slash epsilon)$ time @ibarra1975. The special case $v_i = w_i$ for all $i$ is the Subset Sum problem. Knapsack is also a special case of Integer Linear Programming with a single constraint. The best known exact algorithm is the $O^*(2^(n slash 2))$ meet-in-the-middle approach of Horowitz and Sahni @horowitz1974, which partitions items into two halves and combines sorted sublists. +#{ + let x = load-model-example("Knapsack") + let weights = x.instance.weights + let values = x.instance.values + let C = x.instance.capacity + let n = weights.len() + let config = x.optimal_config + let opt-val = x.optimal_value.Valid + let selected = range(n).filter(i => config.at(i) == 1) + let total-w = selected.map(i => weights.at(i)).sum() + let total-v = selected.map(i => values.at(i)).sum() + [ + #problem-def("Knapsack")[ + Given $n$ items with weights $w_0, dots, w_(n-1) in NN$ and values $v_0, dots, v_(n-1) in NN$, and a capacity $C in NN$, find $S subset.eq {0, dots, n - 1}$ maximizing $sum_(i in S) v_i$ subject to $sum_(i in S) w_i lt.eq C$. + ][ + One of Karp's 21 NP-complete problems @karp1972. Knapsack is only _weakly_ NP-hard: a classical dynamic-programming algorithm runs in $O(n C)$ pseudo-polynomial time, and a fully polynomial-time approximation scheme (FPTAS) achieves $(1 - epsilon)$-optimal value in $O(n^2 slash epsilon)$ time @ibarra1975. The special case $v_i = w_i$ for all $i$ is the Subset Sum problem. Knapsack is also a special case of Integer Linear Programming with a single constraint. The best known exact algorithm is the $O^*(2^(n slash 2))$ meet-in-the-middle approach of Horowitz and Sahni @horowitz1974, which partitions items into two halves and combines sorted sublists. - *Example.* Let $n = 4$ items with weights $(2, 3, 4, 5)$, values $(3, 4, 5, 7)$, and capacity $C = 7$. Selecting $S = {1, 2}$ (items with weights 3 and 4) gives total weight $3 + 4 = 7 lt.eq C$ and total value $4 + 5 = 9$. Selecting $S = {0, 3}$ (weights 2 and 5) gives weight $2 + 5 = 7 lt.eq C$ and value $3 + 7 = 10$, which is optimal. -] + *Example.* Let $n = #n$ items with weights $(#weights.map(w => str(w)).join(", "))$, values $(#values.map(v => str(v)).join(", "))$, and capacity $C = #C$. Selecting $S = {#selected.map(i => str(i)).join(", ")}$ gives total weight $#total-w lt.eq C$ and total value $#total-v$, which is optimal. + ] + ] +} #problem-def("PartiallyOrderedKnapsack")[ Given $n$ items with weights $w_0, dots, w_(n-1) in NN$ and values $v_0, dots, v_(n-1) in NN$, a partial order $prec$ on the items (given by its cover relations), and a capacity $C in NN$, find a downward-closed subset $S subset.eq {0, dots, n - 1}$ (i.e., if $i in S$ and $j prec i$ then $j in S$) maximizing $sum_(i in S) v_i$ subject to $sum_(i in S) w_i lt.eq C$. @@ -2542,21 +2586,84 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ] } -#problem-def("RuralPostman")[ - Given an undirected graph $G = (V, E)$ with edge lengths $l: E -> ZZ_(gt.eq 0)$, a subset $E' subset.eq E$ of required edges, and a bound $B in ZZ^+$, determine whether there exists a circuit (closed walk) in $G$ that traverses every edge in $E'$ and has total length at most $B$. -][ - The Rural Postman Problem (RPP) is a fundamental NP-complete arc-routing problem @lenstra1976 that generalizes the Chinese Postman Problem. When $E' = E$, the problem reduces to finding an Eulerian circuit with minimum augmentation (polynomial-time solvable via $T$-join matching). For general $E' subset.eq E$, exact algorithms use dynamic programming over subsets of required edges in $O(n^2 dot 2^r)$ time, where $r = |E'|$ and $n = |V|$, analogous to the Held-Karp algorithm for TSP. The problem admits a $3 slash 2$-approximation for metric instances @frederickson1979. +#{ + let x = load-model-example("RuralPostman") + let nv = x.instance.graph.num_vertices + let edges = x.instance.graph.edges.map(e => (e.at(0), e.at(1))) + let ne = edges.len() + let edge-lengths = x.instance.edge_lengths + let required = x.instance.required_edges + let nr = required.len() + let B = x.instance.bound + let config = x.optimal_config + // Selected edges (multiplicity >= 1) + let selected = range(ne).filter(i => config.at(i) >= 1) + let total-cost = selected.map(i => config.at(i) * edge-lengths.at(i)).sum() + [ + #problem-def("RuralPostman")[ + Given an undirected graph $G = (V, E)$ with edge lengths $l: E -> ZZ_(gt.eq 0)$, a subset $E' subset.eq E$ of required edges, and a bound $B in ZZ^+$, determine whether there exists a circuit (closed walk) in $G$ that traverses every edge in $E'$ and has total length at most $B$. + ][ + The Rural Postman Problem (RPP) is a fundamental NP-complete arc-routing problem @lenstra1976 that generalizes the Chinese Postman Problem. When $E' = E$, the problem reduces to finding an Eulerian circuit with minimum augmentation (polynomial-time solvable via $T$-join matching). For general $E' subset.eq E$, exact algorithms use dynamic programming over subsets of required edges in $O(n^2 dot 2^r)$ time, where $r = |E'|$ and $n = |V|$, analogous to the Held-Karp algorithm for TSP. The problem admits a $3 slash 2$-approximation for metric instances @frederickson1979. - *Example.* Consider a hexagonal graph with 6 vertices and 8 edges, where all outer edges have length 1 and two diagonal edges have length 2. The required edges are $E' = {(v_0, v_1), (v_2, v_3), (v_4, v_5)}$ with bound $B = 6$. The outer cycle $v_0 -> v_1 -> v_2 -> v_3 -> v_4 -> v_5 -> v_0$ covers all three required edges with total length $6 times 1 = 6 = B$, so the answer is YES. -] + *Example.* Consider a graph with #nv vertices and #ne edges, where #(ne - 2) outer edges have length 1 and 2 diagonal edges have length 2. The required edges are $E' = {#required.map(i => {let e = edges.at(i); $(v_#(e.at(0)), v_#(e.at(1)))$}).join($,$)}$ with bound $B = #B$. The outer cycle #range(nv).map(i => $v_#i$).join($->$)$-> v_0$ covers all #nr required edges with total length $#total-cost = B$, so the answer is YES. -#problem-def("SubgraphIsomorphism")[ - Given graphs $G = (V_1, E_1)$ (host) and $H = (V_2, E_2)$ (pattern), determine whether $G$ contains a subgraph isomorphic to $H$: does there exist an injective function $f: V_2 -> V_1$ such that ${u, v} in E_2 arrow.double {f(u), f(v)} in E_1$? -][ - Subgraph Isomorphism (GT48 in Garey & Johnson @garey1979) is NP-complete by transformation from Clique @garey1979. It strictly generalizes Clique (where $H = K_k$) and also contains Hamiltonian Circuit ($H = C_n$) and Hamiltonian Path ($H = P_n$) as special cases. Brute-force enumeration of all injective mappings $f: V_2 -> V_1$ runs in $O(|V_1|^(|V_2|) dot |E_2|)$ time. For fixed-size patterns, the color-coding technique of Alon, Yuster, and Zwick @alon1995 gives a randomized algorithm in $2^(O(|V_2|)) dot |V_1|^(O("tw"(H)))$ time. Practical algorithms include VF2 @cordella2004 and VF2++ @juttner2018. + #figure( + canvas(length: 1cm, { + import draw: * + let colors = ( + required: rgb("#e15759"), + optional: rgb("#4e79a7"), + unused: luma(200), + ) + let r = 1.5 + // Place vertices on a hexagon + let positions = range(nv).map(i => { + let angle = 90deg - i * 360deg / nv + (calc.cos(angle) * r, calc.sin(angle) * r) + }) - *Example.* Consider host graph $G$ with 7 vertices: a $K_4$ clique on ${0, 1, 2, 3}$ and a triangle on ${4, 5, 6}$ connected via edge $(3, 4)$. Pattern $H = K_4$ with vertices ${a, b, c, d}$. The mapping $f(a) = 0, f(b) = 1, f(c) = 2, f(d) = 3$ preserves all 6 edges of $K_4$, confirming a subgraph isomorphism exists. -] + // Draw edges + for (ei, (u, v)) in edges.enumerate() { + let is-required = required.contains(ei) + let is-selected = config.at(ei) >= 1 + let col = if is-required { colors.required } else if is-selected { colors.optional } else { colors.unused } + let thickness = if is-selected { 1.2pt } else { 0.5pt } + let dash = if not is-selected { "dashed" } else { "solid" } + line(positions.at(u), positions.at(v), stroke: (paint: col, thickness: thickness, dash: dash), name: "e" + str(ei)) + // Edge length label + let mid = ((positions.at(u).at(0) + positions.at(v).at(0)) / 2, (positions.at(u).at(1) + positions.at(v).at(1)) / 2) + content(mid, text(6pt, fill: col)[#edge-lengths.at(ei)], fill: white, frame: "rect", padding: 0.05, stroke: none) + } + + // Draw vertices + for (i, pos) in positions.enumerate() { + circle(pos, radius: 0.18, fill: white, stroke: 0.6pt + black) + content(pos, text(7pt)[$v_#i$]) + } + }), + caption: [Rural Postman instance: #nv vertices, #ne edges, #nr required edges (red, bold). The outer cycle (blue + red edges) has total cost #total-cost $= B$, covering all required edges.], + ) + ] + ] +} + +#{ + let x = load-model-example("SubgraphIsomorphism") + let nv-host = x.instance.host_graph.num_vertices + let ne-host = x.instance.host_graph.edges.len() + let nv-pat = x.instance.pattern_graph.num_vertices + let ne-pat = x.instance.pattern_graph.edges.len() + let config = x.optimal_config + [ + #problem-def("SubgraphIsomorphism")[ + Given graphs $G = (V_1, E_1)$ (host) and $H = (V_2, E_2)$ (pattern), determine whether $G$ contains a subgraph isomorphic to $H$: does there exist an injective function $f: V_2 -> V_1$ such that ${u, v} in E_2 arrow.double {f(u), f(v)} in E_1$? + ][ + Subgraph Isomorphism (GT48 in Garey & Johnson @garey1979) is NP-complete by transformation from Clique @garey1979. It strictly generalizes Clique (where $H = K_k$) and also contains Hamiltonian Circuit ($H = C_n$) and Hamiltonian Path ($H = P_n$) as special cases. Brute-force enumeration of all injective mappings $f: V_2 -> V_1$ runs in $O(|V_1|^(|V_2|) dot |E_2|)$ time. For fixed-size patterns, the color-coding technique of Alon, Yuster, and Zwick @alon1995 gives a randomized algorithm in $2^(O(|V_2|)) dot |V_1|^(O("tw"(H)))$ time. Practical algorithms include VF2 @cordella2004 and VF2++ @juttner2018. + + *Example.* Host graph $G = K_#nv-host$ (#nv-host vertices, #ne-host edges), pattern $H = K_#nv-pat$ (#nv-pat vertices, #ne-pat edges). The mapping $f = (#range(nv-pat).map(i => $#i arrow.bar #config.at(i)$).join($,$))$ is injective and preserves all #ne-pat pattern edges, confirming a subgraph isomorphism exists. + ] + ] +} #{ let x = load-model-example("LongestCommonSubsequence") @@ -2613,13 +2720,24 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ] } -#problem-def("SubsetSum")[ - Given a finite set $A = {a_0, dots, a_(n-1)}$ with sizes $s(a_i) in ZZ^+$ and a target $B in ZZ^+$, determine whether there exists a subset $A' subset.eq A$ such that $sum_(a in A') s(a) = B$. -][ - One of Karp's 21 NP-complete problems @karp1972. Subset Sum is the special case of Knapsack where $v_i = w_i$ for all items and we seek an exact sum rather than an inequality. Though NP-complete, it is only _weakly_ NP-hard: a dynamic-programming algorithm runs in $O(n B)$ pseudo-polynomial time. The best known exact algorithm is the $O^*(2^(n slash 2))$ meet-in-the-middle approach of Horowitz and Sahni @horowitz1974. +#{ + let x = load-model-example("SubsetSum") + let sizes = x.instance.sizes + let target = x.instance.target + let n = sizes.len() + let config = x.optimal_config + let selected = range(n).filter(i => config.at(i) == 1) + let sel-sizes = selected.map(i => sizes.at(i)) + [ + #problem-def("SubsetSum")[ + Given a finite set $A = {a_0, dots, a_(n-1)}$ with sizes $s(a_i) in ZZ^+$ and a target $B in ZZ^+$, determine whether there exists a subset $A' subset.eq A$ such that $sum_(a in A') s(a) = B$. + ][ + One of Karp's 21 NP-complete problems @karp1972. Subset Sum is the special case of Knapsack where $v_i = w_i$ for all items and we seek an exact sum rather than an inequality. Though NP-complete, it is only _weakly_ NP-hard: a dynamic-programming algorithm runs in $O(n B)$ pseudo-polynomial time. The best known exact algorithm is the $O^*(2^(n slash 2))$ meet-in-the-middle approach of Horowitz and Sahni @horowitz1974. - *Example.* Let $A = {3, 7, 1, 8, 2, 4}$ ($n = 6$) and target $B = 11$. Selecting $A' = {3, 8}$ gives sum $3 + 8 = 11 = B$. Another solution: $A' = {7, 4}$ with sum $7 + 4 = 11 = B$. -] + *Example.* Let $A = {#sizes.map(s => str(s)).join(", ")}$ ($n = #n$) and target $B = #target$. Selecting $A' = {#sel-sizes.map(s => str(s)).join(", ")}$ gives sum $#sel-sizes.map(s => str(s)).join(" + ") = #target = B$. + ] + ] +} #problem-def("ResourceConstrainedScheduling")[ Given a set $T$ of $n$ unit-length tasks, $m$ identical processors, $r$ resources with bounds $B_i$ ($1 <= i <= r$), resource requirements $R_i (t)$ for each task $t$ and resource $i$ ($0 <= R_i (t) <= B_i$), and an overall deadline $D in ZZ^+$, determine whether there exists an $m$-processor schedule $sigma : T -> {0, dots, D-1}$ such that for every time slot $u$, at most $m$ tasks are scheduled at $u$ and $sum_(t : sigma(t) = u) R_i (t) <= B_i$ for each resource $i$. @@ -2717,18 +2835,17 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS For $|R| = 2$ strings, the problem is solvable in polynomial time via the duality with the Longest Common Subsequence (LCS): if $"LCS"(r_1, r_2)$ has length $ell$, then the shortest common supersequence has length $|r_1| + |r_2| - ell$, computable in $O(|r_1| dot |r_2|)$ time by dynamic programming. For general $|R| = m$, the brute-force search over all strings of length at most $K$ takes $O(|Sigma|^K)$ time. Applications include bioinformatics (reconstructing ancestral sequences from fragments), data compression (representing multiple strings compactly), and scheduling (merging instruction sequences). - *Example.* Let $Sigma = {#alpha-map.join(", ")}$ and $R = {#r-strs.join(", ")}$. We seek the shortest string $w$ containing both #r-strs.join(" and ") as subsequences. + *Example.* Let $Sigma = {#alpha-map.join(", ")}$ and $R = {#r-strs.join(", ")}$. We seek a string $w$ of length at most $K = #bound$ that contains every $r_i$ as a subsequence. #figure({ - let blue = graph-colors.at(0) - let teal = rgb("#76b7b2") + let r-colors = (graph-colors.at(0), rgb("#76b7b2"), rgb("#f28e2b"), rgb("#e15759"), rgb("#b07aa1")) align(center, stack(dir: ttb, spacing: 0.6cm, stack(dir: ltr, spacing: 0pt, box(width: 1.2cm, height: 0.5cm, align(center + horizon, text(8pt)[$w =$])), ..w.enumerate().map(((i, ch)) => { - let is1 = embeds.at(0).contains(i) - let is2 = embeds.at(1).contains(i) - let fill = if is1 and is2 { blue.transparentize(60%) } else if is1 { blue.transparentize(80%) } else if is2 { teal.transparentize(80%) } else { white } + // Count how many strings use this position + let used = range(nr).filter(ri => embeds.at(ri).contains(i)).len() + let fill = if used >= 2 { r-colors.at(0).transparentize(50%) } else if used == 1 { r-colors.at(0).transparentize(80%) } else { white } box(width: 0.55cm, height: 0.55cm, fill: fill, stroke: 0.5pt + luma(120), align(center + horizon, text(9pt, weight: "bold", ch))) }), @@ -2736,7 +2853,7 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ..range(nr).map(ri => { let embed = embeds.at(ri) let r = r-chars.at(ri) - let col = if ri == 0 { blue } else { teal } + let col = r-colors.at(ri) stack(dir: ltr, spacing: 0pt, box(width: 1.2cm, height: 0.5cm, align(center + horizon, text(8pt, fill: col)[$r_#(ri + 1) =$])), ..range(w-len).map(i => { @@ -2750,10 +2867,10 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS }), )) }, - caption: [Shortest Common Supersequence: $w = #w-str$ (length #w-len) contains #range(nr).map(ri => [$r_#(ri + 1) = #r-strs.at(ri)$ (#if ri == 0 [blue] else [teal], positions #embeds.at(ri).map(p => str(p)).join(","))]).join(" and ") as subsequences. Dots mark unused positions in each embedding.], + caption: [Shortest Common Supersequence: $w = #w-str$ (length #w-len) contains #range(nr).map(ri => [$r_#(ri + 1) = #r-strs.at(ri)$ (positions #embeds.at(ri).map(p => str(p)).join(","))]).join(", ") as subsequences. Dots mark unused positions.], ) - The supersequence $w = #w-str$ has length #w-len and contains both input strings as subsequences. This is optimal because $"LCS"(#r-strs.join(", ")) = "ac"$ (length 2), so the shortest common supersequence has length $#strings.at(0).len() + #strings.at(1).len() - 2 = #w-len$. + The supersequence $w = #w-str$ has length #w-len $lt.eq K = #bound$ and contains all #nr input strings as subsequences. ] ] } @@ -2824,13 +2941,25 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ] } -#problem-def("MinimumFeedbackArcSet")[ - Given a directed graph $G = (V, A)$, find a minimum-size subset $A' subset.eq A$ such that $G - A'$ is a directed acyclic graph (DAG). Equivalently, $A'$ must contain at least one arc from every directed cycle in $G$. -][ - Feedback Arc Set (FAS) is a classical NP-complete problem from Karp's original list @karp1972 (via transformation from Vertex Cover, as presented in Garey & Johnson GT8). The problem arises in ranking aggregation, sports scheduling, deadlock avoidance, and causal inference. Unlike the undirected analogue (which is trivially polynomial --- the number of non-tree edges in a spanning forest), the directed version is NP-hard due to the richer structure of directed cycles. The best known exact algorithm uses dynamic programming over vertex subsets in $O^*(2^n)$ time, generalizing the Held--Karp TSP technique to vertex ordering problems @bodlaender2012. FAS is fixed-parameter tractable with parameter $k = |A'|$: an $O(4^k dot k! dot n^(O(1)))$ algorithm exists via iterative compression @chen2008. Polynomial-time solvable for planar digraphs via the Lucchesi--Younger theorem @lucchesi1978. +#{ + let x = load-model-example("MinimumFeedbackArcSet") + let nv = x.instance.graph.num_vertices + let arcs = x.instance.graph.arcs.map(a => (a.at(0), a.at(1))) + let na = arcs.len() + let weights = x.instance.weights + let config = x.optimal_config + let opt-val = x.optimal_value.Valid + let removed = range(na).filter(i => config.at(i) == 1) + [ + #problem-def("MinimumFeedbackArcSet")[ + Given a directed graph $G = (V, A)$, find a minimum-size subset $A' subset.eq A$ such that $G - A'$ is a directed acyclic graph (DAG). Equivalently, $A'$ must contain at least one arc from every directed cycle in $G$. + ][ + Feedback Arc Set (FAS) is a classical NP-complete problem from Karp's original list @karp1972 (via transformation from Vertex Cover, as presented in Garey & Johnson GT8). The problem arises in ranking aggregation, sports scheduling, deadlock avoidance, and causal inference. Unlike the undirected analogue (which is trivially polynomial --- the number of non-tree edges in a spanning forest), the directed version is NP-hard due to the richer structure of directed cycles. The best known exact algorithm uses dynamic programming over vertex subsets in $O^*(2^n)$ time, generalizing the Held--Karp TSP technique to vertex ordering problems @bodlaender2012. FAS is fixed-parameter tractable with parameter $k = |A'|$: an $O(4^k dot k! dot n^(O(1)))$ algorithm exists via iterative compression @chen2008. Polynomial-time solvable for planar digraphs via the Lucchesi--Younger theorem @lucchesi1978. - *Example.* Consider $G$ with $V = {0, 1, 2, 3, 4, 5}$ and arcs $(0 arrow 1), (1 arrow 2), (2 arrow 0), (1 arrow 3), (3 arrow 4), (4 arrow 1), (2 arrow 5), (5 arrow 3), (3 arrow 0)$. This graph contains four directed cycles: $0 arrow 1 arrow 2 arrow 0$, $1 arrow 3 arrow 4 arrow 1$, $0 arrow 1 arrow 3 arrow 0$, and $2 arrow 5 arrow 3 arrow 0 arrow 1 arrow 2$. Removing $A' = {(0 arrow 1), (3 arrow 4)}$ breaks all four cycles (vertex 0 becomes a sink in the residual graph), giving a minimum FAS of size 2. -] + *Example.* Consider $G$ with $V = {#range(nv).map(v => str(v)).join(", ")}$ and arcs #arcs.map(a => $(#(a.at(0)) arrow #(a.at(1)))$).join($,$). Removing $A' = {#removed.map(i => {let a = arcs.at(i); $(#(a.at(0)) arrow #(a.at(1)))$}).join($,$)}$ (weight #opt-val) breaks all directed cycles, yielding a DAG. + ] + ] +} #{ let x = load-model-example("MultipleChoiceBranching") @@ -2871,76 +3000,98 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ] } -#problem-def("FlowShopScheduling")[ - Given $m$ processors and a set $J$ of $n$ jobs, where each job $j in J$ consists of $m$ tasks $t_1 [j], t_2 [j], dots, t_m [j]$ with lengths $ell(t_i [j]) in ZZ^+_0$, and a deadline $D in ZZ^+$, determine whether there exists a permutation schedule $pi$ of the jobs such that all jobs complete by time $D$. Each job must be processed on machines $1, 2, dots, m$ in order, and job $j$ cannot start on machine $i+1$ until its task on machine $i$ is completed. -][ - Flow Shop Scheduling is a classical NP-complete problem from Garey & Johnson (A5 SS15), strongly NP-hard for $m >= 3$ @garey1976. For $m = 2$, it is solvable in $O(n log n)$ by Johnson's rule @johnson1954. The problem is fundamental in operations research, manufacturing planning, and VLSI design. When restricted to permutation schedules (same job order on all machines), the search space is $n!$ orderings. The best known exact algorithm for $m = 3$ runs in $O^*(3^n)$ time @shang2018; for general $m$, brute-force over $n!$ permutations gives $O(n! dot m n)$. - - *Example.* Let $m = 3$ machines, $n = 5$ jobs with task lengths: - $ ell = mat( - 3, 4, 2; - 2, 3, 5; - 4, 1, 3; - 1, 5, 4; - 3, 2, 3; - ) $ - and deadline $D = 25$. The job order $pi = (j_4, j_1, j_5, j_3, j_2)$ (0-indexed: $3, 0, 4, 2, 1$) yields makespan $23 <= 25$, so a feasible schedule exists. +#{ + let x = load-model-example("FlowShopScheduling") + let m = x.instance.num_processors + let task-lengths = x.instance.task_lengths + let n = task-lengths.len() + let D = x.instance.deadline + let lehmer = x.optimal_config + // Decode Lehmer code to job permutation + let job-order = { + let avail = range(n) + let result = () + for c in lehmer { + result.push(avail.at(c)) + avail = avail.enumerate().filter(((i, v)) => i != c).map(((i, v)) => v) + } + result + } + // Compute Gantt schedule greedily + let machine-end = range(m).map(_ => 0) + let job-end = range(n).map(_ => 0) + let blocks = () + for ji in job-order { + let lengths = task-lengths.at(ji) + for mi in range(m) { + let start = calc.max(machine-end.at(mi), job-end.at(ji)) + let end = start + lengths.at(mi) + blocks.push((mi, ji, start, end)) + machine-end.at(mi) = end + job-end.at(ji) = end + } + } + let makespan = calc.max(..job-end) + [ + #problem-def("FlowShopScheduling")[ + Given $m$ processors and a set $J$ of $n$ jobs, where each job $j in J$ consists of $m$ tasks $t_1 [j], t_2 [j], dots, t_m [j]$ with lengths $ell(t_i [j]) in ZZ^+_0$, and a deadline $D in ZZ^+$, determine whether there exists a permutation schedule $pi$ of the jobs such that all jobs complete by time $D$. Each job must be processed on machines $1, 2, dots, m$ in order, and job $j$ cannot start on machine $i+1$ until its task on machine $i$ is completed. + ][ + Flow Shop Scheduling is a classical NP-complete problem from Garey & Johnson (A5 SS15), strongly NP-hard for $m >= 3$ @garey1976. For $m = 2$, it is solvable in $O(n log n)$ by Johnson's rule @johnson1954. The problem is fundamental in operations research, manufacturing planning, and VLSI design. When restricted to permutation schedules (same job order on all machines), the search space is $n!$ orderings. The best known exact algorithm for $m = 3$ runs in $O^*(3^n)$ time @shang2018; for general $m$, brute-force over $n!$ permutations gives $O(n! dot m n)$. - #figure( - canvas(length: 1cm, { - import draw: * - // Gantt chart for job order [3, 0, 4, 2, 1] on 3 machines - // Schedule computed greedily: - // M1: j3[0,1], j0[1,4], j4[4,7], j2[7,11], j1[11,13] - // M2: j3[1,6], j0[6,10], j4[10,12], j2[12,13], j1[13,16] - // M3: j3[6,10], j0[10,12], j4[12,15], j2[15,18], j1[18,23] - let colors = (rgb("#4e79a7"), rgb("#e15759"), rgb("#76b7b2"), rgb("#f28e2b"), rgb("#59a14f")) - let job-names = ("$j_1$", "$j_2$", "$j_3$", "$j_4$", "$j_5$") - let scale = 0.38 - let row-h = 0.6 - let gap = 0.15 - - // Machine labels - for (mi, label) in ("M1", "M2", "M3").enumerate() { - let y = -mi * (row-h + gap) - content((-0.8, y), text(8pt, label)) - } + *Example.* Let $m = #m$ machines, $n = #n$ jobs with task lengths: + #align(center, math.equation([$ell = #math.mat(..task-lengths.map(row => row.map(v => [#v])))$])) + and deadline $D = #D$. The job order $pi = (#job-order.map(j => $j_#(j + 1)$).join($,$))$ yields makespan $#makespan <= #D$, so a feasible schedule exists. - // Draw schedule blocks: (machine, job-index, start, end) - let blocks = ( - (0, 3, 0, 1), (0, 0, 1, 4), (0, 4, 4, 7), (0, 2, 7, 11), (0, 1, 11, 13), - (1, 3, 1, 6), (1, 0, 6, 10), (1, 4, 10, 12), (1, 2, 12, 13), (1, 1, 13, 16), - (2, 3, 6, 10), (2, 0, 10, 12), (2, 4, 12, 15), (2, 2, 15, 18), (2, 1, 18, 23), - ) + #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, ji, s, e) in blocks { - 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, job-names.at(ji))) - } + // Machine labels + for mi in range(m) { + let y = -mi * (row-h + gap) + content((-0.8, y), text(8pt, "M" + str(mi + 1))) + } - // Time axis - let max-t = 23 - let y-axis = -2 * (row-h + gap) - row-h / 2 - 0.2 - line((0, y-axis), (max-t * scale, y-axis), stroke: 0.4pt) - for t in (0, 5, 10, 15, 20, 23) { - 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))) - } - content((max-t * scale / 2, y-axis - 0.5), text(7pt)[$t$]) + // Draw schedule blocks + for (mi, ji, s, e) in blocks { + 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_#(ji + 1)$])) + } - // Deadline marker - let dl-x = 25 * scale - line((dl-x, row-h / 2 + 0.1), (dl-x, y-axis), stroke: (paint: red, thickness: 0.8pt, dash: "dashed")) - content((dl-x, row-h / 2 + 0.25), text(6pt, fill: red)[$D = 25$]) - }), - caption: [Flow shop schedule for 5 jobs on 3 machines. Job order $(j_4, j_1, j_5, j_3, j_2)$ achieves makespan 23, within deadline $D = 25$ (dashed red line).], - ) -] + // Time axis + 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))) + } + // Add makespan tick if not already shown + 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$]) + + // Deadline marker + let dl-x = D * scale + line((dl-x, row-h / 2 + 0.1), (dl-x, y-axis), stroke: (paint: red, thickness: 0.8pt, dash: "dashed")) + content((dl-x, row-h / 2 + 0.25), text(6pt, fill: red)[$D = #D$]) + }), + caption: [Flow shop schedule for #n jobs on #m machines. Job order $(#job-order.map(j => $j_#(j + 1)$).join($,$))$ achieves makespan #makespan, within deadline $D = #D$ (dashed red line).], + ) + ] + ] +} #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. @@ -3070,69 +3221,56 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS let sol = (config: x.optimal_config, metric: x.optimal_value) // Compute start times from config offsets: start_i = release_i + config_i let starts = range(ntasks).map(i => release.at(i) + sol.config.at(i)) - // Identify the enforcer task: the one with the tightest window (deadline - release == length) - let enforcer = range(ntasks).filter(i => deadline.at(i) - release.at(i) == lengths.at(i)).at(0) - let regular = range(ntasks).filter(i => i != enforcer) - // Partition sum B = total length of regular tasks - let B = regular.map(i => lengths.at(i)).sum() + let max-t = calc.max(..range(ntasks).map(i => deadline.at(i))) [ #problem-def("SequencingWithinIntervals")[ Given a finite set $T$ of tasks and, for each $t in T$, a release time $r(t) >= 0$, a deadline $d(t) >= 0$, and a processing length $ell(t) in ZZ^+$ satisfying $r(t) + ell(t) <= d(t)$, determine whether there exists a feasible schedule $sigma: T -> ZZ_(>= 0)$ such that for each $t in T$: (1) $sigma(t) >= r(t)$, (2) $sigma(t) + ell(t) <= d(t)$, and (3) for all $t' in T backslash {t}$, either $sigma(t') + ell(t') <= sigma(t)$ or $sigma(t') >= sigma(t) + ell(t)$. ][ - Sequencing Within Intervals is problem SS1 in Garey & Johnson @garey1979, proved NP-complete via reduction from Partition (Theorem 3.8). Each task $t$ must execute non-preemptively during the interval $[r(t), d(t))$, occupying $ell(t)$ consecutive time units, and no two tasks may overlap. The problem is a canonical single-machine scheduling problem and one of the earliest NP-completeness results for scheduling theory. - - The NP-completeness proof uses an "enforcer" task pinned at the midpoint of the time horizon, forcing the remaining tasks to split into two balanced groups --- directly encoding the Partition problem. + Sequencing Within Intervals is problem SS1 in Garey & Johnson @garey1979, proved NP-complete via reduction from Partition (Theorem 3.8). Each task $t$ must execute non-preemptively during the interval $[r(t), d(t))$, occupying $ell(t)$ consecutive time units on a single machine, and no two tasks may overlap. - *Example.* Consider #ntasks tasks derived from a Partition instance with $A = {#regular.map(i => str(lengths.at(i))).join(", ")}$ (sum $B = #B$): + *Example.* Consider #ntasks tasks with overlapping availability windows: #align(center, table( columns: ntasks + 1, align: center, - table.header([$"Task"$], ..regular.map(i => [$t_#(i + 1)$]), [$overline(t)$]), - [$r(t)$], ..regular.map(i => [#release.at(i)]), [#release.at(enforcer)], - [$d(t)$], ..regular.map(i => [#deadline.at(i)]), [#deadline.at(enforcer)], - [$ell(t)$], ..regular.map(i => [#lengths.at(i)]), [#lengths.at(enforcer)], + table.header([$"Task"$], ..range(ntasks).map(i => [$t_#(i + 1)$])), + [$r(t)$], ..range(ntasks).map(i => [#release.at(i)]), + [$d(t)$], ..range(ntasks).map(i => [#deadline.at(i)]), + [$ell(t)$], ..range(ntasks).map(i => [#lengths.at(i)]), )) - The enforcer task $overline(t)$ must run in $[#release.at(enforcer), #deadline.at(enforcer))$, splitting the schedule into $[0, #release.at(enforcer))$ and $[#deadline.at(enforcer), #deadline.at(0))$. Each side has #(B / 2) time units, and tasks with total length $#(B / 2)$ must fill each side --- corresponding to a partition of $A$. + Each task can only start within its window $[r(t), d(t) - ell(t)]$, and the windows overlap, so finding a non-overlapping assignment is non-trivial. One feasible schedule places the tasks at #range(ntasks).map(i => $[#starts.at(i), #(starts.at(i) + lengths.at(i)))$).join($,$): #figure( canvas(length: 1cm, { import draw: * - let colors = (rgb("#4e79a7"), rgb("#e15759"), rgb("#76b7b2"), rgb("#f28e2b")) - let enforcer-color = rgb("#b07aa1") - let task-labels = regular.map(i => "$t_" + str(i + 1) + "$") + ("$overline(t)$",) - let task-order = regular + (enforcer,) - let scale = 0.7 + let colors = (rgb("#4e79a7"), rgb("#e15759"), rgb("#76b7b2"), rgb("#f28e2b"), rgb("#b07aa1")) + let scale = 0.65 let row-h = 0.6 - // Single-row Gantt chart: all tasks on one timeline - for (k, i) in task-order.enumerate() { + // Single-row Gantt chart + for i in range(ntasks) { let s = starts.at(i) let e = s + lengths.at(i) let x0 = s * scale let x1 = e * scale - let col = if i == enforcer { enforcer-color } else { colors.at(regular.position(j => j == i)) } + let col = colors.at(i) rect((x0, -row-h / 2), (x1, row-h / 2), fill: col.transparentize(30%), stroke: 0.4pt + col) - content(((x0 + x1) / 2, 0), text(6pt, task-labels.at(k))) + content(((x0 + x1) / 2, 0), text(6pt, [$t_#(i + 1)$])) } - // Release-time and deadline markers for each task - for (k, i) in task-order.enumerate() { - let col = if i == enforcer { enforcer-color } else { colors.at(regular.position(j => j == i)) } - // Release time: upward triangle below axis + // Release-time and deadline markers + for i in range(ntasks) { + let col = colors.at(i) let rx = release.at(i) * scale line((rx, -row-h / 2 - 0.05), (rx, -row-h / 2 - 0.18), stroke: 0.5pt + col) - // Deadline: downward tick above axis let dx = deadline.at(i) * scale line((dx, row-h / 2 + 0.05), (dx, row-h / 2 + 0.18), stroke: 0.5pt + col) } - // Release / deadline group labels content((-0.5, -row-h / 2 - 0.12), text(5pt)[$r$]) content((-0.5, row-h / 2 + 0.12), text(5pt)[$d$]) // Time axis - let max-t = 11 let y-axis = -row-h / 2 - 0.35 line((0, y-axis), (max-t * scale, y-axis), stroke: 0.4pt) for t in range(max-t + 1) { @@ -3143,14 +3281,8 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS } } content((max-t * scale / 2, y-axis - 0.45), text(7pt)[$t$]) - - // Enforcer region highlight - let ex0 = release.at(enforcer) * scale - let ex1 = deadline.at(enforcer) * scale - line((ex0, row-h / 2 + 0.3), (ex0, y-axis), stroke: (paint: enforcer-color, thickness: 0.6pt, dash: "dashed")) - line((ex1, row-h / 2 + 0.3), (ex1, y-axis), stroke: (paint: enforcer-color, thickness: 0.6pt, dash: "dashed")) }), - caption: [Feasible schedule for the SWI instance. The enforcer task $overline(t)$ (purple) is pinned at $[#release.at(enforcer), #deadline.at(enforcer))$, splitting the timeline into two halves of #(B / 2) time units each.], + caption: [A feasible schedule for the SWI instance. Ticks below and above mark release times $r$ and deadlines $d$ for each task.], ) ] ] @@ -3286,8 +3418,7 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS let conj = x.instance.conjuncts let nr = rels.len() let nc = conj.len() - let sol = x.optimal.at(0) - let assignment = sol.config + let assignment = x.optimal_config [ #problem-def("ConjunctiveBooleanQuery")[ Given a finite domain $D = {0, dots, d - 1}$, a collection of relations $R_0, R_1, dots, R_(m-1)$ where each $R_i$ is a set of $a_i$-tuples with entries from $D$, and a conjunctive Boolean query diff --git a/src/models/graph/graph_partitioning.rs b/src/models/graph/graph_partitioning.rs index c6027fff5..1ace66b80 100644 --- a/src/models/graph/graph_partitioning.rs +++ b/src/models/graph/graph_partitioning.rs @@ -142,6 +142,31 @@ crate::declare_variants! { default opt GraphPartitioning => "2^num_vertices", } +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + use crate::topology::SimpleGraph; + // Two triangles connected by 3 edges; balanced cut = 3 + vec![crate::example_db::specs::ModelExampleSpec { + id: "graph_partitioning", + instance: Box::new(GraphPartitioning::new(SimpleGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (1, 2), + (1, 3), + (2, 3), + (2, 4), + (3, 4), + (3, 5), + (4, 5), + ], + ))), + optimal_config: vec![0, 0, 0, 1, 1, 1], + optimal_value: serde_json::json!({"Valid": 3}), + }] +} + #[cfg(test)] #[path = "../../unit_tests/models/graph/graph_partitioning.rs"] mod tests; diff --git a/src/models/graph/minimum_feedback_arc_set.rs b/src/models/graph/minimum_feedback_arc_set.rs index bfb8e3ede..ff2263065 100644 --- a/src/models/graph/minimum_feedback_arc_set.rs +++ b/src/models/graph/minimum_feedback_arc_set.rs @@ -179,6 +179,21 @@ crate::declare_variants! { default opt MinimumFeedbackArcSet => "2^num_vertices", } +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + use crate::topology::DirectedGraph; + // 3-node cycle, unit weights; remove one arc to break cycle, cost = 1 + vec![crate::example_db::specs::ModelExampleSpec { + id: "minimum_feedback_arc_set", + instance: Box::new(MinimumFeedbackArcSet::new( + DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]), + vec![1i32, 1, 1], + )), + optimal_config: vec![0, 0, 1], + optimal_value: serde_json::json!({"Valid": 1}), + }] +} + #[cfg(test)] #[path = "../../unit_tests/models/graph/minimum_feedback_arc_set.rs"] mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 202c53275..fc87b43c3 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -134,5 +134,10 @@ pub(crate) fn canonical_model_example_specs() -> Vec => "2^num_vertices", } +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + use crate::topology::SimpleGraph; + // 6 vertices, 7 edges (path + two long chords), bound K=11 + // Identity permutation [0,1,2,3,4,5] gives cost 1+1+1+1+1+3+3 = 11 + vec![crate::example_db::specs::ModelExampleSpec { + id: "optimal_linear_arrangement", + instance: Box::new(OptimalLinearArrangement::new( + SimpleGraph::new( + 6, + vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 3), (2, 5)], + ), + 11, + )), + optimal_config: vec![0, 1, 2, 3, 4, 5], + optimal_value: serde_json::json!(true), + }] +} + #[cfg(test)] #[path = "../../unit_tests/models/graph/optimal_linear_arrangement.rs"] mod tests; diff --git a/src/models/graph/rural_postman.rs b/src/models/graph/rural_postman.rs index ed3909c1f..e77842b45 100644 --- a/src/models/graph/rural_postman.rs +++ b/src/models/graph/rural_postman.rs @@ -278,6 +278,37 @@ crate::declare_variants! { default sat RuralPostman => "2^num_vertices * num_vertices^2", } +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + use crate::topology::SimpleGraph; + // Issue #248 instance 1: hexagonal graph, 8 edges, E'={e0,e2,e4}, B=6 + // Solution: hexagon cycle with all 6 unit-cost edges, config [1,1,1,1,1,1,0,0] + let graph = SimpleGraph::new( + 6, + vec![ + (0, 1), + (1, 2), + (2, 3), + (3, 4), + (4, 5), + (5, 0), + (0, 3), + (1, 4), + ], + ); + vec![crate::example_db::specs::ModelExampleSpec { + id: "rural_postman", + instance: Box::new(RuralPostman::new( + graph, + vec![1, 1, 1, 1, 1, 1, 2, 2], + vec![0, 2, 4], + 6, + )), + optimal_config: vec![1, 1, 1, 1, 1, 1, 0, 0], + optimal_value: serde_json::json!(true), + }] +} + #[cfg(test)] #[path = "../../unit_tests/models/graph/rural_postman.rs"] mod tests; diff --git a/src/models/graph/subgraph_isomorphism.rs b/src/models/graph/subgraph_isomorphism.rs index 3234f8644..50f9654ec 100644 --- a/src/models/graph/subgraph_isomorphism.rs +++ b/src/models/graph/subgraph_isomorphism.rs @@ -183,6 +183,21 @@ crate::declare_variants! { default sat SubgraphIsomorphism => "num_host_vertices ^ num_pattern_vertices", } +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + use crate::topology::SimpleGraph; + // Host: K4, Pattern: K3 → map [0,1,2] preserves all edges + vec![crate::example_db::specs::ModelExampleSpec { + id: "subgraph_isomorphism", + instance: Box::new(SubgraphIsomorphism::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), + SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]), + )), + optimal_config: vec![0, 1, 2], + optimal_value: serde_json::json!(true), + }] +} + #[cfg(test)] #[path = "../../unit_tests/models/graph/subgraph_isomorphism.rs"] mod tests; diff --git a/src/models/misc/bin_packing.rs b/src/models/misc/bin_packing.rs index 49375b038..9843518da 100644 --- a/src/models/misc/bin_packing.rs +++ b/src/models/misc/bin_packing.rs @@ -158,6 +158,17 @@ crate::declare_variants! { opt BinPacking => "2^num_items", } +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "bin_packing", + // 3 items of sizes [3,3,4], capacity 7 → optimal 2 bins + instance: Box::new(BinPacking::::new(vec![3, 3, 4], 7)), + optimal_config: vec![0, 1, 0], + optimal_value: serde_json::json!({"Valid": 2}), + }] +} + #[cfg(test)] #[path = "../../unit_tests/models/misc/bin_packing.rs"] mod tests; diff --git a/src/models/misc/flow_shop_scheduling.rs b/src/models/misc/flow_shop_scheduling.rs index d5e1286ce..bbdd9a1f8 100644 --- a/src/models/misc/flow_shop_scheduling.rs +++ b/src/models/misc/flow_shop_scheduling.rs @@ -199,6 +199,27 @@ crate::declare_variants! { default sat FlowShopScheduling => "factorial(num_jobs)", } +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "flow_shop_scheduling", + instance: Box::new(FlowShopScheduling::new( + 3, + vec![ + vec![3, 4, 2], + vec![2, 3, 5], + vec![4, 1, 3], + vec![1, 5, 4], + vec![3, 2, 3], + ], + 25, + )), + // Job order [3,0,4,2,1] = Lehmer code [3,0,2,1,0], makespan 23 + optimal_config: vec![3, 0, 2, 1, 0], + optimal_value: serde_json::json!(true), + }] +} + #[cfg(test)] #[path = "../../unit_tests/models/misc/flow_shop_scheduling.rs"] mod tests; diff --git a/src/models/misc/knapsack.rs b/src/models/misc/knapsack.rs index 2eb7c772a..65973fe8e 100644 --- a/src/models/misc/knapsack.rs +++ b/src/models/misc/knapsack.rs @@ -203,6 +203,18 @@ mod nonnegative_i64_vec { } } +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + // 4 items: weights [2,3,4,5], values [3,4,5,7], capacity 7 + // Optimal: items 0,3 → weight=7, value=10 + vec![crate::example_db::specs::ModelExampleSpec { + id: "knapsack", + instance: Box::new(Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7)), + optimal_config: vec![1, 0, 0, 1], + optimal_value: serde_json::json!({"Valid": 10}), + }] +} + #[cfg(test)] #[path = "../../unit_tests/models/misc/knapsack.rs"] mod tests; diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 107451643..094ac8775 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -82,5 +82,9 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec Vec Vec { + // 6 elements [3,7,1,8,2,4], target 11 → select {3,8} + vec![crate::example_db::specs::ModelExampleSpec { + id: "subset_sum", + instance: Box::new(SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32)), + optimal_config: vec![1, 0, 0, 1, 0, 0], + optimal_value: serde_json::json!(true), + }] +} + #[cfg(test)] #[path = "../../unit_tests/models/misc/subset_sum.rs"] mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index a04753b84..d4f3d5663 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -29,9 +29,9 @@ pub use misc::{ BinPacking, CbqRelation, ConjunctiveBooleanQuery, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, PrecedenceConstrainedScheduling, QueryArg, RectilinearPictureCompression, - ResourceConstrainedScheduling, - SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence, - StaffScheduling, StringToStringCorrection, SubsetSum, SumOfSquaresPartition, + ResourceConstrainedScheduling, SequencingWithReleaseTimesAndDeadlines, + SequencingWithinIntervals, ShortestCommonSupersequence, StaffScheduling, + StringToStringCorrection, SubsetSum, SumOfSquaresPartition, }; pub use set::{ ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking, diff --git a/src/unit_tests/models/graph/rural_postman.rs b/src/unit_tests/models/graph/rural_postman.rs index d51428e86..9e72648bb 100644 --- a/src/unit_tests/models/graph/rural_postman.rs +++ b/src/unit_tests/models/graph/rural_postman.rs @@ -175,6 +175,39 @@ fn test_rural_postman_brute_force_no_solution() { assert!(result.is_none()); } +#[test] +fn test_rural_postman_find_all_satisfying() { + // Issue #248 instance 1: hexagonal graph, 6 vertices, 8 edges + // Required edges E'={{0,1},{2,3},{4,5}}, B=6 + // Search space = 3^8 = 6561 + let problem = hexagon_rpp(); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + for sol in &solutions { + assert!(problem.evaluate(sol)); + } + // The issue witness (hexagon cycle, all multiplicity 1) must be among solutions + assert!(solutions.contains(&vec![1, 1, 1, 1, 1, 1, 0, 0])); + // Only the hexagon cycle (cost 6 = B) satisfies; diagonals cost 2 each + assert_eq!(solutions.len(), 1); +} + +#[test] +fn test_rural_postman_find_all_satisfying_empty() { + // Issue #248 instance 2: required edges {0,1} and {4,5} are far apart + // Minimum circuit cost ≥ 8 > B=4 + let graph = SimpleGraph::new( + 6, + vec![(0, 1), (1, 2), (2, 3), (3, 0), (3, 4), (4, 5), (5, 3)], + ); + let edge_lengths = vec![1, 1, 1, 1, 3, 1, 3]; + let required_edges = vec![0, 5]; + let bound = 4; + let problem = RuralPostman::new(graph, edge_lengths, required_edges, bound); + let solver = BruteForce::new(); + assert!(solver.find_all_satisfying(&problem).is_empty()); +} + #[test] fn test_rural_postman_serialization() { let problem = chinese_postman_rpp(); diff --git a/src/unit_tests/models/misc/flow_shop_scheduling.rs b/src/unit_tests/models/misc/flow_shop_scheduling.rs index fe468bd89..58b0490a3 100644 --- a/src/unit_tests/models/misc/flow_shop_scheduling.rs +++ b/src/unit_tests/models/misc/flow_shop_scheduling.rs @@ -150,6 +150,42 @@ fn test_flow_shop_scheduling_empty() { assert!(problem.evaluate(&[])); } +#[test] +fn test_flow_shop_scheduling_find_all_satisfying() { + // Issue #507 example: 3 machines, 5 jobs, D=25 + // Search space = 5! = 120 permutations + let problem = FlowShopScheduling::new( + 3, + vec![ + vec![3, 4, 2], + vec![2, 3, 5], + vec![4, 1, 3], + vec![1, 5, 4], + vec![3, 2, 3], + ], + 25, + ); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + for sol in &solutions { + assert!(problem.evaluate(sol)); + } + // The issue witness sequence [3,0,4,2,1] = Lehmer code [3,0,2,1,0] + // gives makespan 23 ≤ 25 + assert!(solutions.contains(&vec![3, 0, 2, 1, 0])); + // 99 out of 120 permutations have makespan ≤ 25 + assert_eq!(solutions.len(), 99); +} + +#[test] +fn test_flow_shop_scheduling_find_all_satisfying_empty() { + // 2 machines, 2 symmetric jobs [5,5], deadline 10 + // Both orderings give makespan 15 > 10 + let problem = FlowShopScheduling::new(2, vec![vec![5, 5], vec![5, 5]], 10); + let solver = BruteForce::new(); + assert!(solver.find_all_satisfying(&problem).is_empty()); +} + #[test] fn test_flow_shop_scheduling_single_job() { // 3 machines, 1 job: [2, 3, 4] diff --git a/src/unit_tests/models/misc/multiprocessor_scheduling.rs b/src/unit_tests/models/misc/multiprocessor_scheduling.rs index 13f63158d..ee37b078e 100644 --- a/src/unit_tests/models/misc/multiprocessor_scheduling.rs +++ b/src/unit_tests/models/misc/multiprocessor_scheduling.rs @@ -106,6 +106,31 @@ fn test_multiprocessor_scheduling_brute_force_infeasible() { assert!(solution.is_none()); } +#[test] +fn test_multiprocessor_scheduling_find_all_satisfying() { + // Issue #212 example: 5 tasks [4,5,3,2,6], m=2, D=10 + // Search space = 2^5 = 32 + let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + for sol in &solutions { + assert!(problem.evaluate(sol)); + } + // The issue witness {t1,t5} on P0 and {t2,t3,t4} on P1 must be among solutions + assert!(solutions.contains(&vec![0, 1, 1, 1, 0])); + // Only 2 feasible partitions: {t1,t5}/{t2,t3,t4} and its mirror + assert_eq!(solutions.len(), 2); +} + +#[test] +fn test_multiprocessor_scheduling_find_all_satisfying_empty() { + // Same instance but deadline 9: total=20, need each processor ≤ 9, + // but 20 > 2*9 = 18, so impossible + let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 9); + let solver = BruteForce::new(); + assert!(solver.find_all_satisfying(&problem).is_empty()); +} + #[test] fn test_multiprocessor_scheduling_serialization() { let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10); diff --git a/src/unit_tests/models/misc/sequencing_within_intervals.rs b/src/unit_tests/models/misc/sequencing_within_intervals.rs index b3b5d3205..cd4e3a534 100644 --- a/src/unit_tests/models/misc/sequencing_within_intervals.rs +++ b/src/unit_tests/models/misc/sequencing_within_intervals.rs @@ -4,50 +4,51 @@ use crate::traits::Problem; #[test] fn test_sequencing_within_intervals_creation() { + // 5 tasks with overlapping availability windows let problem = SequencingWithinIntervals::new( - vec![0, 0, 0, 0, 5], - vec![11, 11, 11, 11, 6], - vec![3, 1, 2, 4, 1], + vec![0, 1, 3, 6, 0], + vec![5, 8, 9, 12, 12], + vec![2, 2, 2, 3, 2], ); assert_eq!(problem.num_tasks(), 5); - assert_eq!(problem.release_times(), &[0, 0, 0, 0, 5]); - assert_eq!(problem.deadlines(), &[11, 11, 11, 11, 6]); - assert_eq!(problem.lengths(), &[3, 1, 2, 4, 1]); + assert_eq!(problem.release_times(), &[0, 1, 3, 6, 0]); + assert_eq!(problem.deadlines(), &[5, 8, 9, 12, 12]); + assert_eq!(problem.lengths(), &[2, 2, 2, 3, 2]); // dims: d[i] - r[i] - l[i] + 1 - // Task 0: 11 - 0 - 3 + 1 = 9 - // Task 1: 11 - 0 - 1 + 1 = 11 - // Task 2: 11 - 0 - 2 + 1 = 10 - // Task 3: 11 - 0 - 4 + 1 = 8 - // Task 4: 6 - 5 - 1 + 1 = 1 - assert_eq!(problem.dims(), vec![9, 11, 10, 8, 1]); + // Task 0: 5 - 0 - 2 + 1 = 4 + // Task 1: 8 - 1 - 2 + 1 = 6 + // Task 2: 9 - 3 - 2 + 1 = 5 + // Task 3: 12 - 6 - 3 + 1 = 4 + // Task 4: 12 - 0 - 2 + 1 = 11 + assert_eq!(problem.dims(), vec![4, 6, 5, 4, 11]); } #[test] fn test_sequencing_within_intervals_evaluation_feasible() { let problem = SequencingWithinIntervals::new( - vec![0, 0, 0, 0, 5], - vec![11, 11, 11, 11, 6], - vec![3, 1, 2, 4, 1], + vec![0, 1, 3, 6, 0], + vec![5, 8, 9, 12, 12], + vec![2, 2, 2, 3, 2], ); - // Task 0: config=0 -> start=0, runs [0,3) - // Task 1: config=6 -> start=6, runs [6,7) - // Task 2: config=3 -> start=3, runs [3,5) - // Task 3: config=7 -> start=7, runs [7,11) - // Task 4: config=0 -> start=5, runs [5,6) + // Task 0: config=0 -> start=0, runs [0,2) + // Task 1: config=1 -> start=2, runs [2,4) + // Task 2: config=1 -> start=4, runs [4,6) + // Task 3: config=0 -> start=6, runs [6,9) + // Task 4: config=9 -> start=9, runs [9,11) // No overlaps. - assert!(problem.evaluate(&[0, 6, 3, 7, 0])); + assert!(problem.evaluate(&[0, 1, 1, 0, 9])); } #[test] fn test_sequencing_within_intervals_evaluation_infeasible_overlap() { let problem = SequencingWithinIntervals::new( - vec![0, 0, 0, 0, 5], - vec![11, 11, 11, 11, 6], - vec![3, 1, 2, 4, 1], + vec![0, 1, 3, 6, 0], + vec![5, 8, 9, 12, 12], + vec![2, 2, 2, 3, 2], ); - // Task 0: config=0 -> start=0, runs [0,3) - // Task 1: config=1 -> start=1, runs [1,2) -- overlaps with task 0 - assert!(!problem.evaluate(&[0, 1, 3, 7, 0])); + // Task 0: config=0 -> start=0, runs [0,2) + // Task 1: config=0 -> start=1, runs [1,3) -- overlaps with task 0 + assert!(!problem.evaluate(&[0, 0, 1, 0, 9])); } #[test] @@ -77,12 +78,12 @@ fn test_sequencing_within_intervals_solver() { } #[test] -fn test_sequencing_within_intervals_solver_partition_example() { - // Instance from the plan (PARTITION reduction) +fn test_sequencing_within_intervals_solver_canonical() { + // Canonical instance: 5 tasks with overlapping windows let problem = SequencingWithinIntervals::new( - vec![0, 0, 0, 0, 5], - vec![11, 11, 11, 11, 6], - vec![3, 1, 2, 4, 1], + vec![0, 1, 3, 6, 0], + vec![5, 8, 9, 12, 12], + vec![2, 2, 2, 3, 2], ); let solver = BruteForce::new(); let solution = solver.find_satisfying(&problem); @@ -147,6 +148,33 @@ fn test_sequencing_within_intervals_single_task() { assert!(problem.evaluate(&[2])); } +#[test] +fn test_sequencing_within_intervals_find_all_satisfying() { + // Issue #219 canonical instance: 5 tasks with overlapping windows + // dims = [4, 6, 5, 4, 11], search space = 5280 + let problem = SequencingWithinIntervals::new( + vec![0, 1, 3, 6, 0], + vec![5, 8, 9, 12, 12], + vec![2, 2, 2, 3, 2], + ); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + for sol in &solutions { + assert!(problem.evaluate(sol)); + } + // Canonical witness config must be among solutions + assert!(solutions.contains(&vec![0, 1, 1, 0, 9])); + assert_eq!(solutions.len(), 41); +} + +#[test] +fn test_sequencing_within_intervals_find_all_satisfying_empty() { + // Two tasks that must both use time [0,2), impossible without overlap + let problem = SequencingWithinIntervals::new(vec![0, 0], vec![2, 2], vec![2, 2]); + let solver = BruteForce::new(); + assert!(solver.find_all_satisfying(&problem).is_empty()); +} + #[test] #[should_panic(expected = "time window is empty")] fn test_sequencing_within_intervals_invalid_window() { diff --git a/src/unit_tests/models/misc/shortest_common_supersequence.rs b/src/unit_tests/models/misc/shortest_common_supersequence.rs index 0725276bc..5634dcca5 100644 --- a/src/unit_tests/models/misc/shortest_common_supersequence.rs +++ b/src/unit_tests/models/misc/shortest_common_supersequence.rs @@ -122,6 +122,45 @@ fn test_shortestcommonsupersequence_paper_example() { assert!(solver2.find_satisfying(&tight).is_none()); } +#[test] +fn test_shortestcommonsupersequence_find_all_satisfying() { + // Issue #412 instance 1: Σ={a,b,c}, R={"abcb","bcab","acba"}, K=7 + // Search space = 3^7 = 2187 + let problem = ShortestCommonSupersequence::new( + 3, + vec![vec![0, 1, 2, 1], vec![1, 2, 0, 1], vec![0, 2, 1, 0]], + 7, + ); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + for sol in &solutions { + assert!(problem.evaluate(sol)); + } + // The issue witness "abcacba" = [0,1,2,0,2,1,0] must be among solutions + assert!(solutions.contains(&vec![0, 1, 2, 0, 2, 1, 0])); + assert_eq!(solutions.len(), 42); +} + +#[test] +fn test_shortestcommonsupersequence_find_all_satisfying_empty() { + // Issue #412 instance 3: all 6 permutations of {a,b,c}, bound 5 + // Minimum SCS length is 7, so bound 5 is infeasible + let problem = ShortestCommonSupersequence::new( + 3, + vec![ + vec![0, 1, 2], + vec![1, 2, 0], + vec![2, 0, 1], + vec![0, 2, 1], + vec![1, 0, 2], + vec![2, 1, 0], + ], + 5, + ); + let solver = BruteForce::new(); + assert!(solver.find_all_satisfying(&problem).is_empty()); +} + #[test] fn test_shortestcommonsupersequence_serialization() { let problem = ShortestCommonSupersequence::new(3, vec![vec![0, 1, 2], vec![2, 1, 0]], 5); From 9d413f7258a006c4ee162422c8f64545cba30000 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Thu, 19 Mar 2026 18:26:02 +0800 Subject: [PATCH 2/3] Apply rustfmt to long lines in create.rs and resource_constrained_scheduling.rs Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/commands/create.rs | 27 +++++++++++-------- .../misc/resource_constrained_scheduling.rs | 11 +++++--- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index b126068e6..b3009f518 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -16,9 +16,8 @@ use problemreductions::models::misc::{ BinPacking, CbqRelation, ConjunctiveBooleanQuery, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, PartiallyOrderedKnapsack, QueryArg, RectilinearPictureCompression, ResourceConstrainedScheduling, - SequencingWithReleaseTimesAndDeadlines, - SequencingWithinIntervals, ShortestCommonSupersequence, StringToStringCorrection, SubsetSum, - SumOfSquaresPartition, + SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence, + StringToStringCorrection, SubsetSum, SumOfSquaresPartition, }; use problemreductions::models::BiconnectivityAugmentation; use problemreductions::prelude::*; @@ -1638,18 +1637,24 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // ResourceConstrainedScheduling "ResourceConstrainedScheduling" => { let usage = "Usage: pred create ResourceConstrainedScheduling --num-processors 3 --resource-bounds \"20\" --resource-requirements \"6;7;7;6;8;6\" --deadline 2"; - let num_processors = args - .num_processors - .ok_or_else(|| anyhow::anyhow!("ResourceConstrainedScheduling requires --num-processors\n\n{usage}"))?; + let num_processors = args.num_processors.ok_or_else(|| { + anyhow::anyhow!( + "ResourceConstrainedScheduling requires --num-processors\n\n{usage}" + ) + })?; let bounds_str = args.resource_bounds.as_deref().ok_or_else(|| { - anyhow::anyhow!("ResourceConstrainedScheduling requires --resource-bounds\n\n{usage}") + anyhow::anyhow!( + "ResourceConstrainedScheduling requires --resource-bounds\n\n{usage}" + ) })?; let reqs_str = args.resource_requirements.as_deref().ok_or_else(|| { - anyhow::anyhow!("ResourceConstrainedScheduling requires --resource-requirements\n\n{usage}") + anyhow::anyhow!( + "ResourceConstrainedScheduling requires --resource-requirements\n\n{usage}" + ) + })?; + let deadline = args.deadline.ok_or_else(|| { + anyhow::anyhow!("ResourceConstrainedScheduling requires --deadline\n\n{usage}") })?; - let deadline = args - .deadline - .ok_or_else(|| anyhow::anyhow!("ResourceConstrainedScheduling requires --deadline\n\n{usage}"))?; let resource_bounds: Vec = util::parse_comma_list(bounds_str)?; let resource_requirements: Vec> = reqs_str diff --git a/src/unit_tests/models/misc/resource_constrained_scheduling.rs b/src/unit_tests/models/misc/resource_constrained_scheduling.rs index c7d8b1e13..bd5e194e8 100644 --- a/src/unit_tests/models/misc/resource_constrained_scheduling.rs +++ b/src/unit_tests/models/misc/resource_constrained_scheduling.rs @@ -62,14 +62,16 @@ fn test_resource_constrained_scheduling_evaluate_invalid_resource() { #[test] fn test_resource_constrained_scheduling_evaluate_wrong_config_length() { - let problem = ResourceConstrainedScheduling::new(3, vec![20], vec![vec![5], vec![5], vec![5]], 2); + let problem = + ResourceConstrainedScheduling::new(3, vec![20], vec![vec![5], vec![5], vec![5]], 2); assert!(!problem.evaluate(&[0, 1])); assert!(!problem.evaluate(&[0, 1, 0, 1])); } #[test] fn test_resource_constrained_scheduling_evaluate_out_of_range_slot() { - let problem = ResourceConstrainedScheduling::new(3, vec![20], vec![vec![5], vec![5], vec![5]], 2); + let problem = + ResourceConstrainedScheduling::new(3, vec![20], vec![vec![5], vec![5], vec![5]], 2); // Slot 2 is out of range for deadline=2 (valid: 0, 1) assert!(!problem.evaluate(&[0, 1, 2])); } @@ -141,7 +143,10 @@ fn test_resource_constrained_scheduling_serialization() { assert_eq!(restored.num_tasks(), problem.num_tasks()); assert_eq!(restored.num_processors(), problem.num_processors()); assert_eq!(restored.resource_bounds(), problem.resource_bounds()); - assert_eq!(restored.resource_requirements(), problem.resource_requirements()); + assert_eq!( + restored.resource_requirements(), + problem.resource_requirements() + ); assert_eq!(restored.deadline(), problem.deadline()); } From 4f364860877e64f613f5760e8389b48a8f19f1aa Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Thu, 19 Mar 2026 18:34:24 +0800 Subject: [PATCH 3/3] Fix test_create_missing_model_example: use non-default variant GraphPartitioning/SimpleGraph now has a canonical example, so switch the test to MaximumIndependentSet/KingsSubgraph/One which does not. Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/tests/cli_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 8a0a33785..33d6d520b 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -2821,7 +2821,7 @@ fn test_create_model_example_steiner_tree() { #[test] fn test_create_missing_model_example() { let output = pred() - .args(["create", "--example", "GraphPartitioning/SimpleGraph"]) + .args(["create", "--example", "MaximumIndependentSet/KingsSubgraph/One"]) .output() .unwrap(); assert!(!output.status.success());