Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
"SpinGlass": [Spin Glass],
"QUBO": [QUBO],
"ILP": [Integer Linear Programming],
"IntegerKnapsack": [Integer Knapsack],
"Knapsack": [Knapsack],
"PartiallyOrderedKnapsack": [Partially Ordered Knapsack],
"Satisfiability": [SAT],
Expand Down Expand Up @@ -4023,6 +4024,33 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
]
}

#{
let x = load-model-example("IntegerKnapsack")
let sizes = x.instance.sizes
let values = x.instance.values
let B = x.instance.capacity
let n = sizes.len()
let config = x.optimal_config
let opt-val = metric-value(x.optimal_value)
let total-s = range(n).map(i => config.at(i) * sizes.at(i)).sum()
let total-v = range(n).map(i => config.at(i) * values.at(i)).sum()
[
#problem-def("IntegerKnapsack")[
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$.
][
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.]

*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.

#pred-commands(
"pred create --example IntegerKnapsack -o ik.json",
"pred solve ik.json",
"pred evaluate ik.json --config " + x.optimal_config.map(str).join(","),
)
]
]
}

#problem-def("PartiallyOrderedKnapsack")[
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$.
][
Expand Down
1 change: 1 addition & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ Flags by problem type:
SteinerTreeInGraphs --graph, --edge-weights, --terminals
PartitionIntoPathsOfLength2 --graph
ResourceConstrainedScheduling --num-processors, --resource-bounds, --resource-requirements, --deadline
IntegerKnapsack --sizes, --values, --capacity
PartiallyOrderedKnapsack --sizes, --values, --capacity, --precedences
QAP --matrix (cost), --distance-matrix
StrongConnectivityAugmentation --arcs, --candidate-arcs, --bound [--num-vertices]
Expand Down
36 changes: 36 additions & 0 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"SequencingToMinimizeWeightedTardiness" => {
"--sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13"
}
"IntegerKnapsack" => "--sizes 3,4,5,2,7 --values 4,5,7,3,9 --capacity 15",
"SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11",
"ThreePartition" => "--sizes 4,5,6,4,6,5 --bound 15",
"BoyceCoddNormalFormViolation" => {
Expand Down Expand Up @@ -2333,6 +2334,41 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// IntegerKnapsack
"IntegerKnapsack" => {
let sizes_str = args.sizes.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"IntegerKnapsack requires --sizes, --values, and --capacity\n\n\
Usage: pred create IntegerKnapsack --sizes 3,4,5,2,7 --values 4,5,7,3,9 --capacity 15"
)
})?;
let values_str = args.values.as_deref().ok_or_else(|| {
anyhow::anyhow!("IntegerKnapsack requires --values (e.g., 4,5,7,3,9)")
})?;
let cap_str = args
.capacity
.as_deref()
.ok_or_else(|| anyhow::anyhow!("IntegerKnapsack requires --capacity (e.g., 15)"))?;
let sizes: Vec<i64> = util::parse_comma_list(sizes_str)?;
let values: Vec<i64> = util::parse_comma_list(values_str)?;
let capacity: i64 = cap_str.parse()?;
anyhow::ensure!(
sizes.len() == values.len(),
"sizes and values must have the same length, got {} and {}",
sizes.len(),
values.len()
);
anyhow::ensure!(sizes.iter().all(|&s| s > 0), "all sizes must be positive");
anyhow::ensure!(values.iter().all(|&v| v > 0), "all values must be positive");
anyhow::ensure!(capacity >= 0, "capacity must be nonnegative");
(
ser(problemreductions::models::set::IntegerKnapsack::new(
sizes, values, capacity,
))?,
resolved_variant.clone(),
)
}

