From aef42d563ff04304f9fafd4f9e9fe38b947553eb Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Sat, 28 Mar 2026 20:41:27 +0800 Subject: [PATCH 1/5] feat: add QuadraticDiophantineEquations model (#543) Add decision problem: given positive integers a, b, c, determine whether there exist positive integers x, y such that ax^2 + by = c. Single variable x with y derived; complexity O(sqrt(c)) by trial. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/reductions.typ | 48 ++++ problemreductions-cli/src/cli.rs | 10 + problemreductions-cli/src/commands/create.rs | 35 ++- src/models/algebraic/mod.rs | 4 + .../quadratic_diophantine_equations.rs | 219 ++++++++++++++++++ src/models/mod.rs | 3 +- .../quadratic_diophantine_equations.rs | 173 ++++++++++++++ 7 files changed, 490 insertions(+), 2 deletions(-) create mode 100644 src/models/algebraic/quadratic_diophantine_equations.rs create mode 100644 src/unit_tests/models/algebraic/quadratic_diophantine_equations.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index de79d7edb..650b1bcf2 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -174,6 +174,7 @@ "PrecedenceConstrainedScheduling": [Precedence Constrained Scheduling], "PrimeAttributeName": [Prime Attribute Name], "QuadraticAssignment": [Quadratic Assignment], + "QuadraticDiophantineEquations": [Quadratic Diophantine Equations], "QuantifiedBooleanFormulas": [Quantified Boolean Formulas (QBF)], "RectilinearPictureCompression": [Rectilinear Picture Compression], "ResourceConstrainedScheduling": [Resource Constrained Scheduling], @@ -3253,6 +3254,53 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], ] } +#{ + let x = load-model-example("QuadraticDiophantineEquations") + let a = x.instance.a + let b = x.instance.b + let c = x.instance.c + let config = x.optimal_config + let xval = config.at(0) + 1 + let yval = int((c - a * xval * xval) / b) + // Enumerate all valid x values for the table + let max-x = calc.floor(calc.sqrt(c / a)) + let rows = range(1, max-x + 1).map(xi => { + let rem = c - a * xi * xi + let feasible = rem > 0 and calc.rem(rem, b) == 0 + let yi = if feasible { int(rem / b) } else { none } + (xi, rem, feasible, yi) + }) + [ + #problem-def("QuadraticDiophantineEquations")[ + Given positive integers $a$, $b$, $c$, determine whether there exist positive integers $x$, $y$ such that $a x^2 + b y = c$. + ][ + Quadratic Diophantine equations of the form $a x^2 + b y = c$ form one of the simplest families of mixed-degree Diophantine problems. The variable $y$ is entirely determined by $x$ via $y = (c - a x^2) slash b$, so the decision problem reduces to checking whether any $x in {1, dots, floor(sqrt(c slash a))}$ yields a positive integer $y$. This can be done in $O(sqrt(c))$ time by trial#footnote[No algorithm improving on brute-force trial of all candidate $x$ values is known; the registered complexity `sqrt(c_val)` reflects this direct enumeration bound.]. + + *Example.* Let $a = #a$, $b = #b$, $c = #c$. Then $x$ ranges over $1, dots, #max-x$: + + #pred-commands( + "pred create --example QuadraticDiophantineEquations -o qde.json", + "pred solve qde.json", + "pred evaluate qde.json --config " + config.map(str).join(","), + ) + + #align(center, table( + columns: 4, + align: center, + table.header([$x$], [$c - a x^2$], [Divisible by $b$?], [$y$]), + ..rows.map(((xi, rem, ok, yi)) => ( + [$#xi$], + [$#rem$], + [#if ok [Yes] else [No]], + [#if yi != none [$#yi$] else [$dash$]], + )).flatten(), + )) + + The instance is satisfiable: $x = #xval, y = #yval$ gives $#a dot #xval^2 + #b dot #yval = #c$. + ] + ] +} + #{ let x = load-model-example("ClosestVectorProblem") let basis = x.instance.basis diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index fe0b7bd35..61441bfea 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -248,6 +248,7 @@ Flags by problem type: CapacityAssignment --capacities, --cost-matrix, --delay-matrix, --delay-budget SubsetSum --sizes, --target ThreePartition --sizes, --bound + QuadraticDiophantineEquations --coeff-a, --coeff-b, --rhs SumOfSquaresPartition --sizes, --num-groups ExpectedRetrievalCost --probabilities, --num-sectors PaintShop --sequence @@ -732,6 +733,15 @@ pub struct CreateArgs { /// Target string for StringToStringCorrection (comma-separated symbol indices, e.g., "0,1,3,2") #[arg(long)] pub target_string: Option, + /// Coefficient a for QuadraticDiophantineEquations (coefficient of x²) + #[arg(long)] + pub coeff_a: Option, + /// Coefficient b for QuadraticDiophantineEquations (coefficient of y) + #[arg(long)] + pub coeff_b: Option, + /// Right-hand side c for QuadraticDiophantineEquations + #[arg(long)] + pub rhs: Option, } #[derive(clap::Args)] diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index d295670c6..ac8eddf95 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -9,7 +9,7 @@ use anyhow::{bail, Context, Result}; use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExample}; use problemreductions::models::algebraic::{ ClosestVectorProblem, ConsecutiveBlockMinimization, ConsecutiveOnesMatrixAugmentation, - ConsecutiveOnesSubmatrix, SparseMatrixCompression, BMF, + ConsecutiveOnesSubmatrix, QuadraticDiophantineEquations, SparseMatrixCompression, BMF, }; use problemreductions::models::formula::Quantifier; use problemreductions::models::graph::{ @@ -189,6 +189,9 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.conjuncts_spec.is_none() && args.deps.is_none() && args.query.is_none() + && args.coeff_a.is_none() + && args.coeff_b.is_none() + && args.rhs.is_none() } fn emit_problem_output(output: &ProblemJsonOutput, out: &OutputConfig) -> Result<()> { @@ -718,6 +721,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { } "SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11", "ThreePartition" => "--sizes 4,5,6,4,6,5 --bound 15", + "QuadraticDiophantineEquations" => "--coeff-a 3 --coeff-b 5 --rhs 53", "BoyceCoddNormalFormViolation" => { "--n 6 --sets \"0,1:2;2:3;3,4:5\" --target 0,1,2,3,4,5" } @@ -2367,6 +2371,32 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // QuadraticDiophantineEquations + "QuadraticDiophantineEquations" => { + let a = args.coeff_a.ok_or_else(|| { + anyhow::anyhow!( + "QuadraticDiophantineEquations requires --coeff-a, --coeff-b, and --rhs\n\n\ + Usage: pred create QuadraticDiophantineEquations --coeff-a 3 --coeff-b 5 --rhs 53" + ) + })?; + let b = args.coeff_b.ok_or_else(|| { + anyhow::anyhow!( + "QuadraticDiophantineEquations requires --coeff-b\n\n\ + Usage: pred create QuadraticDiophantineEquations --coeff-a 3 --coeff-b 5 --rhs 53" + ) + })?; + let c = args.rhs.ok_or_else(|| { + anyhow::anyhow!( + "QuadraticDiophantineEquations requires --rhs\n\n\ + Usage: pred create QuadraticDiophantineEquations --coeff-a 3 --coeff-b 5 --rhs 53" + ) + })?; + ( + ser(QuadraticDiophantineEquations::try_new(a, b, c).map_err(anyhow::Error::msg)?)?, + resolved_variant.clone(), + ) + } + // SumOfSquaresPartition "SumOfSquaresPartition" => { let sizes_str = args.sizes.as_deref().ok_or_else(|| { @@ -7344,6 +7374,9 @@ mod tests { storage: None, quantifiers: None, homologous_pairs: None, + coeff_a: None, + coeff_b: None, + rhs: None, } } diff --git a/src/models/algebraic/mod.rs b/src/models/algebraic/mod.rs index d341270c4..ed1b50e5f 100644 --- a/src/models/algebraic/mod.rs +++ b/src/models/algebraic/mod.rs @@ -8,6 +8,7 @@ //! - [`ConsecutiveBlockMinimization`]: Consecutive Block Minimization //! - [`ConsecutiveOnesSubmatrix`]: Consecutive Ones Submatrix (column selection with C1P) //! - [`QuadraticAssignment`]: Quadratic Assignment Problem +//! - [`QuadraticDiophantineEquations`]: Decide ax² + by = c in positive integers //! - [`SparseMatrixCompression`]: Sparse Matrix Compression by row overlay pub(crate) mod bmf; @@ -17,6 +18,7 @@ pub(crate) mod consecutive_ones_matrix_augmentation; pub(crate) mod consecutive_ones_submatrix; pub(crate) mod ilp; pub(crate) mod quadratic_assignment; +pub(crate) mod quadratic_diophantine_equations; pub(crate) mod qubo; pub(crate) mod sparse_matrix_compression; @@ -27,6 +29,7 @@ pub use consecutive_ones_matrix_augmentation::ConsecutiveOnesMatrixAugmentation; pub use consecutive_ones_submatrix::ConsecutiveOnesSubmatrix; pub use ilp::{Comparison, LinearConstraint, ObjectiveSense, VariableDomain, ILP}; pub use quadratic_assignment::QuadraticAssignment; +pub use quadratic_diophantine_equations::QuadraticDiophantineEquations; pub use qubo::QUBO; pub use sparse_matrix_compression::SparseMatrixCompression; @@ -41,6 +44,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Result<(), String> { + if a == 0 { + return Err("Coefficient a must be positive".to_string()); + } + if b == 0 { + return Err("Coefficient b must be positive".to_string()); + } + if c == 0 { + return Err("Right-hand side c must be positive".to_string()); + } + Ok(()) + } + + /// Create a new QuadraticDiophantineEquations instance, returning an error + /// instead of panicking when inputs are invalid. + pub fn try_new(a: u64, b: u64, c: u64) -> Result { + Self::validate_inputs(a, b, c)?; + Ok(Self { a, b, c }) + } + + /// Create a new QuadraticDiophantineEquations instance. + /// + /// # Panics + /// + /// Panics if any of a, b, c is zero. + pub fn new(a: u64, b: u64, c: u64) -> Self { + Self::try_new(a, b, c).unwrap_or_else(|msg| panic!("{msg}")) + } + + /// Get the coefficient a (coefficient of x²). + pub fn a(&self) -> u64 { + self.a + } + + /// Get the coefficient b (coefficient of y). + pub fn b(&self) -> u64 { + self.b + } + + /// Get the right-hand side constant c. + pub fn c_val(&self) -> u64 { + self.c + } + + /// Compute the integer square root of n (floor(sqrt(n))). + fn isqrt(n: u64) -> u64 { + if n == 0 { + return 0; + } + let mut x = (n as f64).sqrt() as u64; + // Correct for floating-point imprecision. + while x.saturating_mul(x) > n { + x -= 1; + } + while (x + 1).saturating_mul(x + 1) <= n { + x += 1; + } + x + } + + /// Compute the maximum value of x (floor(sqrt(c/a))). + /// Returns 0 if c < a (no positive x is possible since x >= 1 requires a*1 <= c). + fn max_x(&self) -> u64 { + if self.c < self.a { + return 0; + } + Self::isqrt(self.c / self.a) + } + + /// Check whether a given x yields a valid positive integer y. + /// + /// Returns Some(y) if y is a positive integer, None otherwise. + pub fn check_x(&self, x: u64) -> Option { + if x == 0 { + return None; + } + let ax2 = self.a.checked_mul(x)?.checked_mul(x)?; + if ax2 >= self.c { + return None; + } + let remainder = self.c - ax2; + if !remainder.is_multiple_of(self.b) { + return None; + } + let y = remainder / self.b; + if y == 0 { + return None; + } + Some(y) + } +} + +#[derive(Deserialize)] +struct QuadraticDiophantineEquationsData { + a: u64, + b: u64, + c: u64, +} + +impl<'de> Deserialize<'de> for QuadraticDiophantineEquations { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let data = QuadraticDiophantineEquationsData::deserialize(deserializer)?; + Self::try_new(data.a, data.b, data.c).map_err(D::Error::custom) + } +} + +impl Problem for QuadraticDiophantineEquations { + const NAME: &'static str = "QuadraticDiophantineEquations"; + type Value = Or; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + let max_x = self.max_x() as usize; + if max_x == 0 { + // No valid x exists; return empty config space. + return vec![0]; + } + // One variable: x ranges over {1, ..., max_x}. + // config[0] in {0, ..., max_x - 1} maps to x = config[0] + 1. + vec![max_x] + } + + fn evaluate(&self, config: &[usize]) -> Or { + Or({ + if config.len() != 1 { + return Or(false); + } + let x = (config[0] as u64) + 1; // 1-indexed + self.check_x(x).is_some() + }) + } +} + +crate::declare_variants! { + default QuadraticDiophantineEquations => "sqrt(c_val)", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "quadratic_diophantine_equations", + instance: Box::new(QuadraticDiophantineEquations::new(3, 5, 53)), + // x=1 (config[0]=0) gives y=10: 3*1 + 5*10 = 53 + optimal_config: vec![0], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/algebraic/quadratic_diophantine_equations.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index a3e77f0a1..28e1cb7be 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -11,7 +11,8 @@ pub mod set; // Re-export commonly used types pub use algebraic::{ ClosestVectorProblem, ConsecutiveBlockMinimization, ConsecutiveOnesMatrixAugmentation, - ConsecutiveOnesSubmatrix, QuadraticAssignment, SparseMatrixCompression, BMF, ILP, QUBO, + ConsecutiveOnesSubmatrix, QuadraticAssignment, QuadraticDiophantineEquations, + SparseMatrixCompression, BMF, ILP, QUBO, }; pub use formula::{ CNFClause, CircuitSAT, KSatisfiability, NAESatisfiability, QuantifiedBooleanFormulas, diff --git a/src/unit_tests/models/algebraic/quadratic_diophantine_equations.rs b/src/unit_tests/models/algebraic/quadratic_diophantine_equations.rs new file mode 100644 index 000000000..9b63ef50d --- /dev/null +++ b/src/unit_tests/models/algebraic/quadratic_diophantine_equations.rs @@ -0,0 +1,173 @@ +use crate::models::algebraic::QuadraticDiophantineEquations; +use crate::solvers::BruteForce; +use crate::traits::Problem; +use crate::types::Or; + +fn yes_problem() -> QuadraticDiophantineEquations { + // a=3, b=5, c=53: x=1 gives y=10, x=4 gives y=1 + QuadraticDiophantineEquations::new(3, 5, 53) +} + +fn no_problem() -> QuadraticDiophantineEquations { + // a=3, b=5, c=10: x=1 gives 5y=7, not integer + QuadraticDiophantineEquations::new(3, 5, 10) +} + +#[test] +fn test_quadratic_diophantine_equations_basic() { + let problem = yes_problem(); + assert_eq!(problem.a(), 3); + assert_eq!(problem.b(), 5); + assert_eq!(problem.c_val(), 53); + // max_x = isqrt(53/3) = isqrt(17) = 4 + assert_eq!(problem.dims(), vec![4]); + assert_eq!(problem.num_variables(), 1); + assert_eq!( + ::NAME, + "QuadraticDiophantineEquations" + ); + assert_eq!( + ::variant(), + vec![] + ); +} + +#[test] +fn test_quadratic_diophantine_equations_evaluate_yes() { + let problem = yes_problem(); + // config[0]=0 -> x=1: 3*1 + 5y = 53, y=10 + assert_eq!(problem.evaluate(&[0]), Or(true)); + // config[0]=1 -> x=2: 3*4 + 5y = 53, 5y=41, not integer + assert_eq!(problem.evaluate(&[1]), Or(false)); + // config[0]=2 -> x=3: 3*9 + 5y = 53, 5y=26, not integer + assert_eq!(problem.evaluate(&[2]), Or(false)); + // config[0]=3 -> x=4: 3*16 + 5y = 53, 5y=5, y=1 + assert_eq!(problem.evaluate(&[3]), Or(true)); +} + +#[test] +fn test_quadratic_diophantine_equations_evaluate_no() { + let problem = no_problem(); + // max_x = isqrt(10/3) = isqrt(3) = 1 + assert_eq!(problem.dims(), vec![1]); + // config[0]=0 -> x=1: 3*1 + 5y = 10, 5y=7, not integer + assert_eq!(problem.evaluate(&[0]), Or(false)); +} + +#[test] +fn test_quadratic_diophantine_equations_evaluate_invalid_config() { + let problem = yes_problem(); + // Wrong length + assert_eq!(problem.evaluate(&[]), Or(false)); + assert_eq!(problem.evaluate(&[0, 1]), Or(false)); +} + +#[test] +fn test_quadratic_diophantine_equations_solver_finds_witness() { + let problem = yes_problem(); + let solver = BruteForce::new(); + let witness = solver.find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&witness), Or(true)); +} + +#[test] +fn test_quadratic_diophantine_equations_solver_finds_all_witnesses() { + let problem = yes_problem(); + let solver = BruteForce::new(); + let all = solver.find_all_witnesses(&problem); + // Two solutions: x=1 (config[0]=0) and x=4 (config[0]=3) + assert_eq!(all.len(), 2); + assert!(all.iter().all(|sol| problem.evaluate(sol) == Or(true))); +} + +#[test] +fn test_quadratic_diophantine_equations_solver_no_witness() { + let problem = no_problem(); + let solver = BruteForce::new(); + assert!(solver.find_witness(&problem).is_none()); +} + +#[test] +fn test_quadratic_diophantine_equations_serialization() { + let problem = yes_problem(); + let json = serde_json::to_value(&problem).unwrap(); + assert_eq!( + json, + serde_json::json!({ + "a": 3, + "b": 5, + "c": 53, + }) + ); + + let restored: QuadraticDiophantineEquations = serde_json::from_value(json).unwrap(); + assert_eq!(restored.a(), problem.a()); + assert_eq!(restored.b(), problem.b()); + assert_eq!(restored.c_val(), problem.c_val()); +} + +#[test] +fn test_quadratic_diophantine_equations_deserialization_rejects_invalid() { + // a=0 + let result: Result = + serde_json::from_value(serde_json::json!({"a": 0, "b": 5, "c": 53})); + assert!(result.is_err()); + // b=0 + let result: Result = + serde_json::from_value(serde_json::json!({"a": 3, "b": 0, "c": 53})); + assert!(result.is_err()); + // c=0 + let result: Result = + serde_json::from_value(serde_json::json!({"a": 3, "b": 5, "c": 0})); + assert!(result.is_err()); +} + +#[test] +fn test_quadratic_diophantine_equations_check_x() { + let problem = yes_problem(); + assert_eq!(problem.check_x(1), Some(10)); // 3 + 50 = 53 + assert_eq!(problem.check_x(2), None); // 12 + 5y = 53, 41/5 not integer + assert_eq!(problem.check_x(3), None); // 27 + 5y = 53, 26/5 not integer + assert_eq!(problem.check_x(4), Some(1)); // 48 + 5 = 53 + assert_eq!(problem.check_x(5), None); // 75 > 53 + assert_eq!(problem.check_x(0), None); // x must be positive +} + +#[test] +fn test_quadratic_diophantine_equations_edge_case_c_less_than_a() { + // c < a: no valid x since a*1^2 = a > c + let problem = QuadraticDiophantineEquations::new(10, 1, 5); + assert_eq!(problem.dims(), vec![0]); +} + +#[test] +fn test_quadratic_diophantine_equations_paper_example() { + // From issue: a=3, b=5, c=53. x=1: y=10, x=4: y=1. + let problem = QuadraticDiophantineEquations::new(3, 5, 53); + // Verify the claimed solution x=1 (config[0]=0) + assert_eq!(problem.evaluate(&[0]), Or(true)); + // Verify x=4 (config[0]=3) also works + assert_eq!(problem.evaluate(&[3]), Or(true)); + + let solver = BruteForce::new(); + let all = solver.find_all_witnesses(&problem); + assert_eq!(all.len(), 2); +} + +#[test] +#[should_panic(expected = "Coefficient a must be positive")] +fn test_quadratic_diophantine_equations_panics_on_zero_a() { + QuadraticDiophantineEquations::new(0, 5, 53); +} + +#[test] +#[should_panic(expected = "Coefficient b must be positive")] +fn test_quadratic_diophantine_equations_panics_on_zero_b() { + QuadraticDiophantineEquations::new(3, 0, 53); +} + +#[test] +#[should_panic(expected = "Right-hand side c must be positive")] +fn test_quadratic_diophantine_equations_panics_on_zero_c() { + QuadraticDiophantineEquations::new(3, 5, 0); +} From 988d6a802bcd43b87afa9476d02f754393d236d6 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Sun, 29 Mar 2026 23:25:50 +0800 Subject: [PATCH 2/5] =?UTF-8?q?Fix=20agentic=20review=20issues:=20rename?= =?UTF-8?q?=20c=5Fval=E2=86=92c=20getter,=20fix=20paper=20solve=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename `c_val()` getter to `c()` so user-facing catalog output shows the mathematical parameter name instead of an implementation detail - Add `--solver brute-force` to paper's `pred solve` command since QuadraticDiophantineEquations has no ILP reduction path Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/reductions.typ | 2 +- src/models/algebraic/quadratic_diophantine_equations.rs | 6 +++--- .../models/algebraic/quadratic_diophantine_equations.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index e6bdedf42..c6a727848 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -3282,7 +3282,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], #pred-commands( "pred create --example QuadraticDiophantineEquations -o qde.json", - "pred solve qde.json", + "pred solve qde.json --solver brute-force", "pred evaluate qde.json --config " + config.map(str).join(","), ) diff --git a/src/models/algebraic/quadratic_diophantine_equations.rs b/src/models/algebraic/quadratic_diophantine_equations.rs index 01d91b90e..039ac21ad 100644 --- a/src/models/algebraic/quadratic_diophantine_equations.rs +++ b/src/models/algebraic/quadratic_diophantine_equations.rs @@ -28,7 +28,7 @@ inventory::submit! { inventory::submit! { ProblemSizeFieldEntry { name: "QuadraticDiophantineEquations", - fields: &["c_val"], + fields: &["c"], } } @@ -100,7 +100,7 @@ impl QuadraticDiophantineEquations { } /// Get the right-hand side constant c. - pub fn c_val(&self) -> u64 { + pub fn c(&self) -> u64 { self.c } @@ -200,7 +200,7 @@ impl Problem for QuadraticDiophantineEquations { } crate::declare_variants! { - default QuadraticDiophantineEquations => "sqrt(c_val)", + default QuadraticDiophantineEquations => "sqrt(c)", } #[cfg(feature = "example-db")] diff --git a/src/unit_tests/models/algebraic/quadratic_diophantine_equations.rs b/src/unit_tests/models/algebraic/quadratic_diophantine_equations.rs index 9b63ef50d..8f727c759 100644 --- a/src/unit_tests/models/algebraic/quadratic_diophantine_equations.rs +++ b/src/unit_tests/models/algebraic/quadratic_diophantine_equations.rs @@ -18,7 +18,7 @@ fn test_quadratic_diophantine_equations_basic() { let problem = yes_problem(); assert_eq!(problem.a(), 3); assert_eq!(problem.b(), 5); - assert_eq!(problem.c_val(), 53); + assert_eq!(problem.c(), 53); // max_x = isqrt(53/3) = isqrt(17) = 4 assert_eq!(problem.dims(), vec![4]); assert_eq!(problem.num_variables(), 1); @@ -103,7 +103,7 @@ fn test_quadratic_diophantine_equations_serialization() { let restored: QuadraticDiophantineEquations = serde_json::from_value(json).unwrap(); assert_eq!(restored.a(), problem.a()); assert_eq!(restored.b(), problem.b()); - assert_eq!(restored.c_val(), problem.c_val()); + assert_eq!(restored.c(), problem.c()); } #[test] From 9c8009d14e95fdcf75ee252f52d94ae3de087124 Mon Sep 17 00:00:00 2001 From: zazabap Date: Sun, 29 Mar 2026 16:49:09 +0000 Subject: [PATCH 3/5] fix: rename duplicate rhs CLI field to qde_rhs for QuadraticDiophantineEquations --- problemreductions-cli/src/cli.rs | 4 ++-- problemreductions-cli/src/commands/create.rs | 16 +++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index a1a22f14e..db72a56d2 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -249,7 +249,7 @@ Flags by problem type: ProductionPlanning --num-periods, --demands, --capacities, --setup-costs, --production-costs, --inventory-costs, --cost-bound SubsetSum --sizes, --target ThreePartition --sizes, --bound - QuadraticDiophantineEquations --coeff-a, --coeff-b, --rhs + QuadraticDiophantineEquations --coeff-a, --coeff-b, --qde-rhs SumOfSquaresPartition --sizes, --num-groups ExpectedRetrievalCost --probabilities, --num-sectors PaintShop --sequence @@ -763,7 +763,7 @@ pub struct CreateArgs { pub coeff_b: Option, /// Right-hand side c for QuadraticDiophantineEquations #[arg(long)] - pub rhs: Option, + pub qde_rhs: Option, } #[derive(clap::Args)] diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 1a004ff38..5256d2b52 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -197,6 +197,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.coeff_a.is_none() && args.coeff_b.is_none() && args.rhs.is_none() + && args.qde_rhs.is_none() && args.required_columns.is_none() } @@ -731,7 +732,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "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", - "QuadraticDiophantineEquations" => "--coeff-a 3 --coeff-b 5 --rhs 53", + "QuadraticDiophantineEquations" => "--coeff-a 3 --coeff-b 5 --qde-rhs 53", "BoyceCoddNormalFormViolation" => { "--n 6 --sets \"0,1:2;2:3;3,4:5\" --target 0,1,2,3,4,5" } @@ -2426,20 +2427,20 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { "QuadraticDiophantineEquations" => { let a = args.coeff_a.ok_or_else(|| { anyhow::anyhow!( - "QuadraticDiophantineEquations requires --coeff-a, --coeff-b, and --rhs\n\n\ - Usage: pred create QuadraticDiophantineEquations --coeff-a 3 --coeff-b 5 --rhs 53" + "QuadraticDiophantineEquations requires --coeff-a, --coeff-b, and --qde-rhs\n\n\ + Usage: pred create QuadraticDiophantineEquations --coeff-a 3 --coeff-b 5 --qde-rhs 53" ) })?; let b = args.coeff_b.ok_or_else(|| { anyhow::anyhow!( "QuadraticDiophantineEquations requires --coeff-b\n\n\ - Usage: pred create QuadraticDiophantineEquations --coeff-a 3 --coeff-b 5 --rhs 53" + Usage: pred create QuadraticDiophantineEquations --coeff-a 3 --coeff-b 5 --qde-rhs 53" ) })?; - let c = args.rhs.ok_or_else(|| { + let c = args.qde_rhs.ok_or_else(|| { anyhow::anyhow!( - "QuadraticDiophantineEquations requires --rhs\n\n\ - Usage: pred create QuadraticDiophantineEquations --coeff-a 3 --coeff-b 5 --rhs 53" + "QuadraticDiophantineEquations requires --qde-rhs\n\n\ + Usage: pred create QuadraticDiophantineEquations --coeff-a 3 --coeff-b 5 --qde-rhs 53" ) })?; ( @@ -7736,6 +7737,7 @@ mod tests { coeff_a: None, coeff_b: None, rhs: None, + qde_rhs: None, required_columns: None, } } From 28fb2470928717519da6342e52fbf35bede333b6 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Mon, 30 Mar 2026 01:12:07 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20remove=20OLA=E2=86=92STMWCT=20rule?= =?UTF-8?q?=20file=20accidentally=20included=20from=20merge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The file was brought in by a contaminated merge commit and does not belong to the QuadraticDiophantineEquations model PR. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...uencingtominimizeweightedcompletiontime.rs | 186 ------------------ 1 file changed, 186 deletions(-) delete mode 100644 src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs diff --git a/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs b/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs deleted file mode 100644 index bd3c25d2b..000000000 --- a/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs +++ /dev/null @@ -1,186 +0,0 @@ -//! Reduction from OptimalLinearArrangement to SequencingToMinimizeWeightedCompletionTime. -//! -//! Based on Lawler (1978) and Lawler-Queyranne-Schulz-Shmoys (LQSS), Lemma 4.14. -//! Vertex jobs have unit processing time and weight `d_max - deg(v)`, while -//! zero-processing-time edge jobs with weight 2 are constrained to follow both -//! endpoints. The total weighted completion time equals the OLA cost plus a -//! constant `d_max * n * (n + 1) / 2`. - -use crate::models::graph::OptimalLinearArrangement; -use crate::models::misc::SequencingToMinimizeWeightedCompletionTime; -use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; -use crate::topology::{Graph, SimpleGraph}; - -/// Result of reducing OptimalLinearArrangement to SequencingToMinimizeWeightedCompletionTime. -#[derive(Debug, Clone)] -pub struct ReductionOLAToSTMWCT { - target: SequencingToMinimizeWeightedCompletionTime, - /// Number of vertices in the source graph (vertex tasks are indices 0..num_vertices). - num_vertices: usize, -} - -impl ReductionResult for ReductionOLAToSTMWCT { - type Source = OptimalLinearArrangement; - type Target = SequencingToMinimizeWeightedCompletionTime; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - /// Extract the OLA solution from the scheduling solution. - /// - /// The first `num_vertices` tasks in the schedule correspond to vertex tasks. - /// Their ordering in the schedule gives the linear arrangement: if vertex task - /// v is at schedule position p, then f(v) = p (among the vertex tasks only). - fn extract_solution(&self, target_solution: &[usize]) -> Vec { - // Decode the Lehmer code to get the schedule (task execution order). - let schedule = decode_lehmer(target_solution, target_solution.len()) - .expect("target solution must be a valid Lehmer code"); - - // Collect vertex task positions in the schedule. - // The OLA config maps vertex -> position in {0..n-1}. - let n = self.num_vertices; - let mut vertex_order: Vec = Vec::with_capacity(n); - for &task in &schedule { - if task < n { - vertex_order.push(task); - } - } - - // vertex_order[i] = the vertex that appears at position i among vertex tasks. - // We need config[vertex] = position, so invert this. - let mut config = vec![0usize; n]; - for (position, &vertex) in vertex_order.iter().enumerate() { - config[vertex] = position; - } - config - } -} - -/// Decode a Lehmer code into a permutation (mirrors the misc module helper). -fn decode_lehmer(config: &[usize], n: usize) -> Option> { - if config.len() != n { - return None; - } - let mut available: Vec = (0..n).collect(); - let mut schedule = Vec::with_capacity(n); - for &digit in config { - if digit >= available.len() { - return None; - } - schedule.push(available.remove(digit)); - } - Some(schedule) -} - -/// Encode a schedule (permutation) as a Lehmer code. -fn encode_schedule_as_lehmer(schedule: &[usize]) -> Vec { - let mut available: Vec = (0..schedule.len()).collect(); - let mut config = Vec::with_capacity(schedule.len()); - for &task in schedule { - let digit = available - .iter() - .position(|&candidate| candidate == task) - .expect("schedule must be a permutation"); - config.push(digit); - available.remove(digit); - } - config -} - -#[reduction(overhead = { - num_tasks = "num_vertices + num_edges", -})] -impl ReduceTo - for OptimalLinearArrangement -{ - type Result = ReductionOLAToSTMWCT; - - fn reduce_to(&self) -> Self::Result { - let n = self.graph().num_vertices(); - let edges: Vec<(usize, usize)> = self.graph().edges(); - let m = edges.len(); - - // Compute vertex degrees. - let mut degrees = vec![0u64; n]; - for &(u, v) in &edges { - degrees[u] += 1; - degrees[v] += 1; - } - let d_max = degrees.iter().copied().max().unwrap_or(0); - - // Build task arrays: n vertex tasks + m edge tasks. - let total_tasks = n + m; - let mut lengths = Vec::with_capacity(total_tasks); - let mut weights = Vec::with_capacity(total_tasks); - let mut precedences = Vec::new(); - - // Vertex tasks: l = 1, w = d_max - deg(v). - for v in 0..n { - lengths.push(1u64); - weights.push(d_max - degrees[v]); - } - - // Edge tasks: l = 0, w = 2. - // Precedence: both endpoint vertex tasks must precede the edge task. - for (idx, &(u, v)) in edges.iter().enumerate() { - let edge_task = n + idx; - lengths.push(0u64); - weights.push(2u64); - precedences.push((u, edge_task)); - precedences.push((v, edge_task)); - } - - let target = - SequencingToMinimizeWeightedCompletionTime::new(lengths, weights, precedences); - - ReductionOLAToSTMWCT { - target, - num_vertices: n, - } - } -} - -#[cfg(feature = "example-db")] -pub(crate) fn canonical_rule_example_specs() -> Vec { - use crate::export::SolutionPair; - - vec![crate::example_db::specs::RuleExampleSpec { - id: "optimallineararrangement_to_sequencingtominimizeweightedcompletiontime", - build: || { - // Path graph P_4: 0-1-2-3 - let source = OptimalLinearArrangement::new(SimpleGraph::new( - 4, - vec![(0, 1), (1, 2), (2, 3)], - )); - - // Optimal arrangement: identity [0,1,2,3] with OLA cost 3. - // d_max = 2, constant = 2 * 4 * 5 / 2 = 20, scheduling cost = 23. - // - // Schedule: vertex tasks in order [0,1,2,3], edge tasks immediately - // after both endpoints. - // Full schedule: [0, 1, t_01, 2, t_12, 3, t_23] = [0, 1, 4, 2, 5, 3, 6] - let schedule = vec![0, 1, 4, 2, 5, 3, 6]; - let target_config = encode_schedule_as_lehmer(&schedule); - - // Source config: vertex -> position (0-indexed) - let source_config = vec![0, 1, 2, 3]; - - crate::example_db::specs::rule_example_with_witness::< - _, - SequencingToMinimizeWeightedCompletionTime, - >( - source, - SolutionPair { - source_config, - target_config, - }, - ) - }, - }] -} - -#[cfg(test)] -#[path = "../unit_tests/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs"] -mod tests; From 79eae42c94937c7b46cae7a1918a90d8f3009410 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Mon, 30 Mar 2026 01:21:18 +0800 Subject: [PATCH 5/5] fix: rename --qde-rhs to --coeff-c for consistency All three QDE parameters now use the coeff- prefix: --coeff-a, --coeff-b, --coeff-c. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/reductions.typ | 2 +- problemreductions-cli/src/cli.rs | 6 +++--- problemreductions-cli/src/commands/create.rs | 18 +++++++++--------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 4fab20df5..9fb6e5438 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -3278,7 +3278,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], #problem-def("QuadraticDiophantineEquations")[ Given positive integers $a$, $b$, $c$, determine whether there exist positive integers $x$, $y$ such that $a x^2 + b y = c$. ][ - Quadratic Diophantine equations of the form $a x^2 + b y = c$ form one of the simplest families of mixed-degree Diophantine problems. The variable $y$ is entirely determined by $x$ via $y = (c - a x^2) slash b$, so the decision problem reduces to checking whether any $x in {1, dots, floor(sqrt(c slash a))}$ yields a positive integer $y$. This can be done in $O(sqrt(c))$ time by trial#footnote[No algorithm improving on brute-force trial of all candidate $x$ values is known; the registered complexity `sqrt(c_val)` reflects this direct enumeration bound.]. + Quadratic Diophantine equations of the form $a x^2 + b y = c$ form one of the simplest families of mixed-degree Diophantine problems. The variable $y$ is entirely determined by $x$ via $y = (c - a x^2) slash b$, so the decision problem reduces to checking whether any $x in {1, dots, floor(sqrt(c slash a))}$ yields a positive integer $y$. This can be done in $O(sqrt(c))$ time by trial#footnote[No algorithm improving on brute-force trial of all candidate $x$ values is known; the registered complexity `sqrt(c)` reflects this direct enumeration bound.]. *Example.* Let $a = #a$, $b = #b$, $c = #c$. Then $x$ ranges over $1, dots, #max-x$: diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index db72a56d2..35484abd5 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -249,7 +249,7 @@ Flags by problem type: ProductionPlanning --num-periods, --demands, --capacities, --setup-costs, --production-costs, --inventory-costs, --cost-bound SubsetSum --sizes, --target ThreePartition --sizes, --bound - QuadraticDiophantineEquations --coeff-a, --coeff-b, --qde-rhs + QuadraticDiophantineEquations --coeff-a, --coeff-b, --coeff-c SumOfSquaresPartition --sizes, --num-groups ExpectedRetrievalCost --probabilities, --num-sectors PaintShop --sequence @@ -761,9 +761,9 @@ pub struct CreateArgs { /// Coefficient b for QuadraticDiophantineEquations (coefficient of y) #[arg(long)] pub coeff_b: Option, - /// Right-hand side c for QuadraticDiophantineEquations + /// Constant c for QuadraticDiophantineEquations (right-hand side of ax² + by = c) #[arg(long)] - pub qde_rhs: Option, + pub coeff_c: Option, } #[derive(clap::Args)] diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 5256d2b52..fb22dba6e 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -197,7 +197,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.coeff_a.is_none() && args.coeff_b.is_none() && args.rhs.is_none() - && args.qde_rhs.is_none() + && args.coeff_c.is_none() && args.required_columns.is_none() } @@ -732,7 +732,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "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", - "QuadraticDiophantineEquations" => "--coeff-a 3 --coeff-b 5 --qde-rhs 53", + "QuadraticDiophantineEquations" => "--coeff-a 3 --coeff-b 5 --coeff-c 53", "BoyceCoddNormalFormViolation" => { "--n 6 --sets \"0,1:2;2:3;3,4:5\" --target 0,1,2,3,4,5" } @@ -2427,20 +2427,20 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { "QuadraticDiophantineEquations" => { let a = args.coeff_a.ok_or_else(|| { anyhow::anyhow!( - "QuadraticDiophantineEquations requires --coeff-a, --coeff-b, and --qde-rhs\n\n\ - Usage: pred create QuadraticDiophantineEquations --coeff-a 3 --coeff-b 5 --qde-rhs 53" + "QuadraticDiophantineEquations requires --coeff-a, --coeff-b, and --coeff-c\n\n\ + Usage: pred create QuadraticDiophantineEquations --coeff-a 3 --coeff-b 5 --coeff-c 53" ) })?; let b = args.coeff_b.ok_or_else(|| { anyhow::anyhow!( "QuadraticDiophantineEquations requires --coeff-b\n\n\ - Usage: pred create QuadraticDiophantineEquations --coeff-a 3 --coeff-b 5 --qde-rhs 53" + Usage: pred create QuadraticDiophantineEquations --coeff-a 3 --coeff-b 5 --coeff-c 53" ) })?; - let c = args.qde_rhs.ok_or_else(|| { + let c = args.coeff_c.ok_or_else(|| { anyhow::anyhow!( - "QuadraticDiophantineEquations requires --qde-rhs\n\n\ - Usage: pred create QuadraticDiophantineEquations --coeff-a 3 --coeff-b 5 --qde-rhs 53" + "QuadraticDiophantineEquations requires --coeff-c\n\n\ + Usage: pred create QuadraticDiophantineEquations --coeff-a 3 --coeff-b 5 --coeff-c 53" ) })?; ( @@ -7737,7 +7737,7 @@ mod tests { coeff_a: None, coeff_b: None, rhs: None, - qde_rhs: None, + coeff_c: None, required_columns: None, } }