Skip to content
9 changes: 9 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
"StaffScheduling": [Staff Scheduling],
"MultiprocessorScheduling": [Multiprocessor Scheduling],
"MinimumTardinessSequencing": [Minimum Tardiness Sequencing],
"SumOfSquaresPartition": [Sum of Squares Partition],
"SequencingWithinIntervals": [Sequencing Within Intervals],
"DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow],
"StringToStringCorrection": [String-to-String Correction],
Expand Down Expand Up @@ -2301,6 +2302,14 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS
*Example.* Let $A = {3, 7, 1, 8, 2, 4}$ ($n = 6$) and target $B = 11$. Selecting $A' = {3, 8}$ gives sum $3 + 8 = 11 = B$. Another solution: $A' = {7, 4}$ with sum $7 + 4 = 11 = B$.
]

#problem-def("SumOfSquaresPartition")[
Given a finite set $A = {a_0, dots, a_(n-1)}$ with sizes $s(a_i) in ZZ^+$, a positive integer $K lt.eq |A|$ (number of groups), and a positive integer $J$ (bound), determine whether $A$ can be partitioned into $K$ disjoint sets $A_1, dots, A_K$ such that $sum_(i=1)^K (sum_(a in A_i) s(a))^2 lt.eq J$.
][
Problem SP19 in Garey and Johnson @garey1979. NP-complete in the strong sense, so no pseudo-polynomial time algorithm exists unless $P = NP$. For fixed $K$, a dynamic-programming algorithm runs in $O(n S^(K-1))$ pseudo-polynomial time, where $S = sum s(a)$. The problem remains NP-complete when the exponent 2 is replaced by any fixed rational $alpha > 1$. #footnote[No algorithm improving on brute-force $O(K^n)$ enumeration is known for the general case.] The squared objective penalizes imbalanced partitions, connecting it to variance minimization, load balancing, and $k$-means clustering. Sum of Squares Partition generalizes Partition ($K = 2$, $J = S^2 slash 2$).

*Example.* Let $A = {5, 3, 8, 2, 7, 1}$ ($n = 6$), $K = 3$ groups, and bound $J = 240$. The partition $A_1 = {8, 1}$, $A_2 = {5, 2}$, $A_3 = {3, 7}$ gives group sums $9, 7, 10$ and sum of squares $81 + 49 + 100 = 230 lt.eq 240 = J$. With a tighter bound $J = 225$, the best achievable partition has group sums ${9, 9, 8}$ yielding $81 + 81 + 64 = 226 > 225$, so the answer is NO.
]

#{
let x = load-model-example("SequencingWithReleaseTimesAndDeadlines")
let n = x.instance.lengths.len()
Expand Down
4 changes: 4 additions & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ Flags by problem type:
Factoring --target, --m, --n
BinPacking --sizes, --capacity
SubsetSum --sizes, --target
SumOfSquaresPartition --sizes, --num-groups, --bound
PaintShop --sequence
MaximumSetPacking --sets [--weights]
MinimumSetCovering --universe, --sets [--weights]
Expand Down Expand Up @@ -500,6 +501,9 @@ pub struct CreateArgs {
/// Alphabet size for LCS, SCS, or StringToStringCorrection (optional; inferred from the input strings if omitted)
#[arg(long)]
pub alphabet_size: Option<usize>,
/// Number of groups for SumOfSquaresPartition
#[arg(long)]
pub num_groups: Option<usize>,
/// Functional dependencies for MinimumCardinalityKey (semicolon-separated "lhs>rhs" pairs, e.g., "0,1>2;0,2>3")
#[arg(long)]
pub dependencies: Option<String>,
Expand Down
32 changes: 32 additions & 0 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use problemreductions::models::misc::{
BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing,
MultiprocessorScheduling, PaintShop, SequencingWithReleaseTimesAndDeadlines,
SequencingWithinIntervals, ShortestCommonSupersequence, StringToStringCorrection, SubsetSum,
SumOfSquaresPartition,
};
use problemreductions::models::BiconnectivityAugmentation;
use problemreductions::prelude::*;
Expand Down Expand Up @@ -94,6 +95,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.requirements.is_none()
&& args.num_workers.is_none()
&& args.alphabet_size.is_none()
&& args.num_groups.is_none()
&& args.dependencies.is_none()
&& args.num_attributes.is_none()
&& args.source_string.is_none()
Expand Down Expand Up @@ -328,6 +330,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
}
"SubgraphIsomorphism" => "--graph 0-1,1-2,2-0 --pattern 0-1",
"SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11",
"SumOfSquaresPartition" => "--sizes 5,3,8,2,7,1 --num-groups 3 --bound 240",
"ComparativeContainment" => {
"--universe 4 --r-sets \"0,1,2,3;0,1\" --s-sets \"0,1,2,3;2,3\" --r-weights 2,5 --s-weights 3,6"
}
Expand Down Expand Up @@ -1044,6 +1047,34 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// SumOfSquaresPartition
"SumOfSquaresPartition" => {
let sizes_str = args.sizes.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"SumOfSquaresPartition requires --sizes, --num-groups, and --bound\n\n\
Usage: pred create SumOfSquaresPartition --sizes 5,3,8,2,7,1 --num-groups 3 --bound 240"
)
})?;
let num_groups = args.num_groups.ok_or_else(|| {
anyhow::anyhow!(
"SumOfSquaresPartition requires --num-groups\n\n\
Usage: pred create SumOfSquaresPartition --sizes 5,3,8,2,7,1 --num-groups 3 --bound 240"
)
})?;
let bound = args.bound.ok_or_else(|| {
anyhow::anyhow!(
"SumOfSquaresPartition requires --bound\n\n\
Usage: pred create SumOfSquaresPartition --sizes 5,3,8,2,7,1 --num-groups 3 --bound 240"
)
})?;
let sizes: Vec<i64> = util::parse_comma_list(sizes_str)?;
(
ser(SumOfSquaresPartition::try_new(sizes, num_groups, bound)
.map_err(anyhow::Error::msg)?)?,
resolved_variant.clone(),
)
}

