Skip to content
Open
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
9 changes: 9 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"PartitionIntoTriangles": [Partition Into Triangles],
"FlowShopScheduling": [Flow Shop Scheduling],
"MinimumTardinessSequencing": [Minimum Tardiness Sequencing],
"SumOfSquaresPartition": [Sum of Squares Partition],
)

// Definition label: "def:<ProblemName>" — each definition block must have a matching label
Expand Down Expand Up @@ -1743,6 +1744,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("ShortestCommonSupersequence")
let alpha-size = x.instance.alphabet_size
Expand Down
21 changes: 21 additions & 0 deletions docs/src/reductions/problem_schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,27 @@
}
]
},
{
"name": "SumOfSquaresPartition",
"description": "Partition positive integers into K groups minimizing sum of squared group sums, subject to bound J",
"fields": [
{
"name": "sizes",
"type_name": "Vec<i64>",
"description": "Positive integer size s(a) for each element a in A"
},
{
"name": "num_groups",
"type_name": "usize",
"description": "Number of groups K in the partition"
},
{
"name": "bound",
"type_name": "i64",
"description": "Upper bound J on the sum of squared group sums"
}
]
},
{
"name": "TravelingSalesman",
"description": "Find minimum weight Hamiltonian cycle in a graph (Traveling Salesman Problem)",
Expand Down
11 changes: 9 additions & 2 deletions docs/src/reductions/reduction_graph.json
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,13 @@
"doc_path": "models/misc/struct.SubsetSum.html",
"complexity": "2^(num_elements / 2)"
},
{
"name": "SumOfSquaresPartition",
"variant": {},
"category": "misc",
"doc_path": "models/misc/struct.SumOfSquaresPartition.html",
"complexity": "num_groups^num_elements"
},
{
"name": "TravelingSalesman",
"variant": {
Expand Down Expand Up @@ -1368,7 +1375,7 @@
"doc_path": "rules/spinglass_casts/index.html"
},
{
"source": 60,
"source": 61,
"target": 12,
"overhead": [
{
Expand All @@ -1383,7 +1390,7 @@
"doc_path": "rules/travelingsalesman_ilp/index.html"
},
{
"source": 60,
"source": 61,
"target": 49,
"overhead": [
{
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 @@ -227,6 +227,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 @@ -409,6 +410,9 @@ pub struct CreateArgs {
/// Alphabet size for SCS (optional; inferred from max symbol + 1 if omitted)
#[arg(long)]
pub alphabet_size: Option<usize>,
/// Number of groups for SumOfSquaresPartition
#[arg(long)]
pub num_groups: Option<usize>,
}

#[derive(clap::Args)]
Expand Down
32 changes: 31 additions & 1 deletion problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use problemreductions::models::algebraic::{ClosestVectorProblem, BMF};
use problemreductions::models::graph::{GraphPartitioning, HamiltonianPath};
use problemreductions::models::misc::{
BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing,
PaintShop, ShortestCommonSupersequence, SubsetSum,
PaintShop, ShortestCommonSupersequence, SubsetSum, SumOfSquaresPartition,
};
use problemreductions::prelude::*;
use problemreductions::registry::collect_schemas;
Expand Down Expand Up @@ -64,6 +64,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.deadline.is_none()
&& args.num_processors.is_none()
&& args.alphabet_size.is_none()
&& args.num_groups.is_none()
}

fn emit_problem_output(output: &ProblemJsonOutput, out: &OutputConfig) -> Result<()> {
Expand Down Expand Up @@ -263,6 +264,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",
"SetBasis" => "--universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3",
"ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\" --bound 4",
_ => "",
Expand Down Expand Up @@ -650,6 +652,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
13 changes: 11 additions & 2 deletions problemreductions-cli/src/commands/solve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ fn parse_input(path: &Path) -> Result<SolveInput> {
}
}

fn with_bruteforce_hint(err: anyhow::Error) -> anyhow::Error {
let message = err.to_string();
if message.starts_with("No reduction path from ") && message.ends_with(" to ILP") {
anyhow::anyhow!("{message}\n\nTry `--solver brute-force`.")
} else {
err
}
}

pub fn solve(input: &Path, solver_name: &str, timeout: u64, out: &OutputConfig) -> Result<()> {
if solver_name != "brute-force" && solver_name != "ilp" {
anyhow::bail!(
Expand Down Expand Up @@ -97,7 +106,7 @@ fn solve_problem(
result
}
"ilp" => {
let result = problem.solve_with_ilp()?;
let result = problem.solve_with_ilp().map_err(with_bruteforce_hint)?;
let solver_desc = if name == "ILP" {
"ilp".to_string()
} else {
Expand Down Expand Up @@ -139,7 +148,7 @@ fn solve_bundle(bundle: ReductionBundle, solver_name: &str, out: &OutputConfig)
// 2. Solve the target problem
let target_result = match solver_name {
"brute-force" => target.solve_brute_force()?,
"ilp" => target.solve_with_ilp()?,
"ilp" => target.solve_with_ilp().map_err(with_bruteforce_hint)?,
_ => unreachable!(),
};

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 @@ -688,6 +688,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_then_evaluate() {
// Create a problem
Expand Down Expand Up @@ -2446,6 +2469,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_pipe_to_evaluate() {
// pred create MIS --graph 0-1,1-2 | pred evaluate - --config 1,0,1
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 @@ -32,6 +32,7 @@
{"problem":"ShortestCommonSupersequence","variant":{},"instance":{"alphabet_size":3,"bound":4,"strings":[[0,1,2],[1,0,2]]},"samples":[{"config":[1,0,1,2],"metric":true}],"optimal":[{"config":[0,1,0,2],"metric":true},{"config":[1,0,1,2],"metric":true}]},
{"problem":"SpinGlass","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"couplings":[1,1,1,1,1,1,1],"fields":[0,0,0,0,0],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,2,null],[3,4,null],[0,3,null],[1,3,null],[1,4,null],[2,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0],"metric":{"Valid":-3}}],"optimal":[{"config":[0,0,1,1,0],"metric":{"Valid":-3}},{"config":[0,1,0,0,1],"metric":{"Valid":-3}},{"config":[0,1,0,1,0],"metric":{"Valid":-3}},{"config":[0,1,1,1,0],"metric":{"Valid":-3}},{"config":[1,0,0,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,1,0],"metric":{"Valid":-3}},{"config":[1,1,0,0,1],"metric":{"Valid":-3}}]},
{"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":"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}}]}
],
"rules": [
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ pub mod prelude {
pub use crate::models::misc::{
BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence,
MinimumTardinessSequencing, PaintShop, ShortestCommonSupersequence, SubsetSum,
SumOfSquaresPartition,
};
pub use crate::models::set::{
ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis,
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 @@ -10,6 +10,7 @@
//! - [`PaintShop`]: Minimize color switches in paint shop scheduling
//! - [`ShortestCommonSupersequence`]: Find a common supersequence of bounded length
//! - [`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 @@ -20,6 +21,7 @@ mod minimum_tardiness_sequencing;
pub(crate) mod paintshop;
pub(crate) mod shortest_common_supersequence;
mod subset_sum;
pub(crate) mod sum_of_squares_partition;

pub use bin_packing::BinPacking;
pub use factoring::Factoring;
Expand All @@ -30,6 +32,7 @@ pub use minimum_tardiness_sequencing::MinimumTardinessSequencing;
pub use paintshop::PaintShop;
pub use shortest_common_supersequence::ShortestCommonSupersequence;
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 @@ -38,5 +41,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
specs.extend(paintshop::canonical_model_example_specs());
specs.extend(shortest_common_supersequence::canonical_model_example_specs());
specs.extend(minimum_tardiness_sequencing::canonical_model_example_specs());
specs.extend(sum_of_squares_partition::canonical_model_example_specs());
specs
}
Loading