// SubsetSum
"SubsetSum" => {
let sizes_str = args.sizes.as_deref().ok_or_else(|| {
Expand Down
6 changes: 3 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ pub mod prelude {
TimetableDesign,
};
pub use crate::models::set::{
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking,
MinimumCardinalityKey, MinimumHittingSet, MinimumSetCovering, PrimeAttributeName,
RootedTreeStorageAssignment, SetBasis,
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, IntegerKnapsack,
MaximumSetPacking, MinimumCardinalityKey, MinimumHittingSet, MinimumSetCovering,
PrimeAttributeName, RootedTreeStorageAssignment, SetBasis,
};

// Core traits
Expand Down
2 changes: 1 addition & 1 deletion src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub use misc::{
Term, ThreePartition, TimetableDesign,
};
pub use set::{
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking,
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, IntegerKnapsack, MaximumSetPacking,
MinimumCardinalityKey, MinimumHittingSet, MinimumSetCovering, PrimeAttributeName,
RootedTreeStorageAssignment, SetBasis, TwoDimensionalConsecutiveSets,
};
231 changes: 231 additions & 0 deletions src/models/set/integer_knapsack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
//! Integer Knapsack problem implementation.
//!
//! The Integer Knapsack problem generalizes the 0-1 Knapsack by allowing
//! each item to be selected with a non-negative integer multiplicity.

use crate::registry::{FieldInfo, ProblemSchemaEntry};
use crate::traits::Problem;
use crate::types::Max;
use serde::{Deserialize, Serialize};

inventory::submit! {
ProblemSchemaEntry {
name: "IntegerKnapsack",
display_name: "Integer Knapsack",
aliases: &[],
dimensions: &[],
module_path: module_path!(),
description: "Select items with integer multiplicities to maximize total value subject to capacity constraint",
fields: &[
FieldInfo { name: "sizes", type_name: "Vec<i64>", description: "Positive item sizes s(u)" },
FieldInfo { name: "values", type_name: "Vec<i64>", description: "Positive item values v(u)" },
FieldInfo { name: "capacity", type_name: "i64", description: "Nonnegative knapsack capacity B" },
],
}
}

/// The Integer Knapsack problem.
///
/// Given `n` items, each with positive size `s_i` and positive value `v_i`,
/// and a nonnegative capacity `B`,
/// find non-negative integer multiplicities `c_0, ..., c_{n-1}` such that
/// `sum c_i * s_i <= B`, maximizing `sum c_i * v_i`.
///
/// # Representation
///
/// Variable `i` has domain `{0, ..., floor(B / s_i)}` representing the
/// multiplicity of item `i`.
///
/// # Example
///
/// ```
/// use problemreductions::models::set::IntegerKnapsack;
/// use problemreductions::{Problem, Solver, BruteForce};
///
/// let problem = IntegerKnapsack::new(vec![3, 4, 5, 2, 7], vec![4, 5, 7, 3, 9], 15);
/// let solver = BruteForce::new();
/// let solution = solver.find_witness(&problem);
/// assert!(solution.is_some());
/// ```
#[derive(Debug, Clone, Serialize)]
#[serde(into = "RawIntegerKnapsack")]
pub struct IntegerKnapsack {
sizes: Vec<i64>,
values: Vec<i64>,
capacity: i64,
}

impl IntegerKnapsack {
/// Create a new IntegerKnapsack instance.
///
/// # Panics
/// Panics if `sizes` and `values` have different lengths, or if any
/// size or value is not positive, or if capacity is negative.
pub fn new(sizes: Vec<i64>, values: Vec<i64>, capacity: i64) -> Self {
assert_eq!(
sizes.len(),
values.len(),
"sizes and values must have the same length"
);
assert!(
sizes.iter().all(|&s| s > 0),
"IntegerKnapsack sizes must be positive"
);
assert!(
values.iter().all(|&v| v > 0),
"IntegerKnapsack values must be positive"
);
assert!(
capacity >= 0,
"IntegerKnapsack capacity must be nonnegative"
);
Self {
sizes,
values,
capacity,
}
}

/// Returns the item sizes.
pub fn sizes(&self) -> &[i64] {
&self.sizes
}

/// Returns the item values.
pub fn values(&self) -> &[i64] {
&self.values
}

/// Returns the knapsack capacity.
pub fn capacity(&self) -> i64 {
self.capacity
}

/// Returns the number of items.
pub fn num_items(&self) -> usize {
self.sizes.len()
}
}

impl Problem for IntegerKnapsack {
const NAME: &'static str = "IntegerKnapsack";
type Value = Max<i64>;

fn variant() -> Vec<(&'static str, &'static str)> {
crate::variant_params![]
}

fn dims(&self) -> Vec<usize> {
self.sizes
.iter()
.map(|&s| (self.capacity / s + 1) as usize)
.collect()
}

fn evaluate(&self, config: &[usize]) -> Max<i64> {
if config.len() != self.num_items() {
return Max(None);
}
let dims = self.dims();
if config.iter().zip(dims.iter()).any(|(&c, &d)| c >= d) {
return Max(None);
}
let total_size: i64 = config
.iter()
.enumerate()
.map(|(i, &c)| c as i64 * self.sizes[i])
.sum();
if total_size > self.capacity {
return Max(None);
}
let total_value: i64 = config
.iter()
.enumerate()
.map(|(i, &c)| c as i64 * self.values[i])
.sum();
Max(Some(total_value))
}
}

crate::declare_variants! {
default IntegerKnapsack => "(capacity + 1)^num_items",
}

/// Raw representation for serde deserialization with full validation.
#[derive(Deserialize, Serialize)]
struct RawIntegerKnapsack {
sizes: Vec<i64>,
values: Vec<i64>,
capacity: i64,
}

impl From<IntegerKnapsack> for RawIntegerKnapsack {
fn from(ik: IntegerKnapsack) -> Self {
RawIntegerKnapsack {
sizes: ik.sizes,
values: ik.values,
capacity: ik.capacity,
}
}
}

impl TryFrom<RawIntegerKnapsack> for IntegerKnapsack {
type Error = String;

fn try_from(raw: RawIntegerKnapsack) -> Result<Self, String> {
if raw.sizes.len() != raw.values.len() {
return Err(format!(
"sizes and values must have the same length, got {} and {}",
raw.sizes.len(),
raw.values.len()
));
}
if let Some(&s) = raw.sizes.iter().find(|&&s| s <= 0) {
return Err(format!("expected positive sizes, got {s}"));
}
if let Some(&v) = raw.values.iter().find(|&&v| v <= 0) {
return Err(format!("expected positive values, got {v}"));
}
if raw.capacity < 0 {
return Err(format!(
"expected nonnegative capacity, got {}",
raw.capacity
));
}
Ok(IntegerKnapsack {
sizes: raw.sizes,
values: raw.values,
capacity: raw.capacity,
})
}
}

impl<'de> Deserialize<'de> for IntegerKnapsack {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = RawIntegerKnapsack::deserialize(deserializer)?;
IntegerKnapsack::try_from(raw).map_err(serde::de::Error::custom)
}
}

#[cfg(feature = "example-db")]
pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
// 5 items: sizes [3,4,5,2,7], values [4,5,7,3,9], capacity 15
// Optimal: c=(0,0,1,5,0) → total_size=5+10=15, total_value=7+15=22
vec![crate::example_db::specs::ModelExampleSpec {
id: "integer-knapsack",
instance: Box::new(IntegerKnapsack::new(
vec![3, 4, 5, 2, 7],
vec![4, 5, 7, 3, 9],
15,
)),
optimal_config: vec![0, 0, 1, 5, 0],
optimal_value: serde_json::json!(22),
}]
}

#[cfg(test)]
#[path = "../../unit_tests/models/set/integer_knapsack.rs"]
mod tests;
4 changes: 4 additions & 0 deletions src/models/set/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! - [`ConsecutiveSets`]: Consecutive arrangement of subset elements in a string
//! - [`ExactCoverBy3Sets`]: Exact cover by 3-element subsets (X3C)
//! - [`ComparativeContainment`]: Compare containment-weight sums for two set families
//! - [`IntegerKnapsack`]: Maximize value with integer multiplicities subject to capacity
//! - [`MaximumSetPacking`]: Maximum weight set packing
//! - [`MinimumHittingSet`]: Minimum-size universe subset hitting every set
//! - [`MinimumSetCovering`]: Minimum weight set cover
Expand All @@ -13,6 +14,7 @@
pub(crate) mod comparative_containment;
pub(crate) mod consecutive_sets;
pub(crate) mod exact_cover_by_3_sets;
pub(crate) mod integer_knapsack;
pub(crate) mod maximum_set_packing;
pub(crate) mod minimum_cardinality_key;
pub(crate) mod minimum_hitting_set;
Expand All @@ -25,6 +27,7 @@ pub(crate) mod two_dimensional_consecutive_sets;
pub use comparative_containment::ComparativeContainment;
pub use consecutive_sets::ConsecutiveSets;
pub use exact_cover_by_3_sets::ExactCoverBy3Sets;
pub use integer_knapsack::IntegerKnapsack;
pub use maximum_set_packing::MaximumSetPacking;
pub use minimum_cardinality_key::MinimumCardinalityKey;
pub use minimum_hitting_set::MinimumHittingSet;
Expand All @@ -40,6 +43,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
specs.extend(comparative_containment::canonical_model_example_specs());
specs.extend(consecutive_sets::canonical_model_example_specs());
specs.extend(exact_cover_by_3_sets::canonical_model_example_specs());
specs.extend(integer_knapsack::canonical_model_example_specs());
specs.extend(maximum_set_packing::canonical_model_example_specs());
specs.extend(minimum_cardinality_key::canonical_model_example_specs());
specs.extend(minimum_hitting_set::canonical_model_example_specs());
Expand Down
Loading
Loading