// PaintShop
"PaintShop" => {
let seq_str = args.sequence.as_deref().ok_or_else(|| {
Expand Down Expand Up @@ -3451,6 +3482,7 @@ mod tests {
schedules: None,
requirements: None,
num_workers: None,
num_groups: None,
}
}

Expand Down
51 changes: 51 additions & 0 deletions problemreductions-cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1361,6 +1361,29 @@ fn test_create_set_basis_rejects_out_of_range_elements() {
assert!(!stderr.contains("panicked at"), "stderr: {stderr}");
}

#[test]
fn test_create_sum_of_squares_partition_rejects_negative_bound_without_panicking() {
let output = pred()
.args([
"create",
"SumOfSquaresPartition",
"--sizes",
"1,2,3",
"--num-groups",
"2",
"--bound=-1",
])
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Bound must be nonnegative"),
"stderr: {stderr}"
);
assert!(!stderr.contains("panicked at"), "stderr: {stderr}");
}

#[test]
fn test_create_minimum_cardinality_key_problem_help_uses_supported_flags() {
let output = pred()
Expand Down Expand Up @@ -3851,6 +3874,34 @@ fn test_create_pipe_to_solve() {
);
}

#[test]
fn test_solve_ilp_error_suggests_brute_force_fallback() {
let problem_json = r#"{
"type": "SumOfSquaresPartition",
"data": {
"sizes": [5, 3, 8, 2, 7, 1],
"num_groups": 3,
"bound": 240
}
}"#;
let tmp = std::env::temp_dir().join("pred_test_sum_of_squares_partition.json");
std::fs::write(&tmp, problem_json).unwrap();

let output = pred()
.args(["solve", tmp.to_str().unwrap()])
.output()
.unwrap();
assert!(!output.status.success());

let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("--solver brute-force"),
"stderr should suggest the brute-force fallback, got: {stderr}"
);

std::fs::remove_file(&tmp).ok();
}

