Skip to content

Commit 24dfc58

Browse files
isPANNclaude
andcommitted
Implement Partition -> IntegralFlowWithMultipliers reduction (#363)
Adds Sahni's multiplier-flow gadget: 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 converts the target's "net inflow at least R" condition into the exact equality needed by Partition. Odd-S inputs reduce to a fixed infeasible 3-vertex instance. - src/rules/partition_integralflowwithmultipliers.rs: reduction impl with odd-S/even-S branches and witness extraction from source arcs - src/unit_tests/rules/partition_integralflowwithmultipliers.rs: 5 tests (closed-loop, structure on even-total YES instance, even-total NO exercises bottleneck, odd-total fixed NO target, witness extraction, canonical example spec) - src/rules/mod.rs: register module and example specs - docs/paper/reductions.typ: full theorem with construction, correctness proof, and worked example using the canonical fixture Closes #363. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 5bf5d3d commit 24dfc58

4 files changed

Lines changed: 276 additions & 0 deletions

File tree

docs/paper/reductions.typ

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11690,6 +11690,50 @@ where $P$ is a penalty weight large enough that any constraint violation costs m
1169011690
_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.
1169111691
]
1169211692

11693+
#let part_ifwm = load-example("Partition", "IntegralFlowWithMultipliers")
11694+
#let part_ifwm_sol = part_ifwm.solutions.at(0)
11695+
#let part_ifwm_sizes = part_ifwm.source.instance.sizes
11696+
#let part_ifwm_n = part_ifwm_sizes.len()
11697+
#let part_ifwm_total = part_ifwm_sizes.fold(0, (a, b) => a + b)
11698+
#let part_ifwm_half = part_ifwm_total / 2
11699+
#let part_ifwm_selected = part_ifwm_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i)
11700+
#let part_ifwm_selected_sizes = part_ifwm_selected.map(i => part_ifwm_sizes.at(i))
11701+
#let part_ifwm_source_arcs = part_ifwm_sol.target_config.slice(0, part_ifwm_n)
11702+
#let part_ifwm_relay_arcs = part_ifwm_sol.target_config.slice(part_ifwm_n, 2 * part_ifwm_n)
11703+
#let part_ifwm_bottleneck = part_ifwm_sol.target_config.at(2 * part_ifwm_n)
11704+
#reduction-rule("Partition", "IntegralFlowWithMultipliers",
11705+
example: true,
11706+
example-caption: [#part_ifwm_n elements, total sum $S = #part_ifwm_total$, bottleneck $R = #part_ifwm_half$],
11707+
extra: [
11708+
#pred-commands(
11709+
"pred create --example " + problem-spec(part_ifwm.source) + " -o partition.json",
11710+
"pred reduce partition.json --to " + target-spec(part_ifwm) + " -o bundle.json",
11711+
"pred solve bundle.json",
11712+
"pred evaluate partition.json --config " + part_ifwm_sol.source_config.map(str).join(","),
11713+
)
11714+
11715+
*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$.
11716+
11717+
*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.
11718+
11719+
*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.
11720+
11721+
*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.
11722+
],
11723+
)[
11724+
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.
11725+
][
11726+
_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$.
11727+
11728+
_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$.
11729+
11730+
($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
11731+
$ f(w, t) = sum_(i=1)^n a_i f(s, v_i). $
11732+
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.
11733+
11734+
_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.
11735+
]
11736+
1169311737
// Removed: Partition → ShortestWeightConstrainedPath (unsound reduction, #1006)
1169411738

1169511739
#let ks_qubo = load-example("Knapsack", "QUBO")

src/rules/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ pub(crate) mod naesatisfiability_setsplitting;
101101
pub(crate) mod paintshop_qubo;
102102
pub(crate) mod partition_binpacking;
103103
pub(crate) mod partition_cosineproductintegration;
104+
pub(crate) mod partition_integralflowwithmultipliers;
104105
pub(crate) mod partition_knapsack;
105106
pub(crate) mod partition_multiprocessorscheduling;
106107
pub(crate) mod partition_openshopscheduling;
@@ -447,6 +448,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
447448
specs.extend(minimummultiwaycut_qubo::canonical_rule_example_specs());
448449
specs.extend(paintshop_qubo::canonical_rule_example_specs());
449450
specs.extend(partition_cosineproductintegration::canonical_rule_example_specs());
451+
specs.extend(partition_integralflowwithmultipliers::canonical_rule_example_specs());
450452
specs.extend(partition_knapsack::canonical_rule_example_specs());
451453
specs.extend(partition_openshopscheduling::canonical_rule_example_specs());
452454
specs.extend(partition_productionplanning::canonical_rule_example_specs());
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
//! Reduction from Partition to IntegralFlowWithMultipliers.
2+
//!
3+
//! For an even total sum `S`, this is Sahni's multiplier-flow gadget:
4+
//! items are binary source choices amplified by vertex multipliers and merged
5+
//! through a single bottleneck arc of capacity `S / 2`. For an odd total sum,
6+
//! the reduction returns a fixed infeasible target instance.
7+
8+
use crate::models::graph::IntegralFlowWithMultipliers;
9+
use crate::models::misc::Partition;
10+
use crate::reduction;
11+
use crate::rules::traits::{ReduceTo, ReductionResult};
12+
use crate::topology::DirectedGraph;
13+
14+
/// Result of reducing Partition to IntegralFlowWithMultipliers.
15+
#[derive(Debug, Clone)]
16+
pub struct ReductionPartitionToIntegralFlowWithMultipliers {
17+
target: IntegralFlowWithMultipliers,
18+
source_n: usize,
19+
item_arc_count: usize,
20+
}
21+
22+
impl ReductionResult for ReductionPartitionToIntegralFlowWithMultipliers {
23+
type Source = Partition;
24+
type Target = IntegralFlowWithMultipliers;
25+
26+
fn target_problem(&self) -> &Self::Target {
27+
&self.target
28+
}
29+
30+
fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
31+
if self.item_arc_count == 0 {
32+
return vec![0; self.source_n];
33+
}
34+
35+
if target_solution.len() < self.item_arc_count {
36+
return vec![0; self.source_n];
37+
}
38+
39+
target_solution[..self.item_arc_count].to_vec()
40+
}
41+
}
42+
43+
#[reduction(overhead = {
44+
num_vertices = "num_elements + 3",
45+
num_arcs = "2 * num_elements + 1",
46+
max_capacity = "total_sum",
47+
requirement = "total_sum",
48+
})]
49+
impl ReduceTo<IntegralFlowWithMultipliers> for Partition {
50+
type Result = ReductionPartitionToIntegralFlowWithMultipliers;
51+
52+
fn reduce_to(&self) -> Self::Result {
53+
let total_sum = self.total_sum();
54+
let source_n = self.num_elements();
55+
56+
if !total_sum.is_multiple_of(2) {
57+
let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]);
58+
return ReductionPartitionToIntegralFlowWithMultipliers {
59+
target: IntegralFlowWithMultipliers::new(graph, 0, 2, vec![1, 2, 1], vec![1, 1], 1),
60+
source_n,
61+
item_arc_count: 0,
62+
};
63+
}
64+
65+
let half_sum = total_sum / 2;
66+
let relay = source_n + 1;
67+
let sink = source_n + 2;
68+
69+
let mut arcs = Vec::with_capacity(2 * source_n + 1);
70+
let mut capacities = Vec::with_capacity(2 * source_n + 1);
71+
let mut multipliers = vec![1; source_n + 3];
72+
73+
for (index, &size) in self.sizes().iter().enumerate() {
74+
let item_vertex = index + 1;
75+
arcs.push((0, item_vertex));
76+
capacities.push(1);
77+
multipliers[item_vertex] = size;
78+
}
79+
80+
for (index, &size) in self.sizes().iter().enumerate() {
81+
let item_vertex = index + 1;
82+
arcs.push((item_vertex, relay));
83+
capacities.push(size);
84+
}
85+
86+
arcs.push((relay, sink));
87+
capacities.push(half_sum);
88+
multipliers[relay] = 1;
89+
90+
let graph = DirectedGraph::new(source_n + 3, arcs);
91+
ReductionPartitionToIntegralFlowWithMultipliers {
92+
target: IntegralFlowWithMultipliers::new(
93+
graph,
94+
0,
95+
sink,
96+
multipliers,
97+
capacities,
98+
half_sum,
99+
),
100+
source_n,
101+
item_arc_count: source_n,
102+
}
103+
}
104+
}
105+
106+
#[cfg(feature = "example-db")]
107+
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
108+
use crate::export::SolutionPair;
109+
110+
vec![crate::example_db::specs::RuleExampleSpec {
111+
id: "partition_to_integralflowwithmultipliers",
112+
build: || {
113+
crate::example_db::specs::rule_example_with_witness::<_, IntegralFlowWithMultipliers>(
114+
Partition::new(vec![2, 3, 4, 5, 6, 4]),
115+
SolutionPair {
116+
source_config: vec![1, 0, 1, 0, 1, 0],
117+
target_config: vec![1, 0, 1, 0, 1, 0, 2, 0, 4, 0, 6, 0, 12],
118+
},
119+
)
120+
},
121+
}]
122+
}
123+
124+
#[cfg(test)]
125+
#[path = "../unit_tests/rules/partition_integralflowwithmultipliers.rs"]
126+
mod tests;
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use super::*;
2+
use crate::models::graph::IntegralFlowWithMultipliers;
3+
use crate::models::misc::Partition;
4+
use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target;
5+
use crate::solvers::BruteForce;
6+
7+
#[test]
8+
fn test_partition_to_integralflowwithmultipliers_closed_loop() {
9+
let source = Partition::new(vec![1, 2, 3]);
10+
let reduction = ReduceTo::<IntegralFlowWithMultipliers>::reduce_to(&source);
11+
12+
assert_satisfaction_round_trip_from_satisfaction_target(
13+
&source,
14+
&reduction,
15+
"Partition -> IntegralFlowWithMultipliers closed loop",
16+
);
17+
}
18+
19+
#[test]
20+
fn test_partition_to_integralflowwithmultipliers_structure_even_total() {
21+
let source = Partition::new(vec![2, 3, 4, 5, 6, 4]);
22+
let reduction = ReduceTo::<IntegralFlowWithMultipliers>::reduce_to(&source);
23+
let target = reduction.target_problem();
24+
25+
assert_eq!(target.num_vertices(), 9);
26+
assert_eq!(
27+
target.graph().arcs(),
28+
vec![
29+
(0, 1),
30+
(0, 2),
31+
(0, 3),
32+
(0, 4),
33+
(0, 5),
34+
(0, 6),
35+
(1, 7),
36+
(2, 7),
37+
(3, 7),
38+
(4, 7),
39+
(5, 7),
40+
(6, 7),
41+
(7, 8),
42+
]
43+
);
44+
assert_eq!(
45+
target.capacities(),
46+
&[1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 4, 12]
47+
);
48+
assert_eq!(target.multipliers(), &[1, 2, 3, 4, 5, 6, 4, 1, 1]);
49+
assert_eq!(target.requirement(), 12);
50+
}
51+
52+
#[test]
53+
fn test_partition_to_integralflowwithmultipliers_even_no_instance_uses_bottleneck() {
54+
let source = Partition::new(vec![3, 5]);
55+
let reduction = ReduceTo::<IntegralFlowWithMultipliers>::reduce_to(&source);
56+
let target = reduction.target_problem();
57+
58+
assert_eq!(target.capacities(), &[1, 1, 3, 5, 4]);
59+
assert!(BruteForce::new().find_witness(target).is_none());
60+
}
61+
62+
#[test]
63+
fn test_partition_to_integralflowwithmultipliers_odd_total_is_fixed_no_instance() {
64+
let source = Partition::new(vec![1, 2]);
65+
let reduction = ReduceTo::<IntegralFlowWithMultipliers>::reduce_to(&source);
66+
let target = reduction.target_problem();
67+
68+
assert_eq!(target.num_vertices(), 3);
69+
assert_eq!(target.graph().arcs(), vec![(0, 1), (1, 2)]);
70+
assert_eq!(target.multipliers(), &[1, 2, 1]);
71+
assert_eq!(target.capacities(), &[1, 1]);
72+
assert_eq!(target.requirement(), 1);
73+
assert!(BruteForce::new().find_witness(target).is_none());
74+
assert_eq!(reduction.extract_solution(&[]), vec![0, 0]);
75+
}
76+
77+
#[test]
78+
fn test_partition_to_integralflowwithmultipliers_extract_solution() {
79+
let source = Partition::new(vec![2, 3, 4, 5, 6, 4]);
80+
let reduction = ReduceTo::<IntegralFlowWithMultipliers>::reduce_to(&source);
81+
82+
assert_eq!(
83+
reduction.extract_solution(&[1, 0, 1, 0, 1, 0, 2, 0, 4, 0, 6, 0, 12]),
84+
vec![1, 0, 1, 0, 1, 0]
85+
);
86+
}
87+
88+
#[cfg(feature = "example-db")]
89+
#[test]
90+
fn test_partition_to_integralflowwithmultipliers_canonical_example_spec() {
91+
let example = (canonical_rule_example_specs()
92+
.into_iter()
93+
.find(|spec| spec.id == "partition_to_integralflowwithmultipliers")
94+
.expect("canonical example spec should exist")
95+
.build)();
96+
97+
assert_eq!(example.solutions.len(), 1);
98+
let solution = &example.solutions[0];
99+
assert_eq!(solution.source_config, vec![1, 0, 1, 0, 1, 0]);
100+
assert_eq!(
101+
solution.target_config,
102+
vec![1, 0, 1, 0, 1, 0, 2, 0, 4, 0, 6, 0, 12]
103+
);
104+
}

0 commit comments

Comments
 (0)