diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 449db0e5..832412b1 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -11690,6 +11690,50 @@ where $P$ is a penalty weight large enough that any constraint violation costs m _Solution extraction._ Return the same binary selection vector: element $i$ is in the partition subset if and only if it is selected in the Subset Sum witness. ] +#let part_ifwm = load-example("Partition", "IntegralFlowWithMultipliers") +#let part_ifwm_sol = part_ifwm.solutions.at(0) +#let part_ifwm_sizes = part_ifwm.source.instance.sizes +#let part_ifwm_n = part_ifwm_sizes.len() +#let part_ifwm_total = part_ifwm_sizes.fold(0, (a, b) => a + b) +#let part_ifwm_half = part_ifwm_total / 2 +#let part_ifwm_selected = part_ifwm_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) +#let part_ifwm_selected_sizes = part_ifwm_selected.map(i => part_ifwm_sizes.at(i)) +#let part_ifwm_source_arcs = part_ifwm_sol.target_config.slice(0, part_ifwm_n) +#let part_ifwm_relay_arcs = part_ifwm_sol.target_config.slice(part_ifwm_n, 2 * part_ifwm_n) +#let part_ifwm_bottleneck = part_ifwm_sol.target_config.at(2 * part_ifwm_n) +#reduction-rule("Partition", "IntegralFlowWithMultipliers", + example: true, + example-caption: [#part_ifwm_n elements, total sum $S = #part_ifwm_total$, bottleneck $R = #part_ifwm_half$], + extra: [ + #pred-commands( + "pred create --example " + problem-spec(part_ifwm.source) + " -o partition.json", + "pred reduce partition.json --to " + target-spec(part_ifwm) + " -o bundle.json", + "pred solve bundle.json", + "pred evaluate partition.json --config " + part_ifwm_sol.source_config.map(str).join(","), + ) + + *Step 1 -- Source instance.* The canonical Partition multiset is $(#part_ifwm_sizes.map(str).join(", "))$, so the total is $S = #part_ifwm_total$ and any balanced witness must sum to $S / 2 = #part_ifwm_half$. + + *Step 2 -- Build the relay network.* The reduction creates vertices $s$, one item vertex $v_i$ per element, a relay vertex $w$, and sink $t$. It adds unit-capacity arcs $(s, v_i)$, item arcs $(v_i, w)$ with capacities $(#part_ifwm_sizes.map(str).join(", "))$, and one bottleneck arc $(w, t)$ with capacity $#part_ifwm_half$. The target witness therefore has $#part_ifwm_sol.target_config.len()$ arc-flow coordinates ordered as source arcs, relay arcs, then the bottleneck arc. + + *Step 3 -- Verify the canonical witness.* The source witness $bold(x) = (#part_ifwm_sol.source_config.map(str).join(", "))$ selects item indices $\{#part_ifwm_selected.map(str).join(", ")\}$ with sizes $(#part_ifwm_selected_sizes.map(str).join(", "))$, summing to $#part_ifwm_half$. On the target side, the source arcs carry $(#part_ifwm_source_arcs.map(str).join(", "))$, the relay arcs carry $(#part_ifwm_relay_arcs.map(str).join(", "))$, and the bottleneck arc carries $#part_ifwm_bottleneck$. Thus the relay receives $#part_ifwm_selected_sizes.map(str).join(" + ") = #part_ifwm_half$ units and the sink inflow equals the requirement #sym.checkmark. + + *Witness semantics.* The fixture stores one canonical balanced subset. Other balanced subsets may exist, but every feasible target witness still extracts by reading the first $n$ unit-capacity source arcs. + ], +)[ + This $O(n)$ reduction @sahni1974 @garey1979[ND33] implements Sahni's multiplier-flow gadget for subset selection. Each Partition element becomes an item vertex whose multiplier amplifies a binary source choice into either $0$ or $a_i$ units entering a relay. A single bottleneck arc of capacity $S / 2$ then converts the target model's sink condition "net inflow at least $R$" into the exact equality needed by Partition. +][ + _Construction._ Let the source multiset be $A = {a_1, dots, a_n}$ with total sum $S = sum_(i=1)^n a_i$. If $S$ is odd, return a fixed infeasible Integral Flow With Multipliers instance with vertices $(s, u, t)$, arcs $(s, u)$ and $(u, t)$ both of capacity $1$, multiplier $h(u) = 2$, and requirement $R = 1$. Otherwise set $M = S / 2$ and build a directed graph with vertices $s, v_1, dots, v_n, w, t$. Add arcs $(s, v_i)$ of capacity $1$ and arcs $(v_i, w)$ of capacity $a_i$ for each $i in {1, dots, n}$, plus one bottleneck arc $(w, t)$ of capacity $M$. Assign multipliers $h(v_i) = a_i$ and $h(w) = 1$, and set the sink requirement to $R = M$. + + _Correctness._ ($arrow.r.double$) If the Partition instance is satisfiable, choose a subset $I subset.eq {1, dots, n}$ with $sum_(i in I) a_i = S / 2 = M$. Send one unit on $(s, v_i)$ for each $i in I$ and zero otherwise. Multiplier conservation at each item vertex forces $f(v_i, w) = a_i$ when $i in I$ and $0$ otherwise. The relay multiplier is $1$, so the total flow on $(w, t)$ becomes $sum_(i in I) a_i = M$, which respects the bottleneck capacity and meets the sink requirement $R = M$. When $S$ is odd, the source instance is unsatisfiable and the fixed target is also infeasible because conservation at $u$ would require $f(u, t) = 2 f(s, u)$ while the arc capacity is only $1$. + + ($arrow.l.double$) Suppose the target instance is feasible. In the odd branch the fixed target is infeasible, so only the even branch can yield a witness. Every arc $(s, v_i)$ has capacity $1$, hence integrality forces $f(s, v_i) in {0, 1}$. Conservation at $v_i$ gives $f(v_i, w) = a_i f(s, v_i)$, so each item contributes either $0$ or exactly $a_i$ units to the relay. Conservation at $w$ with multiplier $1$ gives + $ f(w, t) = sum_(i=1)^n a_i f(s, v_i). $ + The bottleneck capacity enforces $f(w, t) <= M$, while the sink requirement enforces $f(w, t) >= R = M$. Therefore $f(w, t) = M = S / 2$, and the indices with $f(s, v_i) = 1$ form a balanced partition subset. + + _Solution extraction._ Read the first $n$ arc-flow coordinates, corresponding to the unit-capacity arcs $(s, v_1), dots, (s, v_n)$. Output bit $x_i = f(s, v_i)$. In the odd branch, return the all-zero source vector. +] + // Removed: Partition → ShortestWeightConstrainedPath (unsound reduction, #1006) #let ks_qubo = load-example("Knapsack", "QUBO") diff --git a/docs/plans/2026-05-26-partition-to-integral-flow-with-multipliers.md b/docs/plans/2026-05-26-partition-to-integral-flow-with-multipliers.md new file mode 100644 index 00000000..551c0c8e --- /dev/null +++ b/docs/plans/2026-05-26-partition-to-integral-flow-with-multipliers.md @@ -0,0 +1,140 @@ +# Issue 363 Plan: Partition -> IntegralFlowWithMultipliers + +Issue: [#363](https://github.com/CodingThrust/problem-reductions/issues/363) +Title: `[Rule] PARTITION to INTEGRAL FLOW WITH MULTIPLIERS` + +## Objective + +Implement the witness-preserving reduction `Partition -> IntegralFlowWithMultipliers` using Sahni's 1974 multiplier-flow gadget with the relay bottleneck fix documented in the issue body and comments. The rule must map: + +- even-total `Partition` instances to a relay network whose sink inflow is forced to equal `S / 2`, and +- odd-total instances to a fixed infeasible `IntegralFlowWithMultipliers` instance. + +Verification mode: default. No `--no-verify`. + +## Reference Notes + +- Primary source: Sartaj Sahni, *Computationally Related Problems*, SIAM J. Comput. 3(4):262-279, 1974, Section 2.2 / Fig. 2.2.1 (`sum of subsets -> N(i)`). +- Catalog source: Garey and Johnson, ND33, `Integral Flow With Multipliers`. +- Issue comments already resolved the earlier false-positive construction by adding relay vertex `w` and bottleneck arc `(w, t)` with capacity `S / 2`. + +## Action Pipeline + +This plan follows `.claude/skills/add-rule/SKILL.md` Steps 1-7. + +## Batch 1: Steps 1-5.5 (verification, implementation, tests, example-db) + +### 1. Mathematical verification + +- Re-state the reduction precisely in repository semantics: + - Source: `Partition` + - Target: `IntegralFlowWithMultipliers` + - Witness on source: binary subset vector over `sizes` + - Witness on target: integral arc-flow vector in graph arc order +- Verify the two branches: + - odd total `S`: fixed 3-vertex NO instance with `h(u) = 2`, capacities `(1, 1)`, `R = 1` + - even total `S`: vertices `s, v_1, ..., v_n, w, t`; arcs `(s, v_i)`, `(v_i, w)`, `(w, t)`; multipliers `h(v_i) = a_i`, `h(w) = 1`; requirement `R = S / 2` +- Lock the extraction rule: + - read the `n` source-item arcs `(s, v_i)` + - map `flow = 1` to selected item and `flow = 0` to unselected item + - odd branch returns the all-zero source config + +### 2. Implement the reduction + +- Add `src/rules/partition_integralflowwithmultipliers.rs`. +- Implement `ReductionPartitionToIntegralFlowWithMultipliers` with: + - `target: IntegralFlowWithMultipliers` + - `source_n: usize` + - `item_arc_count: usize` so extraction can distinguish even vs odd branch reliably +- Implement `ReduceTo for Partition`. +- Construction details: + - even branch: + - vertex numbering `0 = s`, `1..=n = v_i`, `n + 1 = w`, `n + 2 = t` + - arcs in deterministic order: all `(s, v_i)`, then all `(v_i, w)`, then `(w, t)` + - capacities: `1`, then `a_i`, then `S / 2` + - multipliers: source/sink placeholders `1`, item multipliers `a_i`, relay multiplier `1` + - requirement `S / 2` + - odd branch: + - fixed graph `s -> u -> t` + - capacities `[1, 1]` + - multipliers `[1, 2, 1]` + - requirement `1` +- Add exact overhead metadata: + - `num_vertices = "num_elements + 3"` + - `num_arcs = "2 * num_elements + 1"` + - `max_capacity = "total_sum"` + - `requirement = "total_sum"` + Notes: + - these are valid asymptotic upper bounds across both branches; the odd branch is constant size + - `total_sum` safely upper-bounds both `S / 2` and `max_i a_i` + +### 3. Register in `src/rules/mod.rs` + +- Add `mod partition_integralflowwithmultipliers;`. + +### 4. Write unit tests + +- Add `src/unit_tests/rules/partition_integralflowwithmultipliers.rs`. +- Required coverage: + - closed-loop YES instance using brute force on target and round-tripping to source + - even-sum NO instance `{3, 5}` proving the bottleneck removes the earlier false positive + - odd-sum NO instance `{1, 2}` proving the fixed NO target is infeasible + - structure test on the worked example `{2, 3, 4, 5, 6, 4}`: + - vertex count `9` + - arc order and capacities + - multipliers + - requirement `12` + - extraction test from a hand-written feasible target flow +- Reuse `assert_satisfaction_round_trip_from_satisfaction_target` if it fits the witness-preserving pattern. + +### 5. Add canonical example to `example_db` + +- Add a builder in `src/example_db/rule_builders.rs` with id `partition_to_integralflowwithmultipliers`. +- Use the issue's tutorial instance `A = {2, 3, 4, 5, 6, 4}` and the canonical half-sum witness selecting `{2, 4, 6}`. +- Ensure the target witness matches the rule's deterministic arc order: + - source arcs: `[1, 0, 1, 0, 1, 0]` + - relay arcs: `[2, 0, 4, 0, 6, 0]` + - bottleneck arc: `[12]` + +### 5.5. Local rule-level verification before paper + +- Run focused tests for: + - model feasibility expectations + - new rule unit tests + - example-db lookup if needed +- Fix any witness-ordering or feasibility mismatches before touching paper. + +## Batch 2: Step 6 and Step 7 (paper, exports, fixtures, final verification) + +### 6. Document in paper + +- Add a `reduction-rule("Partition", "IntegralFlowWithMultipliers", ...)` entry to `docs/paper/reductions.typ`. +- Include: + - construction summary citing Sahni 1974 / Garey-Johnson ND33 + - correctness proof with the exact-equality argument: + - cap `(w, t) <= S / 2` + - sink requirement `>= S / 2` + - therefore sink inflow `= S / 2` + - solution extraction from the unit-capacity source-item arcs + - explicit odd-total preprocessing branch +- Add a worked example derived from the canonical example fixture, starting with `pred-commands()`. + +### 7. Regenerate exports and verify + +- Run the required generators after the rule and paper are in place: + - `cargo run --example export_graph` + - `cargo run --example export_schemas` + - `make regenerate-fixtures` +- Run default verification commands without `--no-verify`: + - at minimum `make test clippy` + - `make paper` +- Inspect `git status --short` and stage only intended tracked outputs. Ignore generated `docs/src/reductions/` exports if they appear untracked/ignored. + +## Expected Deliverables + +- New reduction source file and tests +- Rule registration +- Canonical example-db entry and regenerated fixture data +- Paper entry for `Partition -> IntegralFlowWithMultipliers` +- Updated reduction graph / schema exports +- Clean implementation commit(s), plan-file removal commit, PR comment, and pushed branch diff --git a/src/rules/mod.rs b/src/rules/mod.rs index cd59acef..aea124f6 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -101,6 +101,7 @@ pub(crate) mod naesatisfiability_setsplitting; pub(crate) mod paintshop_qubo; pub(crate) mod partition_binpacking; pub(crate) mod partition_cosineproductintegration; +pub(crate) mod partition_integralflowwithmultipliers; pub(crate) mod partition_knapsack; pub(crate) mod partition_multiprocessorscheduling; pub(crate) mod partition_openshopscheduling; @@ -447,6 +448,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec &Self::Target { + &self.target + } + + fn extract_solution(&self, target_solution: &[usize]) -> Vec { + if self.item_arc_count == 0 { + return vec![0; self.source_n]; + } + + if target_solution.len() < self.item_arc_count { + return vec![0; self.source_n]; + } + + target_solution[..self.item_arc_count].to_vec() + } +} + +#[reduction(overhead = { + num_vertices = "num_elements + 3", + num_arcs = "2 * num_elements + 1", + max_capacity = "total_sum", + requirement = "total_sum", +})] +impl ReduceTo for Partition { + type Result = ReductionPartitionToIntegralFlowWithMultipliers; + + fn reduce_to(&self) -> Self::Result { + let total_sum = self.total_sum(); + let source_n = self.num_elements(); + + if !total_sum.is_multiple_of(2) { + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); + return ReductionPartitionToIntegralFlowWithMultipliers { + target: IntegralFlowWithMultipliers::new(graph, 0, 2, vec![1, 2, 1], vec![1, 1], 1), + source_n, + item_arc_count: 0, + }; + } + + let half_sum = total_sum / 2; + let relay = source_n + 1; + let sink = source_n + 2; + + let mut arcs = Vec::with_capacity(2 * source_n + 1); + let mut capacities = Vec::with_capacity(2 * source_n + 1); + let mut multipliers = vec![1; source_n + 3]; + + for (index, &size) in self.sizes().iter().enumerate() { + let item_vertex = index + 1; + arcs.push((0, item_vertex)); + capacities.push(1); + multipliers[item_vertex] = size; + } + + for (index, &size) in self.sizes().iter().enumerate() { + let item_vertex = index + 1; + arcs.push((item_vertex, relay)); + capacities.push(size); + } + + arcs.push((relay, sink)); + capacities.push(half_sum); + multipliers[relay] = 1; + + let graph = DirectedGraph::new(source_n + 3, arcs); + ReductionPartitionToIntegralFlowWithMultipliers { + target: IntegralFlowWithMultipliers::new( + graph, + 0, + sink, + multipliers, + capacities, + half_sum, + ), + source_n, + item_arc_count: source_n, + } + } +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + use crate::export::SolutionPair; + + vec![crate::example_db::specs::RuleExampleSpec { + id: "partition_to_integralflowwithmultipliers", + build: || { + crate::example_db::specs::rule_example_with_witness::<_, IntegralFlowWithMultipliers>( + Partition::new(vec![2, 3, 4, 5, 6, 4]), + SolutionPair { + source_config: vec![1, 0, 1, 0, 1, 0], + target_config: vec![1, 0, 1, 0, 1, 0, 2, 0, 4, 0, 6, 0, 12], + }, + ) + }, + }] +} + +#[cfg(test)] +#[path = "../unit_tests/rules/partition_integralflowwithmultipliers.rs"] +mod tests; diff --git a/src/unit_tests/rules/partition_integralflowwithmultipliers.rs b/src/unit_tests/rules/partition_integralflowwithmultipliers.rs new file mode 100644 index 00000000..6149a2d9 --- /dev/null +++ b/src/unit_tests/rules/partition_integralflowwithmultipliers.rs @@ -0,0 +1,104 @@ +use super::*; +use crate::models::graph::IntegralFlowWithMultipliers; +use crate::models::misc::Partition; +use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; +use crate::solvers::BruteForce; + +#[test] +fn test_partition_to_integralflowwithmultipliers_closed_loop() { + let source = Partition::new(vec![1, 2, 3]); + let reduction = ReduceTo::::reduce_to(&source); + + assert_satisfaction_round_trip_from_satisfaction_target( + &source, + &reduction, + "Partition -> IntegralFlowWithMultipliers closed loop", + ); +} + +#[test] +fn test_partition_to_integralflowwithmultipliers_structure_even_total() { + let source = Partition::new(vec![2, 3, 4, 5, 6, 4]); + let reduction = ReduceTo::::reduce_to(&source); + let target = reduction.target_problem(); + + assert_eq!(target.num_vertices(), 9); + assert_eq!( + target.graph().arcs(), + vec![ + (0, 1), + (0, 2), + (0, 3), + (0, 4), + (0, 5), + (0, 6), + (1, 7), + (2, 7), + (3, 7), + (4, 7), + (5, 7), + (6, 7), + (7, 8), + ] + ); + assert_eq!( + target.capacities(), + &[1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 4, 12] + ); + assert_eq!(target.multipliers(), &[1, 2, 3, 4, 5, 6, 4, 1, 1]); + assert_eq!(target.requirement(), 12); +} + +#[test] +fn test_partition_to_integralflowwithmultipliers_even_no_instance_uses_bottleneck() { + let source = Partition::new(vec![3, 5]); + let reduction = ReduceTo::::reduce_to(&source); + let target = reduction.target_problem(); + + assert_eq!(target.capacities(), &[1, 1, 3, 5, 4]); + assert!(BruteForce::new().find_witness(target).is_none()); +} + +#[test] +fn test_partition_to_integralflowwithmultipliers_odd_total_is_fixed_no_instance() { + let source = Partition::new(vec![1, 2]); + let reduction = ReduceTo::::reduce_to(&source); + let target = reduction.target_problem(); + + assert_eq!(target.num_vertices(), 3); + assert_eq!(target.graph().arcs(), vec![(0, 1), (1, 2)]); + assert_eq!(target.multipliers(), &[1, 2, 1]); + assert_eq!(target.capacities(), &[1, 1]); + assert_eq!(target.requirement(), 1); + assert!(BruteForce::new().find_witness(target).is_none()); + assert_eq!(reduction.extract_solution(&[]), vec![0, 0]); +} + +#[test] +fn test_partition_to_integralflowwithmultipliers_extract_solution() { + let source = Partition::new(vec![2, 3, 4, 5, 6, 4]); + let reduction = ReduceTo::::reduce_to(&source); + + assert_eq!( + reduction.extract_solution(&[1, 0, 1, 0, 1, 0, 2, 0, 4, 0, 6, 0, 12]), + vec![1, 0, 1, 0, 1, 0] + ); +} + +#[cfg(feature = "example-db")] +#[test] +fn test_partition_to_integralflowwithmultipliers_canonical_example_spec() { + let example = (canonical_rule_example_specs() + .into_iter() + .find(|spec| spec.id == "partition_to_integralflowwithmultipliers") + .expect("canonical example spec should exist") + .build)(); + + assert_eq!(example.solutions.len(), 1); + let solution = &example.solutions[0]; + assert_eq!(solution.source_config, vec![1, 0, 1, 0, 1, 0]); + assert_eq!( + solution.target_config, + vec![1, 0, 1, 0, 1, 0, 2, 0, 4, 0, 6, 0, 12] + ); +}