Skip to content

Commit 102b941

Browse files
isPANNclaudezazabap
authored
Fix #532: [Model] IntegerKnapsack (#815)
* feat: add IntegerKnapsack model (unbounded knapsack with integer multiplicities) Closes #532. Adds IntegerKnapsack to `src/models/set/` with Max<i64> value type, per-item variable domains {0,...,floor(B/s_i)}, CLI create support, canonical example, paper entry, and 20 unit tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix deserialization validation and CLI input validation for IntegerKnapsack Use TryFrom<RawIntegerKnapsack> pattern to validate cross-field invariants (sizes/values length match) during deserialization, preventing panics from malformed JSON. Add anyhow::ensure! checks in CLI create path for friendly error messages instead of assertion panics on invalid user input. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: zazabap <sweynan@icloud.com>
1 parent 6a4f5c7 commit 102b941

File tree

8 files changed

+529
-4
lines changed

8 files changed

+529
-4
lines changed

docs/paper/reductions.typ

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
"SpinGlass": [Spin Glass],
112112
"QUBO": [QUBO],
113113
"ILP": [Integer Linear Programming],
114+
"IntegerKnapsack": [Integer Knapsack],
114115
"Knapsack": [Knapsack],
115116
"PartiallyOrderedKnapsack": [Partially Ordered Knapsack],
116117
"Satisfiability": [SAT],
@@ -4023,6 +4024,33 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
40234024
]
40244025
}
40254026

4027+
#{
4028+
let x = load-model-example("IntegerKnapsack")
4029+
let sizes = x.instance.sizes
4030+
let values = x.instance.values
4031+
let B = x.instance.capacity
4032+
let n = sizes.len()
4033+
let config = x.optimal_config
4034+
let opt-val = metric-value(x.optimal_value)
4035+
let total-s = range(n).map(i => config.at(i) * sizes.at(i)).sum()
4036+
let total-v = range(n).map(i => config.at(i) * values.at(i)).sum()
4037+
[
4038+
#problem-def("IntegerKnapsack")[
4039+
Given $n$ items with sizes $s_0, dots, s_(n-1) in ZZ^+$ and values $v_0, dots, v_(n-1) in ZZ^+$, and a capacity $B in NN$, find non-negative integer multiplicities $c_0, dots, c_(n-1) in NN$ maximizing $sum_(i=0)^(n-1) c_i dot v_i$ subject to $sum_(i=0)^(n-1) c_i dot s_i lt.eq B$.
4040+
][
4041+
The Integer Knapsack (also called the _unbounded knapsack problem_) generalizes the 0-1 Knapsack by allowing each item to be selected more than once. Like 0-1 Knapsack, it admits a pseudo-polynomial $O(n B)$ dynamic-programming algorithm @garey1979. The problem is weakly NP-hard: when item sizes are bounded by a polynomial in $n$, DP runs in polynomial time. The brute-force approach enumerates all multiplicity vectors, giving $O(product_(i=0)^(n-1)(floor.l B slash s_i floor.r + 1))$ configurations.#footnote[No algorithm improving on brute-force enumeration of multiplicity vectors is known for the general Integer Knapsack problem.]
4042+
4043+
*Example.* Let $n = #n$ items with sizes $(#sizes.map(s => str(s)).join(", "))$, values $(#values.map(v => str(v)).join(", "))$, and capacity $B = #B$. Setting multiplicities $(#config.map(c => str(c)).join(", "))$ gives total size $#total-s lt.eq B$ and total value $#total-v$, which is optimal.
4044+
4045+
#pred-commands(
4046+
"pred create --example IntegerKnapsack -o ik.json",
4047+
"pred solve ik.json",
4048+
"pred evaluate ik.json --config " + x.optimal_config.map(str).join(","),
4049+
)
4050+
]
4051+
]
4052+
}
4053+
40264054
#problem-def("PartiallyOrderedKnapsack")[
40274055
Given $n$ items with weights $w_0, dots, w_(n-1) in NN$ and values $v_0, dots, v_(n-1) in NN$, a partial order $prec$ on the items (given by its cover relations), and a capacity $C in NN$, find a downward-closed subset $S subset.eq {0, dots, n - 1}$ (i.e., if $i in S$ and $j prec i$ then $j in S$) maximizing $sum_(i in S) v_i$ subject to $sum_(i in S) w_i lt.eq C$.
40284056
][

problemreductions-cli/src/cli.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ Flags by problem type:
297297
SteinerTreeInGraphs --graph, --edge-weights, --terminals
298298
PartitionIntoPathsOfLength2 --graph
299299
ResourceConstrainedScheduling --num-processors, --resource-bounds, --resource-requirements, --deadline
300+
IntegerKnapsack --sizes, --values, --capacity
300301
PartiallyOrderedKnapsack --sizes, --values, --capacity, --precedences
301302
QAP --matrix (cost), --distance-matrix
302303
StrongConnectivityAugmentation --arcs, --candidate-arcs, --bound [--num-vertices]

problemreductions-cli/src/commands/create.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
725725
"SequencingToMinimizeWeightedTardiness" => {
726726
"--sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13"
727727
}
728+
"IntegerKnapsack" => "--sizes 3,4,5,2,7 --values 4,5,7,3,9 --capacity 15",
728729
"SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11",
729730
"ThreePartition" => "--sizes 4,5,6,4,6,5 --bound 15",
730731
"BoyceCoddNormalFormViolation" => {
@@ -2333,6 +2334,41 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
23332334
)
23342335
}
23352336

