Skip to content

Commit f3204d5

Browse files
GiggleLiuisPANN
andauthored
Fix #125: [Rule] SubsetSum to ClosestVectorProblem (#709)
* Add plan for #125: [Rule] SubsetSum to ClosestVectorProblem * Implement #125: [Rule] SubsetSum to ClosestVectorProblem * chore: remove plan file after implementation --------- Co-authored-by: Xiwei Pan <90967972+isPANN@users.noreply.github.com>
1 parent 54aca98 commit f3204d5

5 files changed

Lines changed: 230 additions & 0 deletions

File tree

docs/paper/reductions.typ

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4586,6 +4586,50 @@ where $P$ is a penalty weight large enough that any constraint violation costs m
45864586
_Solution extraction._ For each $i$: if $y_i$ is selected ($x_(2i) = 1$), set $x_i = 1$; if $z_i$ is selected ($x_(2i+1) = 1$), set $x_i = 0$.
45874587
]
45884588

4589+
#{
4590+
let ss-cvp = load-example("SubsetSum", "ClosestVectorProblem")
4591+
let ss-cvp-sol = ss-cvp.solutions.at(0)
4592+
let ss-cvp-sizes = ss-cvp.source.instance.sizes
4593+
let ss-cvp-target = ss-cvp.source.instance.target
4594+
let ss-cvp-basis = ss-cvp.target.instance.basis
4595+
let ss-cvp-target-vec = ss-cvp.target.instance.target
4596+
let ss-cvp-n = ss-cvp-sizes.len()
4597+
let ss-cvp-x = ss-cvp-sol.target_config
4598+
let to-mat(m) = math.mat(..m.map(row => row.map(v => $#v$)))
4599+
[
4600+
#reduction-rule("SubsetSum", "ClosestVectorProblem",
4601+
example: true,
4602+
example-caption: [#ss-cvp-n elements, target sum $B = #ss-cvp-target$],
4603+
extra: [
4604+
*Step 1 -- Source instance.* The canonical Subset Sum instance has sizes $(#ss-cvp-sizes.map(str).join(", "))$ and target $B = #ss-cvp-target$.
4605+
4606+
*Step 2 -- Build the lattice.* The reduction creates the basis
4607+
$ bold(B) = #to-mat(ss-cvp-basis) $
4608+
together with target $ bold(t) = (#ss-cvp-target-vec.map(str).join(", "))^top $
4609+
and binary bounds $x_i in {0,1}$ for all $#ss-cvp-n$ coordinates.
4610+
4611+
*Step 3 -- Verify the canonical witness.* The fixture stores $bold(x) = (#ss-cvp-x.map(str).join(", "))$, which selects sizes $3$ and $8$ and therefore satisfies $3 + 8 = #ss-cvp-target$. Since $bold(B) bold(x) = (1, 0, 0, 1, #ss-cvp-target)^top$, the difference vector is $(0.5, -0.5, -0.5, 0.5, 0)^top$ and the Euclidean distance is $sqrt(#ss-cvp-n / 4) = 1$.
4612+
4613+
*Witness semantics.* The example DB stores one canonical minimizer. This source instance also has another satisfying subset, $(1, 1, 1, 0)$, so the reduction has multiple optimal CVP witnesses even though only one is serialized.
4614+
],
4615+
)[
4616+
Classical lattice embedding for Subset Sum following Lagarias and Odlyzko @lagarias1985, with the $1/2$-target CVP formulation in the style of Coster et al. @coster1992. For an instance with $n$ elements, the reduction produces $n$ basis vectors in ambient dimension $n + 1$: the first $n$ coordinates enforce binary structure and the last coordinate records the subset sum error.
4617+
][
4618+
_Construction._ Given sizes $s_0, dots, s_(n-1) in ZZ^+$ and target $B in ZZ^+$, define one basis vector per element:
4619+
$ bold(b)_i = bold(e)_i + s_i bold(e)_(n+1) $
4620+
for $i in {0, dots, n-1}$. Equivalently, the basis matrix has columns $bold(b)_0, dots, bold(b)_(n-1)$, so its first $n$ rows form the identity matrix and its last row is $(s_0, dots, s_(n-1))$. Set the target vector to
4621+
$ bold(t) = (1/2, dots, 1/2, B)^top $
4622+
and restrict every CVP variable to $x_i in {0, 1}$.
4623+
4624+
_Correctness._ ($arrow.r.double$) If $bold(x) in {0,1}^n$ is a satisfying Subset Sum solution, then $sum_i s_i x_i = B$ and
4625+
$ norm(bold(B) bold(x) - bold(t))_2^2 = sum_(i=0)^(n-1) (x_i - 1/2)^2 + (sum_i s_i x_i - B)^2 = n/4. $
4626+
Hence every satisfying subset becomes a CVP solution at distance $sqrt(n / 4)$. ($arrow.l.double$) Conversely, binary bounds force every CVP candidate to lie in ${0,1}^n$. The first $n$ coordinates always contribute exactly $n/4$ to the squared distance, so a CVP minimizer attains distance $sqrt(n/4)$ if and only if the last coordinate contributes $0$, i.e. $sum_i s_i x_i = B$. When the Subset Sum instance is unsatisfiable, every binary vector has strictly larger distance.
4627+
4628+
_Solution extraction._ Return the binary CVP vector unchanged.
4629+
]
4630+
]
4631+
}
4632+
45894633
#reduction-rule("ILP", "QUBO")[
45904634
A binary ILP optimizes a linear objective over binary variables subject to linear constraints. The penalty method converts each equality constraint $bold(a)_k^top bold(x) = b_k$ into the quadratic penalty $(bold(a)_k^top bold(x) - b_k)^2$, which is zero if and only if the constraint is satisfied. Inequality constraints are first converted to equalities using binary slack variables with powers-of-two coefficients. The resulting unconstrained quadratic over binary variables is a QUBO whose matrix $Q$ combines the negated objective (as diagonal terms) with the expanded constraint penalties (as a Gram matrix $A^top A$).
45914635
][

docs/paper/references.bib

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,28 @@ @inproceedings{karp1972
143143
pages = {85--103}
144144
}
145145

146+
@article{lagarias1985,
147+
author = {Jeffrey C. Lagarias and Andrew M. Odlyzko},
148+
title = {Solving Low-Density Subset Sum Problems},
149+
journal = {Journal of the ACM},
150+
volume = {32},
151+
number = {1},
152+
pages = {229--246},
153+
year = {1985},
154+
doi = {10.1145/2455.2461}
155+
}
156+
157+
@article{coster1992,
158+
author = {Matthijs J. Coster and Antoine Joux and Brian A. LaMacchia and Andrew M. Odlyzko and Claus-Peter Schnorr and Jacques Stern},
159+
title = {Improved Low-Density Subset Sum Algorithms},
160+
journal = {Computational Complexity},
161+
volume = {2},
162+
number = {2},
163+
pages = {111--128},
164+
year = {1992},
165+
doi = {10.1007/BF01201999}
166+
}
167+
146168
@inproceedings{cook1971,
147169
author = {Stephen A. Cook},
148170
title = {The Complexity of Theorem-Proving Procedures},

src/rules/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ pub(crate) mod sat_minimumdominatingset;
3737
mod spinglass_casts;
3838
pub(crate) mod spinglass_maxcut;
3939
pub(crate) mod spinglass_qubo;
40+
pub(crate) mod subsetsum_closestvectorproblem;
4041
#[cfg(test)]
4142
pub(crate) mod test_helpers;
4243
mod traits;
@@ -115,6 +116,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
115116
specs.extend(sat_minimumdominatingset::canonical_rule_example_specs());
116117
specs.extend(spinglass_maxcut::canonical_rule_example_specs());
117118
specs.extend(spinglass_qubo::canonical_rule_example_specs());
119+
specs.extend(subsetsum_closestvectorproblem::canonical_rule_example_specs());
118120
specs.extend(travelingsalesman_qubo::canonical_rule_example_specs());
119121
#[cfg(feature = "ilp-solver")]
120122
{
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//! Reduction from Subset Sum to Closest Vector Problem.
2+
3+
use crate::models::algebraic::{ClosestVectorProblem, VarBounds};
4+
use crate::models::misc::SubsetSum;
5+
use crate::reduction;
6+
use crate::rules::traits::{ReduceTo, ReductionResult};
7+
use num_bigint::BigUint;
8+
use num_traits::ToPrimitive;
9+
10+
/// Result of reducing SubsetSum to ClosestVectorProblem.
11+
#[derive(Debug, Clone)]
12+
pub struct ReductionSubsetSumToClosestVectorProblem {
13+
target: ClosestVectorProblem<i32>,
14+
}
15+
16+
impl ReductionResult for ReductionSubsetSumToClosestVectorProblem {
17+
type Source = SubsetSum;
18+
type Target = ClosestVectorProblem<i32>;
19+
20+
fn target_problem(&self) -> &Self::Target {
21+
&self.target
22+
}
23+
24+
fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
25+
target_solution.to_vec()
26+
}
27+
}
28+
29+
fn biguint_to_i32(value: &BigUint) -> i32 {
30+
value
31+
.to_i32()
32+
.expect("SubsetSum -> ClosestVectorProblem requires all sizes and target to fit in i32")
33+
}
34+
35+
#[reduction(
36+
overhead = {
37+
ambient_dimension = "num_elements + 1",
38+
num_basis_vectors = "num_elements",
39+
}
40+
)]
41+
impl ReduceTo<ClosestVectorProblem<i32>> for SubsetSum {
42+
type Result = ReductionSubsetSumToClosestVectorProblem;
43+
44+
fn reduce_to(&self) -> Self::Result {
45+
let n = self.num_elements();
46+
let mut basis = Vec::with_capacity(n);
47+
for (i, size) in self.sizes().iter().enumerate() {
48+
let mut column = vec![0i32; n + 1];
49+
column[i] = 1;
50+
column[n] = biguint_to_i32(size);
51+
basis.push(column);
52+
}
53+
54+
let mut target = vec![0.5; n];
55+
target.push(biguint_to_i32(self.target()) as f64);
56+
57+
ReductionSubsetSumToClosestVectorProblem {
58+
target: ClosestVectorProblem::new(basis, target, vec![VarBounds::binary(); n]),
59+
}
60+
}
61+
}
62+
63+
#[cfg(feature = "example-db")]
64+
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
65+
use crate::export::SolutionPair;
66+
67+
vec![crate::example_db::specs::RuleExampleSpec {
68+
id: "subsetsum_to_closestvectorproblem",
69+
build: || {
70+
crate::example_db::specs::rule_example_with_witness::<_, ClosestVectorProblem<i32>>(
71+
SubsetSum::new(vec![3u32, 7, 1, 8], 11u32),
72+
SolutionPair {
73+
source_config: vec![1, 0, 0, 1],
74+
target_config: vec![1, 0, 0, 1],
75+
},
76+
)
77+
},
78+
}]
79+
}
80+
81+
#[cfg(test)]
82+
#[path = "../unit_tests/rules/subsetsum_closestvectorproblem.rs"]
83+
mod tests;
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
use super::*;
2+
use crate::models::algebraic::{ClosestVectorProblem, VarBounds};
3+
use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target;
4+
use crate::solvers::{BruteForce, Solver};
5+
use crate::traits::Problem;
6+
use crate::types::SolutionSize;
7+
use std::collections::HashSet;
8+
9+
#[test]
10+
fn test_subsetsum_to_closestvectorproblem_closed_loop() {
11+
let source = SubsetSum::new(vec![3u32, 7, 1, 8], 11u32);
12+
let reduction = ReduceTo::<ClosestVectorProblem<i32>>::reduce_to(&source);
13+
let target = reduction.target_problem();
14+
15+
assert_eq!(target.num_basis_vectors(), 4);
16+
assert_eq!(target.ambient_dimension(), 5);
17+
assert_eq!(target.bounds(), &[VarBounds::binary(); 4]);
18+
19+
assert_satisfaction_round_trip_from_optimization_target(
20+
&source,
21+
&reduction,
22+
"SubsetSum -> ClosestVectorProblem closed loop",
23+
);
24+
}
25+
26+
#[test]
27+
fn test_subsetsum_to_closestvectorproblem_structure() {
28+
let source = SubsetSum::new(vec![3u32, 7, 1, 8], 11u32);
29+
let reduction = ReduceTo::<ClosestVectorProblem<i32>>::reduce_to(&source);
30+
let target = reduction.target_problem();
31+
32+
assert_eq!(target.basis()[0], vec![1, 0, 0, 0, 3]);
33+
assert_eq!(target.basis()[1], vec![0, 1, 0, 0, 7]);
34+
assert_eq!(target.basis()[2], vec![0, 0, 1, 0, 1]);
35+
assert_eq!(target.basis()[3], vec![0, 0, 0, 1, 8]);
36+
assert_eq!(target.target(), &[0.5, 0.5, 0.5, 0.5, 11.0]);
37+
}
38+
39+
#[test]
40+
fn test_subsetsum_to_closestvectorproblem_issue_example_minimizers() {
41+
let source = SubsetSum::new(vec![3u32, 7, 1, 8], 11u32);
42+
let reduction = ReduceTo::<ClosestVectorProblem<i32>>::reduce_to(&source);
43+
let target = reduction.target_problem();
44+
let solutions: HashSet<Vec<usize>> = BruteForce::new()
45+
.find_all_best(target)
46+
.into_iter()
47+
.collect();
48+
49+
let expected: HashSet<Vec<usize>> = [vec![1, 0, 0, 1], vec![1, 1, 1, 0]].into_iter().collect();
50+
assert_eq!(solutions, expected);
51+
52+
for solution in &solutions {
53+
assert_eq!(target.evaluate(solution), SolutionSize::Valid(1.0));
54+
}
55+
}
56+
57+
#[test]
58+
fn test_subsetsum_to_closestvectorproblem_unsatisfiable_instance() {
59+
let source = SubsetSum::new(vec![2u32, 4, 6], 5u32);
60+
let reduction = ReduceTo::<ClosestVectorProblem<i32>>::reduce_to(&source);
61+
let target = reduction.target_problem();
62+
let best = BruteForce::new()
63+
.find_best(target)
64+
.expect("unsatisfiable instance should still have a best CVP assignment");
65+
66+
match target.evaluate(&best) {
67+
SolutionSize::Valid(value) => assert!(value > (source.num_elements() as f64).sqrt() / 2.0),
68+
SolutionSize::Invalid => panic!("CVP solution should be valid"),
69+
}
70+
}
71+
72+
#[test]
73+
#[should_panic(
74+
expected = "SubsetSum -> ClosestVectorProblem requires all sizes and target to fit in i32"
75+
)]
76+
fn test_subsetsum_to_closestvectorproblem_panics_on_large_coefficients() {
77+
let source = SubsetSum::new(vec![(i32::MAX as u64) + 1], 1u64);
78+
let _ = ReduceTo::<ClosestVectorProblem<i32>>::reduce_to(&source);
79+
}

0 commit comments

Comments
 (0)