Skip to main content

problemreductions/models/misc/
subset_product.rs

1//! Subset Product problem implementation.
2//!
3//! Given a set of positive integers and a target value, the problem asks whether
4//! any subset's product equals exactly the target. A multiplicative analogue of
5//! Subset Sum; NP-complete (see e.g. Garey & Johnson, 1979).
6//!
7//! This implementation uses arbitrary-precision integers (`BigUint`) so
8//! reductions can construct large instances without fixed-width overflow.
9
10use crate::registry::{FieldInfo, ProblemSchemaEntry};
11use crate::traits::Problem;
12use num_bigint::{BigUint, ToBigUint};
13use num_traits::{One, Zero};
14use serde::{Deserialize, Serialize};
15
16inventory::submit! {
17    ProblemSchemaEntry {
18        name: "SubsetProduct",
19        display_name: "Subset Product",
20        aliases: &[],
21        dimensions: &[],
22        category: crate::registry::ProblemCategory::Misc,
23        module_path: module_path!(),
24        description: "Find a subset of positive integers whose product equals exactly a target value",
25        fields: &[
26            FieldInfo { name: "sizes", type_name: "Vec<BigUint>", description: "Positive integer sizes s(a) for each element" },
27            FieldInfo { name: "target", type_name: "BigUint", description: "Target product B" },
28        ],
29    }
30}
31
32/// The Subset Product problem.
33///
34/// Given a set of `n` positive integers and a target `B`, determine whether
35/// there exists a subset whose elements multiply to exactly `B`.
36///
37/// # Representation
38///
39/// Each element has a binary variable: `x_i = 1` if element `i` is selected,
40/// `0` otherwise. The problem is satisfiable iff `∏_{i: x_i=1} sizes[i] == target`.
41///
42/// # Example
43///
44/// ```
45/// use problemreductions::models::misc::SubsetProduct;
46/// use problemreductions::{Problem, BruteForce};
47///
48/// let problem = SubsetProduct::new(vec![2u32, 3, 5, 7, 6, 10], 210u32);
49/// let solver = BruteForce::new();
50/// let solution = solver.solve(&problem).unwrap();
51/// assert!(solution.is_some());
52/// ```
53#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(try_from = "SubsetProductData")]
55pub struct SubsetProduct {
56    #[serde(with = "super::biguint_serde::decimal_biguint_vec")]
57    sizes: Vec<BigUint>,
58    #[serde(with = "super::biguint_serde::decimal_biguint")]
59    target: BigUint,
60}
61
62#[derive(Deserialize)]
63struct SubsetProductData {
64    #[serde(with = "super::biguint_serde::decimal_biguint_vec")]
65    sizes: Vec<BigUint>,
66    #[serde(with = "super::biguint_serde::decimal_biguint")]
67    target: BigUint,
68}
69
70impl TryFrom<SubsetProductData> for SubsetProduct {
71    type Error = crate::registry::ConstructionError;
72    fn try_from(data: SubsetProductData) -> Result<Self, Self::Error> {
73        if data.sizes.iter().any(BigUint::is_zero) {
74            return Err("all sizes must be positive (> 0)".into());
75        }
76        if data.target.is_zero() {
77            return Err("SubsetProduct target must be positive".into());
78        }
79        Ok(Self {
80            sizes: data.sizes,
81            target: data.target,
82        })
83    }
84}
85
86impl SubsetProduct {
87    /// Create a new SubsetProduct instance.
88    ///
89    /// # Panics
90    ///
91    /// Panics if any size is not positive (must be > 0) or if target is zero.
92    pub fn new<S, T>(sizes: Vec<S>, target: T) -> Self
93    where
94        S: ToBigUint,
95        T: ToBigUint,
96    {
97        Self::try_new(sizes, target).unwrap_or_else(|error| panic!("{error}"))
98    }
99
100    fn try_new<S, T>(sizes: Vec<S>, target: T) -> Result<Self, crate::registry::ConstructionError>
101    where
102        S: ToBigUint,
103        T: ToBigUint,
104    {
105        let sizes = sizes
106            .into_iter()
107            .map(|size| size.to_biguint().ok_or("all sizes must be positive (> 0)"))
108            .collect::<Result<Vec<_>, _>>()?;
109        let target = target
110            .to_biguint()
111            .ok_or("SubsetProduct target must be nonnegative")?;
112        SubsetProductData { sizes, target }.try_into()
113    }
114
115    /// Create a SubsetProduct without validating sizes (for testing edge cases).
116    #[cfg(test)]
117    pub(crate) fn new_unchecked(sizes: Vec<BigUint>, target: BigUint) -> Self {
118        Self { sizes, target }
119    }
120
121    /// Returns the element sizes.
122    pub fn sizes(&self) -> &[BigUint] {
123        &self.sizes
124    }
125
126    /// Returns the target product.
127    pub fn target(&self) -> &BigUint {
128        &self.target
129    }
130
131    /// Returns the number of elements.
132    pub fn num_elements(&self) -> usize {
133        self.sizes.len()
134    }
135}
136
137impl Problem for SubsetProduct {
138    const NAME: &'static str = "SubsetProduct";
139    type Solution = Vec<bool>;
140    type Value = crate::types::Or;
141
142    crate::problem_parameters![("num_elements", num_elements),];
143
144    fn variant() -> Vec<(&'static str, &'static str)> {
145        crate::variant_params![]
146    }
147
148    fn evaluate(
149        &self,
150        config: &Self::Solution,
151    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
152        Ok({
153            crate::types::Or({
154                if config.len() != self.num_elements() {
155                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
156                        "subset-selection length does not match the elements".into(),
157                    ));
158                }
159                let mut product = BigUint::one();
160                for (i, &x) in config.iter().enumerate() {
161                    if x {
162                        product *= &self.sizes[i];
163                        if product > self.target {
164                            return Ok(crate::types::Or(false));
165                        }
166                    }
167                }
168                product == self.target
169            })
170        })
171    }
172}
173
174impl crate::solvers::BruteForceProblem for SubsetProduct {
175    fn dimensions(&self) -> Vec<usize> {
176        vec![2; self.num_elements()]
177    }
178}
179
180crate::declare_variants! {
181    default SubsetProduct => "2^num_elements",
182}
183
184crate::register_brute_force! {
185    SubsetProduct decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
186}
187
188#[cfg(feature = "example-db")]
189pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
190    // 6 elements [2,3,5,7,6,10], target 210 → select {2,3,5,7}
191    vec![crate::example_db::specs::ModelExampleSpec {
192        id: "subset_product",
193        instance: Box::new(SubsetProduct::new(vec![2u32, 3, 5, 7, 6, 10], 210u32)),
194        optimal_config: serde_json::json!(vec![true, true, true, true, false, false]),
195        optimal_value: serde_json::json!(true),
196    }]
197}
198
199#[cfg(test)]
200#[path = "../../unit_tests/models/misc/subset_product.rs"]
201mod tests;