2337+
// IntegerKnapsack
2338+
"IntegerKnapsack" => {
2339+
let sizes_str = args.sizes.as_deref().ok_or_else(|| {
2340+
anyhow::anyhow!(
2341+
"IntegerKnapsack requires --sizes, --values, and --capacity\n\n\
2342+
Usage: pred create IntegerKnapsack --sizes 3,4,5,2,7 --values 4,5,7,3,9 --capacity 15"
2343+
)
2344+
})?;
2345+
let values_str = args.values.as_deref().ok_or_else(|| {
2346+
anyhow::anyhow!("IntegerKnapsack requires --values (e.g., 4,5,7,3,9)")
2347+
})?;
2348+
let cap_str = args
2349+
.capacity
2350+
.as_deref()
2351+
.ok_or_else(|| anyhow::anyhow!("IntegerKnapsack requires --capacity (e.g., 15)"))?;
2352+
let sizes: Vec<i64> = util::parse_comma_list(sizes_str)?;
2353+
let values: Vec<i64> = util::parse_comma_list(values_str)?;
2354+
let capacity: i64 = cap_str.parse()?;
2355+
anyhow::ensure!(
2356+
sizes.len() == values.len(),
2357+
"sizes and values must have the same length, got {} and {}",
2358+
sizes.len(),
2359+
values.len()
2360+
);
2361+
anyhow::ensure!(sizes.iter().all(|&s| s > 0), "all sizes must be positive");
2362+
anyhow::ensure!(values.iter().all(|&v| v > 0), "all values must be positive");
2363+
anyhow::ensure!(capacity >= 0, "capacity must be nonnegative");
2364+
(
2365+
ser(problemreductions::models::set::IntegerKnapsack::new(
2366+
sizes, values, capacity,
2367+
))?,
2368+
resolved_variant.clone(),
2369+
)
2370+
}
2371+
23362372
// SubsetSum
23372373
"SubsetSum" => {
23382374
let sizes_str = args.sizes.as_deref().ok_or_else(|| {

src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,9 @@ pub mod prelude {
8383
TimetableDesign,
8484
};
8585
pub use crate::models::set::{
86-
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking,
87-
MinimumCardinalityKey, MinimumHittingSet, MinimumSetCovering, PrimeAttributeName,
88-
RootedTreeStorageAssignment, SetBasis,
86+
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, IntegerKnapsack,
87+
MaximumSetPacking, MinimumCardinalityKey, MinimumHittingSet, MinimumSetCovering,
88+
PrimeAttributeName, RootedTreeStorageAssignment, SetBasis,
8989
};
9090

9191
// Core traits

src/models/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ pub use misc::{
5050
Term, ThreePartition, TimetableDesign,
5151
};
5252
pub use set::{
53-
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking,
53+
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, IntegerKnapsack, MaximumSetPacking,
5454
MinimumCardinalityKey, MinimumHittingSet, MinimumSetCovering, PrimeAttributeName,
5555
RootedTreeStorageAssignment, SetBasis, TwoDimensionalConsecutiveSets,
5656
};

src/models/set/integer_knapsack.rs

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
//! Integer Knapsack problem implementation.
2+
//!
3+
//! The Integer Knapsack problem generalizes the 0-1 Knapsack by allowing
4+
//! each item to be selected with a non-negative integer multiplicity.
5+
6+
use crate::registry::{FieldInfo, ProblemSchemaEntry};
7+
use crate::traits::Problem;
8+
use crate::types::Max;
9+
use serde::{Deserialize, Serialize};
10+
11+
inventory::submit! {
12+
ProblemSchemaEntry {
13+
name: "IntegerKnapsack",
14+
display_name: "Integer Knapsack",
15+
aliases: &[],
16+
dimensions: &[],
17+
module_path: module_path!(),
18+
description: "Select items with integer multiplicities to maximize total value subject to capacity constraint",
19+
fields: &[
20+
FieldInfo { name: "sizes", type_name: "Vec<i64>", description: "Positive item sizes s(u)" },
21+
FieldInfo { name: "values", type_name: "Vec<i64>", description: "Positive item values v(u)" },
22+
FieldInfo { name: "capacity", type_name: "i64", description: "Nonnegative knapsack capacity B" },
23+
],
24+
}
25+
}
26+
27+
/// The Integer Knapsack problem.
28+
///
29+
/// Given `n` items, each with positive size `s_i` and positive value `v_i`,
30+
/// and a nonnegative capacity `B`,
31+
/// find non-negative integer multiplicities `c_0, ..., c_{n-1}` such that
32+
/// `sum c_i * s_i <= B`, maximizing `sum c_i * v_i`.
33+
///
34+
/// # Representation
35+
///
36+
/// Variable `i` has domain `{0, ..., floor(B / s_i)}` representing the
37+
/// multiplicity of item `i`.
38+
///
39+
/// # Example
40+
///
41+
/// ```
42+
/// use problemreductions::models::set::IntegerKnapsack;
43+
/// use problemreductions::{Problem, Solver, BruteForce};
44+
///
45+
/// let problem = IntegerKnapsack::new(vec![3, 4, 5, 2, 7], vec![4, 5, 7, 3, 9], 15);
46+
/// let solver = BruteForce::new();
47+
/// let solution = solver.find_witness(&problem);
48+
/// assert!(solution.is_some());
49+
/// ```
50+
#[derive(Debug, Clone, Serialize)]
51+
#[serde(into = "RawIntegerKnapsack")]
52+
pub struct IntegerKnapsack {
53+
sizes: Vec<i64>,
54+
values: Vec<i64>,
55+
capacity: i64,
56+
}
57+
58+
impl IntegerKnapsack {
59+
/// Create a new IntegerKnapsack instance.
60+
///
61+
/// # Panics
62+
/// Panics if `sizes` and `values` have different lengths, or if any
63+
/// size or value is not positive, or if capacity is negative.
64+
pub fn new(sizes: Vec<i64>, values: Vec<i64>, capacity: i64) -> Self {
65+
assert_eq!(
66+
sizes.len(),
67+
values.len(),
68+
"sizes and values must have the same length"
69+
);
70+
assert!(
71+
sizes.iter().all(|&s| s > 0),
72+
"IntegerKnapsack sizes must be positive"
73+
);
74+
assert!(
75+
values.iter().all(|&v| v > 0),
76+
"IntegerKnapsack values must be positive"
77+
);
78+
assert!(
79+
capacity >= 0,
80+
"IntegerKnapsack capacity must be nonnegative"
81+
);
82+
Self {
83+
sizes,
84+
values,
85+
capacity,
86+
}
87+
}
88+
89+
/// Returns the item sizes.
90+
pub fn sizes(&self) -> &[i64] {
91+
&self.sizes
92+
}
93+
94+
/// Returns the item values.
95+
pub fn values(&self) -> &[i64] {
96+
&self.values
97+
}
98+
99+
/// Returns the knapsack capacity.
100+
pub fn capacity(&self) -> i64 {
101+
self.capacity
102+
}
103+
104+
/// Returns the number of items.
105+
pub fn num_items(&self) -> usize {
106+
self.sizes.len()
107+
}
108+
}
109+
110+
impl Problem for IntegerKnapsack {
111+
const NAME: &'static str = "IntegerKnapsack";
112+
type Value = Max<i64>;
113+
114+
fn variant() -> Vec<(&'static str, &'static str)> {
115+
crate::variant_params![]
116+
}
117+
118+
fn dims(&self) -> Vec<usize> {
119+
self.sizes
120+
.iter()
121+
.map(|&s| (self.capacity / s + 1) as usize)
122+
.collect()
123+
}
124+
125+
fn evaluate(&self, config: &[usize]) -> Max<i64> {
126+
if config.len() != self.num_items() {
127+
return Max(None);
128+
}
129+
let dims = self.dims();
130+
if config.iter().zip(dims.iter()).any(|(&c, &d)| c >= d) {
131+
return Max(None);
132+
}
133+
let total_size: i64 = config
134+
.iter()
135+
.enumerate()
136+
.map(|(i, &c)| c as i64 * self.sizes[i])
137+
.sum();
138+
if total_size > self.capacity {
139+
return Max(None);
140+
}
141+
let total_value: i64 = config
142+
.iter()
143+
.enumerate()
144+
.map(|(i, &c)| c as i64 * self.values[i])
145+
.sum();
146+
Max(Some(total_value))
147+
}
148+
}
149+
150+
crate::declare_variants! {
151+
default IntegerKnapsack => "(capacity + 1)^num_items",
152+
}
153+
154+
/// Raw representation for serde deserialization with full validation.
155+
#[derive(Deserialize, Serialize)]
156+
struct RawIntegerKnapsack {
157+
sizes: Vec<i64>,
158+
values: Vec<i64>,
159+
capacity: i64,
160+
}
161+
162+
impl From<IntegerKnapsack> for RawIntegerKnapsack {
163+
fn from(ik: IntegerKnapsack) -> Self {
164+
RawIntegerKnapsack {
165+
sizes: ik.sizes,
166+
values: ik.values,
167+
capacity: ik.capacity,
168+
}
169+
}
170+
}
171+
172+
impl TryFrom<RawIntegerKnapsack> for IntegerKnapsack {
173+
type Error = String;
174+
175+
fn try_from(raw: RawIntegerKnapsack) -> Result<Self, String> {
176+
if raw.sizes.len() != raw.values.len() {
177+
return Err(format!(
178+
"sizes and values must have the same length, got {} and {}",
179+
raw.sizes.len(),
180+
raw.values.len()
181+
));
182+
}
183+
if let Some(&s) = raw.sizes.iter().find(|&&s| s <= 0) {
184+
return Err(format!("expected positive sizes, got {s}"));
185+
}
186+
if let Some(&v) = raw.values.iter().find(|&&v| v <= 0) {
187+
return Err(format!("expected positive values, got {v}"));
188+
}
189+
if raw.capacity < 0 {
190+
return Err(format!(
191+
"expected nonnegative capacity, got {}",
192+
raw.capacity
193+
));
194+
}
195+
Ok(IntegerKnapsack {
196+
sizes: raw.sizes,
197+
values: raw.values,
198+
capacity: raw.capacity,
199+
})
200+
}
201+
}
202+
203+
impl<'de> Deserialize<'de> for IntegerKnapsack {
204+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
205+
where
206+
D: serde::Deserializer<'de>,
207+
{
208+
let raw = RawIntegerKnapsack::deserialize(deserializer)?;
209+
IntegerKnapsack::try_from(raw).map_err(serde::de::Error::custom)
210+
}
211+
}
212+
213+
#[cfg(feature = "example-db")]
214+
pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
215+
// 5 items: sizes [3,4,5,2,7], values [4,5,7,3,9], capacity 15
216+
// Optimal: c=(0,0,1,5,0) → total_size=5+10=15, total_value=7+15=22
217+
vec![crate::example_db::specs::ModelExampleSpec {
218+
id: "integer-knapsack",
219+
instance: Box::new(IntegerKnapsack::new(
220+
vec![3, 4, 5, 2, 7],
221+
vec![4, 5, 7, 3, 9],
222+
15,
223+
)),
224+
optimal_config: vec![0, 0, 1, 5, 0],
225+
optimal_value: serde_json::json!(22),
226+
}]
227+
}
228+
229+
#[cfg(test)]
230+
#[path = "../../unit_tests/models/set/integer_knapsack.rs"]
231+
mod tests;

src/models/set/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! - [`ConsecutiveSets`]: Consecutive arrangement of subset elements in a string
55
//! - [`ExactCoverBy3Sets`]: Exact cover by 3-element subsets (X3C)
66
//! - [`ComparativeContainment`]: Compare containment-weight sums for two set families
7+
//! - [`IntegerKnapsack`]: Maximize value with integer multiplicities subject to capacity
78
//! - [`MaximumSetPacking`]: Maximum weight set packing
89
//! - [`MinimumHittingSet`]: Minimum-size universe subset hitting every set
910
//! - [`MinimumSetCovering`]: Minimum weight set cover
@@ -13,6 +14,7 @@
1314
pub(crate) mod comparative_containment;
1415
pub(crate) mod consecutive_sets;
1516
pub(crate) mod exact_cover_by_3_sets;
17+
pub(crate) mod integer_knapsack;
1618
pub(crate) mod maximum_set_packing;
1719
pub(crate) mod minimum_cardinality_key;
1820
pub(crate) mod minimum_hitting_set;
@@ -25,6 +27,7 @@ pub(crate) mod two_dimensional_consecutive_sets;
2527
pub use comparative_containment::ComparativeContainment;
2628
pub use consecutive_sets::ConsecutiveSets;
2729
pub use exact_cover_by_3_sets::ExactCoverBy3Sets;
30+
pub use integer_knapsack::IntegerKnapsack;
2831
pub use maximum_set_packing::MaximumSetPacking;
2932
pub use minimum_cardinality_key::MinimumCardinalityKey;
3033
pub use minimum_hitting_set::MinimumHittingSet;
@@ -40,6 +43,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
4043
specs.extend(comparative_containment::canonical_model_example_specs());
4144
specs.extend(consecutive_sets::canonical_model_example_specs());
4245
specs.extend(exact_cover_by_3_sets::canonical_model_example_specs());
46+
specs.extend(integer_knapsack::canonical_model_example_specs());
4347
specs.extend(maximum_set_packing::canonical_model_example_specs());
4448
specs.extend(minimum_cardinality_key::canonical_model_example_specs());
4549
specs.extend(minimum_hitting_set::canonical_model_example_specs());

0 commit comments

Comments
 (0)