#[test]
fn test_create_multiple_choice_branching_pipe_to_solve() {
let create_out = pred()
Expand Down
1 change: 1 addition & 0 deletions src/example_db/fixtures/examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
{"problem":"SteinerTree","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[2,5,2,1,5,6,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"terminals":[0,2,4]},"samples":[{"config":[1,0,1,1,0,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[1,0,1,1,0,0,1],"metric":{"Valid":6}}]},
{"problem":"StringToStringCorrection","variant":{},"instance":{"alphabet_size":4,"bound":2,"source":[0,1,2,3,1,0],"target":[0,1,3,2,1]},"samples":[{"config":[8,5],"metric":true}],"optimal":[{"config":[5,7],"metric":true},{"config":[8,5],"metric":true}]},
{"problem":"StrongConnectivityAugmentation","variant":{"weight":"i32"},"instance":{"bound":8,"candidate_arcs":[[4,0,10],[4,3,3],[4,2,3],[4,1,3],[3,0,7],[3,1,3],[2,0,7],[2,1,3],[1,0,5]],"graph":{"inner":{"edge_property":"directed","edges":[[0,1,null],[1,2,null],[2,3,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}}},"samples":[{"config":[0,0,0,1,0,0,0,0,1],"metric":true},{"config":[0,0,0,0,0,0,0,0,0],"metric":false}],"optimal":[{"config":[0,0,0,1,0,0,0,0,1],"metric":true}]},
{"problem":"SumOfSquaresPartition","variant":{},"instance":{"bound":240,"num_groups":3,"sizes":[5,3,8,2,7,1]},"samples":[{"config":[1,2,0,1,2,0],"metric":true}],"optimal":[{"config":[0,0,1,0,2,0],"metric":true},{"config":[0,0,1,0,2,1],"metric":true},{"config":[0,0,1,0,2,2],"metric":true},{"config":[0,0,1,1,2,0],"metric":true},{"config":[0,0,1,1,2,1],"metric":true},{"config":[0,0,1,1,2,2],"metric":true},{"config":[0,0,1,2,2,0],"metric":true},{"config":[0,0,1,2,2,1],"metric":true},{"config":[0,0,1,2,2,2],"metric":true},{"config":[0,0,2,0,1,0],"metric":true},{"config":[0,0,2,0,1,1],"metric":true},{"config":[0,0,2,0,1,2],"metric":true},{"config":[0,0,2,1,1,0],"metric":true},{"config":[0,0,2,1,1,1],"metric":true},{"config":[0,0,2,1,1,2],"metric":true},{"config":[0,0,2,2,1,0],"metric":true},{"config":[0,0,2,2,1,1],"metric":true},{"config":[0,0,2,2,1,2],"metric":true},{"config":[0,1,1,0,2,0],"metric":true},{"config":[0,1,1,0,2,2],"metric":true},{"config":[0,1,1,2,2,0],"metric":true},{"config":[0,1,2,0,1,0],"metric":true},{"config":[0,1,2,0,1,1],"metric":true},{"config":[0,1,2,0,1,2],"metric":true},{"config":[0,1,2,2,1,0],"metric":true},{"config":[0,2,1,0,2,0],"metric":true},{"config":[0,2,1,0,2,1],"metric":true},{"config":[0,2,1,0,2,2],"metric":true},{"config":[0,2,1,1,2,0],"metric":true},{"config":[0,2,2,0,1,0],"metric":true},{"config":[0,2,2,0,1,1],"metric":true},{"config":[0,2,2,1,1,0],"metric":true},{"config":[1,0,0,1,2,1],"metric":true},{"config":[1,0,0,1,2,2],"metric":true},{"config":[1,0,0,2,2,1],"metric":true},{"config":[1,0,2,1,0,0],"metric":true},{"config":[1,0,2,1,0,1],"metric":true},{"config":[1,0,2,1,0,2],"metric":true},{"config":[1,0,2,2,0,1],"metric":true},{"config":[1,1,0,0,2,0],"metric":true},{"config":[1,1,0,0,2,1],"metric":true},{"config":[1,1,0,0,2,2],"metric":true},{"config":[1,1,0,1,2,0],"metric":true},{"config":[1,1,0,1,2,1],"metric":true},{"config":[1,1,0,1,2,2],"metric":true},{"config":[1,1,0,2,2,0],"metric":true},{"config":[1,1,0,2,2,1],"metric":true},{"config":[1,1,0,2,2,2],"metric":true},{"config":[1,1,2,0,0,0],"metric":true},{"config":[1,1,2,0,0,1],"metric":true},{"config":[1,1,2,0,0,2],"metric":true},{"config":[1,1,2,1,0,0],"metric":true},{"config":[1,1,2,1,0,1],"metric":true},{"config":[1,1,2,1,0,2],"metric":true},{"config":[1,1,2,2,0,0],"metric":true},{"config":[1,1,2,2,0,1],"metric":true},{"config":[1,1,2,2,0,2],"metric":true},{"config":[1,2,0,0,2,1],"metric":true},{"config":[1,2,0,1,2,0],"metric":true},{"config":[1,2,0,1,2,1],"metric":true},{"config":[1,2,0,1,2,2],"metric":true},{"config":[1,2,2,0,0,1],"metric":true},{"config":[1,2,2,1,0,0],"metric":true},{"config":[1,2,2,1,0,1],"metric":true},{"config":[2,0,0,1,1,2],"metric":true},{"config":[2,0,0,2,1,1],"metric":true},{"config":[2,0,0,2,1,2],"metric":true},{"config":[2,0,1,1,0,2],"metric":true},{"config":[2,0,1,2,0,0],"metric":true},{"config":[2,0,1,2,0,1],"metric":true},{"config":[2,0,1,2,0,2],"metric":true},{"config":[2,1,0,0,1,2],"metric":true},{"config":[2,1,0,2,1,0],"metric":true},{"config":[2,1,0,2,1,1],"metric":true},{"config":[2,1,0,2,1,2],"metric":true},{"config":[2,1,1,0,0,2],"metric":true},{"config":[2,1,1,2,0,0],"metric":true},{"config":[2,1,1,2,0,2],"metric":true},{"config":[2,2,0,0,1,0],"metric":true},{"config":[2,2,0,0,1,1],"metric":true},{"config":[2,2,0,0,1,2],"metric":true},{"config":[2,2,0,1,1,0],"metric":true},{"config":[2,2,0,1,1,1],"metric":true},{"config":[2,2,0,1,1,2],"metric":true},{"config":[2,2,0,2,1,0],"metric":true},{"config":[2,2,0,2,1,1],"metric":true},{"config":[2,2,0,2,1,2],"metric":true},{"config":[2,2,1,0,0,0],"metric":true},{"config":[2,2,1,0,0,1],"metric":true},{"config":[2,2,1,0,0,2],"metric":true},{"config":[2,2,1,1,0,0],"metric":true},{"config":[2,2,1,1,0,1],"metric":true},{"config":[2,2,1,1,0,2],"metric":true},{"config":[2,2,1,2,0,0],"metric":true},{"config":[2,2,1,2,0,1],"metric":true},{"config":[2,2,1,2,0,2],"metric":true}]},
{"problem":"TravelingSalesman","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[1,3,2,2,3,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}]},
{"problem":"UndirectedTwoCommodityIntegralFlow","variant":{},"instance":{"capacities":[1,1,2],"graph":{"inner":{"edge_property":"undirected","edges":[[0,2,null],[1,2,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}},"requirement_1":1,"requirement_2":1,"sink_1":3,"sink_2":3,"source_1":0,"source_2":1},"samples":[{"config":[1,0,0,0,0,0,1,0,1,0,1,0],"metric":true}],"optimal":[{"config":[0,0,1,0,1,0,0,0,1,0,1,0],"metric":true},{"config":[1,0,0,0,0,0,1,0,1,0,1,0],"metric":true}]}
],
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pub mod prelude {
MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop,
SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals,
ShortestCommonSupersequence, StaffScheduling, StringToStringCorrection, SubsetSum,
SumOfSquaresPartition,
};
pub use crate::models::set::{
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking,
Expand Down
4 changes: 4 additions & 0 deletions src/models/misc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
//! - [`ShortestCommonSupersequence`]: Find a common supersequence of bounded length
//! - [`StringToStringCorrection`]: String-to-String Correction (derive target via deletions and swaps)
//! - [`SubsetSum`]: Find a subset summing to exactly a target value
//! - [`SumOfSquaresPartition`]: Partition integers into K groups minimizing sum of squared group sums

mod bin_packing;
pub(crate) mod factoring;
Expand All @@ -29,6 +30,7 @@ pub(crate) mod shortest_common_supersequence;
mod staff_scheduling;
pub(crate) mod string_to_string_correction;
mod subset_sum;
pub(crate) mod sum_of_squares_partition;

pub use bin_packing::BinPacking;
pub use factoring::Factoring;
Expand All @@ -44,6 +46,7 @@ pub use shortest_common_supersequence::ShortestCommonSupersequence;
pub use staff_scheduling::StaffScheduling;
pub use string_to_string_correction::StringToStringCorrection;
pub use subset_sum::SubsetSum;
pub use sum_of_squares_partition::SumOfSquaresPartition;

#[cfg(feature = "example-db")]
pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
Expand All @@ -57,6 +60,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
specs.extend(shortest_common_supersequence::canonical_model_example_specs());
specs.extend(string_to_string_correction::canonical_model_example_specs());
specs.extend(minimum_tardiness_sequencing::canonical_model_example_specs());
specs.extend(sum_of_squares_partition::canonical_model_example_specs());
specs.extend(sequencing_with_release_times_and_deadlines::canonical_model_example_specs());
specs
}
Loading
Loading