From b74600de6faa61b5129f9fb21083c2fe30eacece Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Sat, 28 Mar 2026 22:42:20 +0800 Subject: [PATCH] Fix #548: Add CosineProductIntegration model and Partition reduction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add CosineProductIntegration (Garey & Johnson A7/AN14) as an independent model with a Partition → CosineProductIntegration reduction. The problem asks whether a balanced sign assignment exists for a sequence of integer frequencies, which is equivalent to Partition for positive integers but generalizes to arbitrary signed coefficients. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/reductions.typ | 27 ++++ docs/paper/references.bib | 8 + src/lib.rs | 19 +-- src/models/misc/cosine_product_integration.rs | 140 ++++++++++++++++++ src/models/misc/mod.rs | 4 + src/models/mod.rs | 10 +- src/rules/mod.rs | 2 + .../partition_cosineproductintegration.rs | 76 ++++++++++ .../models/misc/cosine_product_integration.rs | 101 +++++++++++++ .../partition_cosineproductintegration.rs | 103 +++++++++++++ 10 files changed, 476 insertions(+), 14 deletions(-) create mode 100644 src/models/misc/cosine_product_integration.rs create mode 100644 src/rules/partition_cosineproductintegration.rs create mode 100644 src/unit_tests/models/misc/cosine_product_integration.rs create mode 100644 src/unit_tests/rules/partition_cosineproductintegration.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index f5ad712f9..15c431e52 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -144,6 +144,7 @@ "LongestCommonSubsequence": [Longest Common Subsequence], "ExactCoverBy3Sets": [Exact Cover by 3-Sets], "SubsetSum": [Subset Sum], + "CosineProductIntegration": [Cosine Product Integration], "Partition": [Partition], "ThreePartition": [3-Partition], "PartialFeedbackEdgeSet": [Partial Feedback Edge Set], @@ -4683,6 +4684,14 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], *Example.* Let $A = {3, 1, 1, 2, 2, 1}$ ($n = 6$, total sum $= 10$). Setting $A' = {3, 2}$ (indices 0, 3) gives sum $3 + 2 = 5 = 10 slash 2$, and $A without A' = {1, 1, 2, 1}$ also sums to 5. Hence a balanced partition exists. ] +#problem-def("CosineProductIntegration")[ + Given a sequence of integers $(a_1, a_2, dots, a_n)$, determine whether there exists a sign assignment $epsilon in {-1, +1}^n$ such that $sum_(i=1)^n epsilon_i a_i = 0$. +][ + Garey & Johnson problem A7/AN14. The original formulation asks whether $integral_0^(2 pi) product_(i=1)^n cos(a_i theta) d theta = 0$; by expanding each cosine as $(e^(i a_i theta) + e^(-i a_i theta)) slash 2$ via Euler's formula and integrating, the integral equals $(2 pi slash 2^n)$ times the number of sign assignments $epsilon$ with $sum epsilon_i a_i = 0$. Hence the integral is nonzero if and only if a balanced sign assignment exists, making this equivalent to a generalisation of Partition to signed integers. NP-complete by reduction from Partition @plaisted1976. Solvable in pseudo-polynomial time via dynamic programming on achievable partial sums. + + *Example.* Let $(a_1, a_2, a_3) = (2, 3, 5)$. The sign assignment $(+1, +1, -1)$ gives $2 + 3 - 5 = 0$, so the integral is nonzero. +] + #{ let x = load-model-example("ShortestCommonSupersequence") let alpha-size = x.instance.alphabet_size @@ -7063,6 +7072,24 @@ where $P$ is a penalty weight large enough that any constraint violation costs m _Solution extraction._ Discard slack variables: return $bold(x)' [0..n]$. ] +#let part_cpi = load-example("Partition", "CosineProductIntegration") +#let part_cpi_sol = part_cpi.solutions.at(0) +#let part_cpi_sizes = part_cpi.source.instance.sizes +#let part_cpi_n = part_cpi_sizes.len() +#let part_cpi_coeffs = part_cpi.target.instance.coefficients +#reduction-rule("Partition", "CosineProductIntegration", + example: true, + example-caption: [#part_cpi_n elements], +)[ + This $O(n)$ identity reduction casts each positive integer size $s_i$ to the corresponding integer coefficient $a_i = s_i$. A balanced partition (two subsets of equal sum) exists if and only if a balanced sign assignment ($sum epsilon_i a_i = 0$) exists, because assigning element $i$ to subset $A'$ corresponds to $epsilon_i = -1$ and to $A without A'$ corresponds to $epsilon_i = +1$. Reference: Plaisted (1976) @plaisted1976. +][ + _Construction._ Given Partition sizes $s_0, dots, s_(n-1) in ZZ^+$, set the CosineProductIntegration coefficients to $a_i = s_i$ for each $i in {0, dots, n-1}$. + + _Correctness._ ($arrow.r.double$) If a balanced partition exists with subset $A'$ having $sum_(a in A') s(a) = S slash 2$, then the sign assignment $epsilon_i = -1$ for $i in A'$ and $epsilon_i = +1$ otherwise gives $sum epsilon_i a_i = S - 2 dot S slash 2 = 0$. ($arrow.l.double$) If a balanced sign assignment exists with $sum epsilon_i a_i = 0$, the elements with $epsilon_i = -1$ form a subset summing to $S slash 2$, which is a valid partition. + + _Solution extraction._ Return the same binary vector: $x_i = 1$ (element in second subset) corresponds to $epsilon_i = -1$ (negative sign). +] + #let part_ks = load-example("Partition", "Knapsack") #let part_ks_sol = part_ks.solutions.at(0) #let part_ks_sizes = part_ks.source.instance.sizes diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 69a3e0002..3b63d53a5 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -1446,3 +1446,11 @@ @article{edmondsjohnson1973 pages = {88--124}, year = {1973} } + +@techreport{plaisted1976, + author = {David A. Plaisted}, + title = {Some Polynomial and Integer Divisibility Problems Are {NP}-Hard}, + institution = {Stanford University, Department of Computer Science}, + number = {STAN-CS-76-583}, + year = {1976} +} diff --git a/src/lib.rs b/src/lib.rs index ab7083bcd..1d95630dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,15 +71,16 @@ pub mod prelude { pub use crate::models::misc::{ AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CapacityAssignment, CbqRelation, ConjunctiveBooleanQuery, ConjunctiveQueryFoldability, ConsistencyOfDatabaseFrequencyTables, - EnsembleComputation, ExpectedRetrievalCost, Factoring, FlowShopScheduling, - GroupingBySwapping, JobShopScheduling, Knapsack, LongestCommonSubsequence, - MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, Partition, - ProductionPlanning, QueryArg, RectilinearPictureCompression, ResourceConstrainedScheduling, - SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost, - SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness, - SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, - ShortestCommonSupersequence, StackerCrane, StaffScheduling, StringToStringCorrection, - SubsetSum, SumOfSquaresPartition, Term, ThreePartition, TimetableDesign, + CosineProductIntegration, EnsembleComputation, ExpectedRetrievalCost, Factoring, + FlowShopScheduling, GroupingBySwapping, JobShopScheduling, Knapsack, + LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, + Partition, ProductionPlanning, QueryArg, RectilinearPictureCompression, + ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines, + SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime, + SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines, + SequencingWithinIntervals, ShortestCommonSupersequence, StackerCrane, StaffScheduling, + StringToStringCorrection, SubsetSum, SumOfSquaresPartition, Term, ThreePartition, + TimetableDesign, }; pub use crate::models::set::{ ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking, diff --git a/src/models/misc/cosine_product_integration.rs b/src/models/misc/cosine_product_integration.rs new file mode 100644 index 000000000..1716cc770 --- /dev/null +++ b/src/models/misc/cosine_product_integration.rs @@ -0,0 +1,140 @@ +//! Cosine Product Integration problem implementation. +//! +//! Given integer frequencies `a_1, ..., a_n`, determine whether a sign +//! assignment `ε ∈ {-1, +1}^n` exists with `∑ εᵢ aᵢ = 0`. +//! +//! This is equivalent to asking whether +//! `∫₀²π ∏ᵢ cos(aᵢ θ) dθ ≠ 0` (Garey & Johnson A7 AN14). +//! The integral is nonzero exactly when such a balanced sign assignment +//! exists, so the G&J question "does the integral equal zero?" is the +//! complement of this satisfaction problem. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::Problem; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "CosineProductIntegration", + display_name: "Cosine Product Integration", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Decide whether a balanced sign assignment exists for a sequence of integer frequencies", + fields: &[ + FieldInfo { + name: "coefficients", + type_name: "Vec", + description: "Integer cosine frequencies", + }, + ], + } +} + +/// The Cosine Product Integration problem. +/// +/// Given integer coefficients `a_1, ..., a_n`, determine whether there +/// exists a sign assignment `ε ∈ {-1, +1}^n` with `∑ εᵢ aᵢ = 0`. +/// +/// # Representation +/// +/// Each variable chooses a sign: `0` means `+aᵢ`, `1` means `−aᵢ`. +/// A configuration is satisfying when the resulting signed sum is zero. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::CosineProductIntegration; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // coefficients [2, 3, 5]: sign assignment (+2, +3, -5) = 0 +/// let problem = CosineProductIntegration::new(vec![2, 3, 5]); +/// let solver = BruteForce::new(); +/// let solution = solver.find_witness(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CosineProductIntegration { + coefficients: Vec, +} + +impl CosineProductIntegration { + /// Create a new CosineProductIntegration instance. + /// + /// # Panics + /// + /// Panics if `coefficients` is empty. + pub fn new(coefficients: Vec) -> Self { + assert!( + !coefficients.is_empty(), + "CosineProductIntegration requires at least one coefficient" + ); + Self { coefficients } + } + + /// Returns the cosine coefficients. + pub fn coefficients(&self) -> &[i64] { + &self.coefficients + } + + /// Returns the number of coefficients. + pub fn num_coefficients(&self) -> usize { + self.coefficients.len() + } +} + +impl Problem for CosineProductIntegration { + const NAME: &'static str = "CosineProductIntegration"; + type Value = crate::types::Or; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + vec![2; self.num_coefficients()] + } + + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_coefficients() { + return crate::types::Or(false); + } + if config.iter().any(|&v| v >= 2) { + return crate::types::Or(false); + } + let signed_sum: i128 = self + .coefficients + .iter() + .zip(config.iter()) + .map(|(&a, &bit)| { + let val = a as i128; + if bit == 0 { + val + } else { + -val + } + }) + .sum(); + signed_sum == 0 + }) + } +} + +crate::declare_variants! { + default CosineProductIntegration => "2^(num_coefficients / 2)", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "cosine_product_integration", + instance: Box::new(CosineProductIntegration::new(vec![2, 3, 5])), + optimal_config: vec![0, 0, 1], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/cosine_product_integration.rs"] +mod tests; diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index b64413ce9..0e5572ca4 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -17,6 +17,7 @@ //! - [`LongestCommonSubsequence`]: Longest Common Subsequence //! - [`MinimumTardinessSequencing`]: Minimize tardy tasks in single-machine scheduling //! - [`PaintShop`]: Minimize color switches in paint shop scheduling +//! - [`CosineProductIntegration`]: Balanced sign assignment for integer frequencies //! - [`Partition`]: Partition a multiset into two equal-sum subsets //! - [`PartiallyOrderedKnapsack`]: Knapsack with precedence constraints //! - [`PrecedenceConstrainedScheduling`]: Schedule unit tasks on processors by deadline @@ -68,6 +69,7 @@ mod capacity_assignment; pub(crate) mod conjunctive_boolean_query; pub(crate) mod conjunctive_query_foldability; mod consistency_of_database_frequency_tables; +mod cosine_product_integration; mod ensemble_computation; pub(crate) mod expected_retrieval_cost; pub(crate) mod factoring; @@ -109,6 +111,7 @@ pub use conjunctive_query_foldability::{ConjunctiveQueryFoldability, Term}; pub use consistency_of_database_frequency_tables::{ ConsistencyOfDatabaseFrequencyTables, FrequencyTable, KnownValue, }; +pub use cosine_product_integration::CosineProductIntegration; pub use ensemble_computation::EnsembleComputation; pub use expected_retrieval_cost::ExpectedRetrievalCost; pub use factoring::Factoring; @@ -182,5 +185,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec &Self::Target { + &self.target + } + + fn extract_solution(&self, target_solution: &[usize]) -> Vec { + target_solution.to_vec() + } +} + +#[reduction(overhead = { + num_coefficients = "num_elements", +})] +impl ReduceTo for Partition { + type Result = ReductionPartitionToCPI; + + fn reduce_to(&self) -> Self::Result { + let coefficients: Vec = self.sizes().iter().map(|&s| s as i64).collect(); + ReductionPartitionToCPI { + target: CosineProductIntegration::new(coefficients), + } + } +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + use crate::export::SolutionPair; + + vec![crate::example_db::specs::RuleExampleSpec { + id: "partition_to_cosineproductintegration", + build: || { + // sizes [3, 1, 1, 2, 2, 1]: partition {3,2,1}={6} and {1,2,1}={4}? No... + // Actually [3,1,1,2,2,1] sum=10, need sum=5 each. + // config [1,0,0,1,0,0] → selected={3,2}=5, rest={1,1,2,1}=5 ✓ + // sign assignment: bit=1→−, bit=0→+ : (+3,−1,−1,+2,−2,−1) = 3-1-1+2-2-1=0? No, 3-1-1+2-2-1=0. Yes! + // Wait: config [1,0,0,1,0,0] means elements 0,3 in subset 1. + // For CPI: bit 1 means −a_i. So −3+1+1−2+2+1 = 0. Yes! + crate::example_db::specs::rule_example_with_witness::<_, CosineProductIntegration>( + Partition::new(vec![3, 1, 1, 2, 2, 1]), + SolutionPair { + source_config: vec![1, 0, 0, 1, 0, 0], + target_config: vec![1, 0, 0, 1, 0, 0], + }, + ) + }, + }] +} + +#[cfg(test)] +#[path = "../unit_tests/rules/partition_cosineproductintegration.rs"] +mod tests; diff --git a/src/unit_tests/models/misc/cosine_product_integration.rs b/src/unit_tests/models/misc/cosine_product_integration.rs new file mode 100644 index 000000000..9d44ddda3 --- /dev/null +++ b/src/unit_tests/models/misc/cosine_product_integration.rs @@ -0,0 +1,101 @@ +use crate::models::misc::CosineProductIntegration; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; + +#[test] +fn test_cosine_product_integration_creation() { + let p = CosineProductIntegration::new(vec![2, 3, 5]); + assert_eq!(p.coefficients(), &[2, 3, 5]); + assert_eq!(p.num_coefficients(), 3); +} + +#[test] +fn test_cosine_product_integration_dims() { + let p = CosineProductIntegration::new(vec![1, 2, 3]); + assert_eq!(p.dims(), vec![2, 2, 2]); +} + +#[test] +fn test_cosine_product_integration_evaluate_satisfying() { + // [2, 3, 5]: (+2, +3, -5) = 0 → satisfying + let p = CosineProductIntegration::new(vec![2, 3, 5]); + assert!(p.evaluate(&[0, 0, 1]).0); +} + +#[test] +fn test_cosine_product_integration_evaluate_not_satisfying() { + // [2, 3, 5]: (+2, +3, +5) = 10 → not satisfying + let p = CosineProductIntegration::new(vec![2, 3, 5]); + assert!(!p.evaluate(&[0, 0, 0]).0); +} + +#[test] +fn test_cosine_product_integration_unsatisfiable() { + // [1, 2, 6]: total=9 (odd), no balanced sign assignment + let p = CosineProductIntegration::new(vec![1, 2, 6]); + let solver = BruteForce::new(); + assert!(solver.find_witness(&p).is_none()); +} + +#[test] +fn test_cosine_product_integration_solver() { + let p = CosineProductIntegration::new(vec![2, 3, 5]); + let solver = BruteForce::new(); + let witness = solver.find_witness(&p).unwrap(); + assert!(p.evaluate(&witness).0); +} + +#[test] +fn test_cosine_product_integration_aggregate() { + let p = CosineProductIntegration::new(vec![2, 3, 5]); + let solver = BruteForce::new(); + let value = solver.solve(&p); + assert!(value.0); + + let p2 = CosineProductIntegration::new(vec![1, 2, 6]); + let value2 = solver.solve(&p2); + assert!(!value2.0); +} + +#[test] +fn test_cosine_product_integration_negative_coefficients() { + // [-3, 2, 1]: (-(-3), +2, -1) = (3, 2, -1) = 4, not zero + // but (-3, +2, +1) = 0 → config [0, 0, 0] → -3+2+1=0 + let p = CosineProductIntegration::new(vec![-3, 2, 1]); + assert!(p.evaluate(&[0, 0, 0]).0); // -3 + 2 + 1 = 0 +} + +#[test] +fn test_cosine_product_integration_invalid_config() { + let p = CosineProductIntegration::new(vec![1, 2, 3]); + // Wrong length + assert!(!p.evaluate(&[0, 0]).0); + // Out of range + assert!(!p.evaluate(&[0, 2, 0]).0); +} + +#[test] +fn test_cosine_product_integration_serialization() { + let p = CosineProductIntegration::new(vec![2, 3, 5]); + let json = serde_json::to_string(&p).unwrap(); + let p2: CosineProductIntegration = serde_json::from_str(&json).unwrap(); + assert_eq!(p2.coefficients(), p.coefficients()); +} + +#[test] +fn test_cosine_product_integration_all_witnesses() { + // [2, 3, 5]: two balanced assignments: (+2,+3,-5)=0 and (-2,-3,+5)=0 + let p = CosineProductIntegration::new(vec![2, 3, 5]); + let solver = BruteForce::new(); + let witnesses = solver.find_all_witnesses(&p); + assert_eq!(witnesses.len(), 2); + for w in &witnesses { + assert!(p.evaluate(w).0); + } +} + +#[test] +#[should_panic] +fn test_cosine_product_integration_empty() { + CosineProductIntegration::new(vec![]); +} diff --git a/src/unit_tests/rules/partition_cosineproductintegration.rs b/src/unit_tests/rules/partition_cosineproductintegration.rs new file mode 100644 index 000000000..410a1d9c8 --- /dev/null +++ b/src/unit_tests/rules/partition_cosineproductintegration.rs @@ -0,0 +1,103 @@ +use super::*; +use crate::models::misc::{CosineProductIntegration, Partition}; +use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; +use crate::solvers::BruteForce; +use crate::traits::Problem; + +fn reduce_partition(sizes: &[u64]) -> (Partition, ReductionPartitionToCPI) { + let source = Partition::new(sizes.to_vec()); + let reduction = ReduceTo::::reduce_to(&source); + (source, reduction) +} + +fn assert_satisfiability_matches( + source: &Partition, + target: &CosineProductIntegration, + expected: bool, +) { + let solver = BruteForce::new(); + assert_eq!(solver.find_witness(source).is_some(), expected); + assert_eq!(solver.find_witness(target).is_some(), expected); +} + +#[test] +fn test_partition_to_cosineproductintegration_closed_loop() { + let (source, reduction) = reduce_partition(&[3, 1, 1, 2, 2, 1]); + assert_satisfaction_round_trip_from_satisfaction_target( + &source, + &reduction, + "Partition -> CosineProductIntegration closed loop", + ); +} + +#[test] +fn test_partition_to_cosineproductintegration_structure() { + let (source, reduction) = reduce_partition(&[2, 3, 5]); + let target = reduction.target_problem(); + + assert_eq!(target.coefficients(), &[2, 3, 5]); + assert_eq!(target.num_coefficients(), source.num_elements()); +} + +#[test] +fn test_partition_to_cosineproductintegration_odd_sum() { + // sum = 2+4+5 = 11 (odd), no valid partition / no balanced sign assignment + let (source, reduction) = reduce_partition(&[2, 4, 5]); + let target = reduction.target_problem(); + assert_satisfiability_matches(&source, target, false); +} + +#[test] +fn test_partition_to_cosineproductintegration_equal_elements() { + let (source, reduction) = reduce_partition(&[3, 3, 3, 3]); + let target = reduction.target_problem(); + assert_satisfiability_matches(&source, target, true); + + assert_satisfaction_round_trip_from_satisfaction_target( + &source, + &reduction, + "Partition -> CPI equal elements", + ); +} + +#[test] +fn test_partition_to_cosineproductintegration_solution_extraction() { + let (source, reduction) = reduce_partition(&[1, 2, 3, 4]); + let target = reduction.target_problem(); + + let solver = BruteForce::new(); + let target_solutions = solver.find_all_witnesses(target); + + for sol in &target_solutions { + let extracted = reduction.extract_solution(sol); + assert_eq!(extracted.len(), source.num_elements()); + let target_valid = target.evaluate(sol); + let source_valid = source.evaluate(&extracted); + if target_valid.0 { + assert!( + source_valid.0, + "Valid CPI solution should yield valid Partition" + ); + } + } +} + +#[test] +fn test_partition_to_cosineproductintegration_single_element() { + let (source, reduction) = reduce_partition(&[4]); + let target = reduction.target_problem(); + assert_satisfiability_matches(&source, target, false); +} + +#[test] +fn test_partition_to_cosineproductintegration_two_elements() { + let (source, reduction) = reduce_partition(&[5, 5]); + let target = reduction.target_problem(); + assert_satisfiability_matches(&source, target, true); + + assert_satisfaction_round_trip_from_satisfaction_target( + &source, + &reduction, + "Partition -> CPI two elements", + ); +}