Source: Partition
Target: SumOfSquaresPartition
Motivation: Establishes NP-hardness of SumOfSquaresPartition from Partition (one of Karp's 21). The reduction is Garey & Johnson SP19's textbook construction restricted to K = 2: among all 2-way partitions of A, the sum of squared group sums is minimized when the two groups have equal sums (S/2 each), giving minimum S²/2. Hence Partition is YES iff the minimum sum of squares is ⌈S²/2⌉ (equivalently, ≤ S²/2 when S is even).
Reference: Garey & Johnson, Computers and Intractability, SP19, p. 225.
GJ Source Entry
[SP19] MINIMUM SUM OF SQUARES
INSTANCE: Finite set A, a size s(a) in Z^+ for each a in A, positive integers K<=|A| and J.
QUESTION: Can A be partitioned into K disjoint sets A_1,A_2,...,A_K such that
Sum_{i=1}^{K}(Sum_{a in A_i} s(a))^2 <= J ?
Reference: Transformation from PARTITION or 3-PARTITION.
Comment: NP-complete in the strong sense. NP-complete in the ordinary sense and solvable in pseudo-polynomial time for any fixed K. Variants in which the bound K on the number of sets is replaced by a bound B on either the maximum set cardinality or the maximum total set size are also NP-complete in the strong sense [Wong and Yao, 1976].
Codebase Note: Why This Is a Witness Reduction (No J Field)
The SumOfSquaresPartition model in this codebase is a pure minimization problem with Value = Min<i64> and fields (sizes, num_groups). There is no J bound field and no Decision<SumOfSquaresPartition> registered. So the reduction is implemented in the same witness-style form as partition_multiprocessorscheduling.rs:
- The target
SumOfSquaresPartition instance encodes the construction (identical sizes, K = 2).
ReductionResult::extract_solution is the identity (the group assignment is a valid Partition subset assignment).
- The closed-loop test relies on
source.evaluate(extracted_config) — Partition::evaluate already returns Or(true) exactly when the config splits the sum in half. The bound J = S²/2 is therefore an implicit property of the optimal target witness, not a stored field.
This matches the precedent set by partition_multiprocessorscheduling.rs and other Partition → ... reductions where the target is constructed to make Partition's YES/NO answer recoverable from source.evaluate(extract_solution(target_witness)).
(A separate follow-up issue may introduce Decision<SumOfSquaresPartition> with an explicit J bound; this issue does not depend on that.)
Reduction Algorithm
Given a Partition instance with sizes A = {s_1, ..., s_n} (s_i > 0, source Value = Or), construct a SumOfSquaresPartition (Value = Min<i64>) as follows:
- Guard small instances. If
n < 2 (i.e. K = 2 > n would violate SumOfSquaresPartition's constructor invariant num_groups ≤ num_elements), emit a sentinel target SumOfSquaresPartition::new(vec![1, 1], 2) and remember source_n. extract_solution returns vec![0; source_n], on which Partition::evaluate returns Or(false) — the correct YES/NO answer because a single positive element cannot be split into two equal-sum groups (and Partition::new already rejects n = 0).
- Normal case (
n ≥ 2). Cast each s_i: u64 to i64 (sizes in Partition are bounded by u64::MAX; in practice all canonical examples and reductions in this repo use values well within i64::MAX). Construct target = SumOfSquaresPartition::new(sizes_i64, 2).
- Witness extraction.
extract_solution(target_config) = target_config.to_vec() when its length equals source_n; otherwise (sentinel case) vec![0; source_n].
Why this works (G&J SP19 specialized to K=2). Let S = Σ s_i and let S_1, S_2 be the two group sums under a 2-partition (S_1 + S_2 = S). Then
S_1² + S_2² = (S_1 + S_2)² − 2 S_1 S_2 = S² − 2 S_1 S_2,
which is minimized when S_1 S_2 is maximized, i.e. when S_1 = S_2 = S/2. The minimum is S²/2 (achievable iff S is even and a balanced subset exists).
So:
Partition YES ⇔ some 2-partition of A has S_1 = S_2 ⇔ min Σ (group_sum)² = S²/2 ⇔ the target's optimal witness is a balanced split ⇔ Partition::evaluate(extracted_witness) = Or(true).
Partition NO ⇔ every 2-partition has S_1 ≠ S_2 ⇔ target's optimum strictly exceeds S²/2 ⇔ the optimal witness is unbalanced ⇔ Partition::evaluate(extracted_witness) = Or(false).
This is the standard test pattern used by test_partition_to_multiprocessorscheduling_solution_extraction.
Size Overhead
Symbols:
num_elements = |A| (getter on Partition).
| Target metric (code name) |
Polynomial expression |
num_elements |
num_elements |
num_groups |
2 |
(Field names match SumOfSquaresPartition's size_fields = ["num_elements", "num_groups"]. The macro overhead validator will reject any other name.)
Construction is O(n) time and O(n) space.
Validation Method
- Closed-loop test (balanced even-sum case). Input
Partition::new(vec![3, 1, 1, 2, 2, 1]) (n=6, S=10). Expected: target optimal min = 50 = S²/2; Partition::evaluate(extracted_witness) = Or(true).
- Closed-loop test (odd-sum NO case). Input
Partition::new(vec![2, 4, 5]) (n=3, S=11). Expected: target optimal min > 60.5 (in fact = 61 = 5² + 6² for split {5},{2,4}); Partition::evaluate(extracted_witness) = Or(false).
- Closed-loop test (even-sum but unbalanced NO case). Input
Partition::new(vec![1, 1, 1, 5]) (n=4, S=8). No balanced subset (smallest gap is {5} vs {1,1,1} giving 5 vs 3). Expected: target min > 32; extracted witness fails Partition::evaluate.
- Singleton corner case. Input
Partition::new(vec![5]) (n=1). Sentinel path triggers; extract_solution returns [0]; Partition::evaluate([0]) = Or(false).
- Structure tests. For each non-sentinel input, assert
target.sizes() == source.sizes().map(i64::from), target.num_groups() == 2, target.num_elements() == source.num_elements().
Example
Source (Partition): A = {3, 1, 1, 2, 2, 1} (n = 6, S = 10).
A balanced partition requires each half to sum to S/2 = 5.
Constructed target (SumOfSquaresPartition):
sizes = [3, 1, 1, 2, 2, 1] (as Vec<i64>),
num_groups = 2.
- (No
J field — the bound S²/2 = 50 is an implicit property of the optimum.)
Optimal target witness (one of several): [0, 1, 1, 0, 1, 1] → group 0 sums to 3+2 = 5, group 1 sums to 1+1+2+1 = 5. Target value: 5² + 5² = 50 = S²/2.
Source-side check: Partition::evaluate([0, 1, 1, 0, 1, 1]) = Or(5+5 == 10) = Or(true). ✓
Imbalanced comparison. Witness [0, 0, 0, 1, 0, 0] → groups {3,1,1,2,1} (sum 8) vs {2} (sum 2). Target value: 64 + 4 = 68 > 50. Source-side: Partition::evaluate([0,0,0,1,0,0]) = Or(2 ≠ 8) = Or(false). Confirms only balanced splits achieve the optimum.
Singleton sentinel. Partition::new(vec![5]) → sentinel target SumOfSquaresPartition::new(vec![1,1], 2). extract_solution(any) = [0] (length matches source_n = 1). Partition::evaluate([0]) = Or(false) — correct.
References
- Garey, M. R. and Johnson, D. S. (1979). Computers and Intractability: A Guide to the Theory of NP-Completeness, problem SP19, p. 225. W. H. Freeman.
- C. K. Wong and A. C. Yao (1976). "A combinatorial optimization problem related to data set allocation." Revue Française d'Automatique, Informatique, Recherche Opérationnelle, Sér. Bleue 10(suppl.), pp. 83–95.
Source:
PartitionTarget:
SumOfSquaresPartitionMotivation: Establishes NP-hardness of
SumOfSquaresPartitionfromPartition(one of Karp's 21). The reduction is Garey & Johnson SP19's textbook construction restricted toK = 2: among all 2-way partitions ofA, the sum of squared group sums is minimized when the two groups have equal sums (S/2each), giving minimumS²/2. HencePartitionis YES iff the minimum sum of squares is⌈S²/2⌉(equivalently, ≤S²/2whenSis even).Reference: Garey & Johnson, Computers and Intractability, SP19, p. 225.
GJ Source Entry
Codebase Note: Why This Is a Witness Reduction (No
JField)The
SumOfSquaresPartitionmodel in this codebase is a pure minimization problem withValue = Min<i64>and fields(sizes, num_groups). There is noJbound field and noDecision<SumOfSquaresPartition>registered. So the reduction is implemented in the same witness-style form aspartition_multiprocessorscheduling.rs:SumOfSquaresPartitioninstance encodes the construction (identical sizes,K = 2).ReductionResult::extract_solutionis the identity (the group assignment is a valid Partition subset assignment).source.evaluate(extracted_config)—Partition::evaluatealready returnsOr(true)exactly when the config splits the sum in half. The boundJ = S²/2is therefore an implicit property of the optimal target witness, not a stored field.This matches the precedent set by
partition_multiprocessorscheduling.rsand otherPartition → ...reductions where the target is constructed to make Partition's YES/NO answer recoverable fromsource.evaluate(extract_solution(target_witness)).(A separate follow-up issue may introduce
Decision<SumOfSquaresPartition>with an explicitJbound; this issue does not depend on that.)Reduction Algorithm
Given a
Partitioninstance with sizesA = {s_1, ..., s_n}(s_i > 0, sourceValue = Or), construct aSumOfSquaresPartition(Value = Min<i64>) as follows:n < 2(i.e.K = 2 > nwould violateSumOfSquaresPartition's constructor invariantnum_groups ≤ num_elements), emit a sentinel targetSumOfSquaresPartition::new(vec![1, 1], 2)and remembersource_n.extract_solutionreturnsvec![0; source_n], on whichPartition::evaluatereturnsOr(false)— the correct YES/NO answer because a single positive element cannot be split into two equal-sum groups (andPartition::newalready rejectsn = 0).n ≥ 2). Cast eachs_i: u64toi64(sizes in Partition are bounded byu64::MAX; in practice all canonical examples and reductions in this repo use values well withini64::MAX). Constructtarget = SumOfSquaresPartition::new(sizes_i64, 2).extract_solution(target_config) = target_config.to_vec()when its length equalssource_n; otherwise (sentinel case)vec![0; source_n].Why this works (G&J SP19 specialized to K=2). Let
S = Σ s_iand letS_1, S_2be the two group sums under a 2-partition (S_1 + S_2 = S). ThenS_1² + S_2² = (S_1 + S_2)² − 2 S_1 S_2 = S² − 2 S_1 S_2,which is minimized when
S_1 S_2is maximized, i.e. whenS_1 = S_2 = S/2. The minimum isS²/2(achievable iffSis even and a balanced subset exists).So:
PartitionYES ⇔ some 2-partition ofAhasS_1 = S_2⇔min Σ (group_sum)² = S²/2⇔ the target's optimal witness is a balanced split ⇔Partition::evaluate(extracted_witness) = Or(true).PartitionNO ⇔ every 2-partition hasS_1 ≠ S_2⇔ target's optimum strictly exceedsS²/2⇔ the optimal witness is unbalanced ⇔Partition::evaluate(extracted_witness) = Or(false).This is the standard test pattern used by
test_partition_to_multiprocessorscheduling_solution_extraction.Size Overhead
Symbols:
num_elements=|A|(getter onPartition).num_elementsnum_elementsnum_groups2(Field names match
SumOfSquaresPartition'ssize_fields = ["num_elements", "num_groups"]. The macro overhead validator will reject any other name.)Construction is
O(n)time andO(n)space.Validation Method
Partition::new(vec![3, 1, 1, 2, 2, 1])(n=6, S=10). Expected: target optimal min = 50 = S²/2;Partition::evaluate(extracted_witness) = Or(true).Partition::new(vec![2, 4, 5])(n=3, S=11). Expected: target optimal min > 60.5 (in fact = 61 = 5² + 6² for split {5},{2,4});Partition::evaluate(extracted_witness) = Or(false).Partition::new(vec![1, 1, 1, 5])(n=4, S=8). No balanced subset (smallest gap is{5} vs {1,1,1}giving 5 vs 3). Expected: target min > 32; extracted witness failsPartition::evaluate.Partition::new(vec![5])(n=1). Sentinel path triggers;extract_solutionreturns[0];Partition::evaluate([0]) = Or(false).target.sizes() == source.sizes().map(i64::from),target.num_groups() == 2,target.num_elements() == source.num_elements().Example
Source (
Partition):A = {3, 1, 1, 2, 2, 1}(n = 6, S = 10).A balanced partition requires each half to sum to
S/2 = 5.Constructed target (
SumOfSquaresPartition):sizes = [3, 1, 1, 2, 2, 1](asVec<i64>),num_groups = 2.Jfield — the boundS²/2 = 50is an implicit property of the optimum.)Optimal target witness (one of several):
[0, 1, 1, 0, 1, 1]→ group 0 sums to3+2 = 5, group 1 sums to1+1+2+1 = 5. Target value:5² + 5² = 50 = S²/2.Source-side check:
Partition::evaluate([0, 1, 1, 0, 1, 1]) = Or(5+5 == 10) = Or(true). ✓Imbalanced comparison. Witness
[0, 0, 0, 1, 0, 0]→ groups{3,1,1,2,1}(sum 8) vs{2}(sum 2). Target value:64 + 4 = 68 > 50. Source-side:Partition::evaluate([0,0,0,1,0,0]) = Or(2 ≠ 8) = Or(false). Confirms only balanced splits achieve the optimum.Singleton sentinel.
Partition::new(vec![5])→ sentinel targetSumOfSquaresPartition::new(vec![1,1], 2).extract_solution(any) = [0](length matchessource_n = 1).Partition::evaluate([0]) = Or(false)— correct.References