Skip to content

Commit 904ffcb

Browse files
committed
Add 3-SAT to quadratic diophantine equations reduction
1 parent c828d9e commit 904ffcb

5 files changed

Lines changed: 440 additions & 138 deletions

File tree

src/models/algebraic/quadratic_diophantine_equations.rs

Lines changed: 193 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
//! Quadratic Diophantine Equations problem implementation.
22
//!
3-
//! Given positive integers a, b, c, determine whether there exist
4-
//! positive integers x, y such that ax² + by = c.
3+
//! Given positive integers `a`, `b`, and `c`, determine whether there exist
4+
//! positive integers `x`, `y` such that `a x^2 + b y = c`.
5+
//!
6+
//! The witness integer `x` is encoded as a little-endian binary vector so the
7+
//! model can represent large reductions without fixed-width overflow.
58
69
use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry};
710
use crate::traits::Problem;
811
use crate::types::Or;
12+
use num_bigint::{BigUint, ToBigUint};
13+
use num_traits::{One, Zero};
914
use serde::de::Error as _;
1015
use serde::{Deserialize, Deserializer, Serialize};
1116

@@ -16,147 +21,245 @@ inventory::submit! {
1621
aliases: &["QDE"],
1722
dimensions: &[],
1823
module_path: module_path!(),
19-
description: "Decide whether ax² + by = c has a solution in positive integers x, y",
24+
description: "Decide whether ax^2 + by = c has a solution in positive integers x, y",
2025
fields: &[
21-
FieldInfo { name: "a", type_name: "u64", description: "Coefficient of x²" },
22-
FieldInfo { name: "b", type_name: "u64", description: "Coefficient of y" },
23-
FieldInfo { name: "c", type_name: "u64", description: "Right-hand side constant" },
26+
FieldInfo { name: "a", type_name: "BigUint", description: "Coefficient of x^2" },
27+
FieldInfo { name: "b", type_name: "BigUint", description: "Coefficient of y" },
28+
FieldInfo { name: "c", type_name: "BigUint", description: "Right-hand side constant" },
2429
],
2530
}
2631
}
2732

2833
inventory::submit! {
2934
ProblemSizeFieldEntry {
3035
name: "QuadraticDiophantineEquations",
31-
fields: &["c"],
36+
fields: &["bit_length_a", "bit_length_b", "bit_length_c"],
3237
}
3338
}
3439

3540
/// Quadratic Diophantine Equations problem.
3641
///
37-
/// Given positive integers a, b, c, determine whether there exist
38-
/// positive integers x, y such that ax² + by = c.
39-
///
40-
/// # Example
42+
/// Given positive integers `a`, `b`, and `c`, determine whether there exist
43+
/// positive integers `x`, `y` such that `a x^2 + b y = c`.
4144
///
42-
/// ```
43-
/// use problemreductions::models::algebraic::QuadraticDiophantineEquations;
44-
/// use problemreductions::{Problem, Solver, BruteForce};
45-
///
46-
/// // a=3, b=5, c=53: x=1 gives y=10, x=4 gives y=1
47-
/// let problem = QuadraticDiophantineEquations::new(3, 5, 53);
48-
/// let solver = BruteForce::new();
49-
/// let witness = solver.find_witness(&problem);
50-
/// assert!(witness.is_some());
51-
/// ```
45+
/// The configuration encodes `x` in little-endian binary:
46+
/// `config[i] in {0,1}` is the coefficient of `2^i`.
5247
#[derive(Debug, Clone, Serialize)]
5348
pub struct QuadraticDiophantineEquations {
54-
/// Coefficient of x².
55-
a: u64,
49+
/// Coefficient of x^2.
50+
#[serde(with = "crate::models::misc::biguint_serde::decimal_biguint")]
51+
a: BigUint,
5652
/// Coefficient of y.
57-
b: u64,
53+
#[serde(with = "crate::models::misc::biguint_serde::decimal_biguint")]
54+
b: BigUint,
5855
/// Right-hand side constant.
59-
c: u64,
56+
#[serde(with = "crate::models::misc::biguint_serde::decimal_biguint")]
57+
c: BigUint,
58+
}
59+
60+
fn bit_length(value: &BigUint) -> usize {
61+
if value.is_zero() {
62+
0
63+
} else {
64+
let bytes = value.to_bytes_be();
65+
let msb = *bytes.first().expect("nonzero BigUint has bytes");
66+
8 * (bytes.len() - 1) + (8 - msb.leading_zeros() as usize)
67+
}
6068
}
6169

6270
impl QuadraticDiophantineEquations {
63-
fn validate_inputs(a: u64, b: u64, c: u64) -> Result<(), String> {
64-
if a == 0 {
71+
fn validate_inputs(a: &BigUint, b: &BigUint, c: &BigUint) -> Result<(), String> {
72+
if a.is_zero() {
6573
return Err("Coefficient a must be positive".to_string());
6674
}
67-
if b == 0 {
75+
if b.is_zero() {
6876
return Err("Coefficient b must be positive".to_string());
6977
}
70-
if c == 0 {
78+
if c.is_zero() {
7179
return Err("Right-hand side c must be positive".to_string());
7280
}
7381
Ok(())
7482
}
7583

84+
fn isqrt(n: &BigUint) -> BigUint {
85+
if n.is_zero() {
86+
return BigUint::zero();
87+
}
88+
89+
let mut low = BigUint::zero();
90+
let mut high = BigUint::one() << ((bit_length(n) + 1) / 2);
91+
92+
while low < high {
93+
let mid = (&low + &high + BigUint::one()) >> 1usize;
94+
if &mid * &mid <= *n {
95+
low = mid;
96+
} else {
97+
high = mid - BigUint::one();
98+
}
99+
}
100+
101+
low
102+
}
103+
76104
/// Create a new QuadraticDiophantineEquations instance, returning an error
77105
/// instead of panicking when inputs are invalid.
78-
pub fn try_new(a: u64, b: u64, c: u64) -> Result<Self, String> {
79-
Self::validate_inputs(a, b, c)?;
106+
pub fn try_new<A, B, C>(a: A, b: B, c: C) -> Result<Self, String>
107+
where
108+
A: ToBigUint,
109+
B: ToBigUint,
110+
C: ToBigUint,
111+
{
112+
let a = a
113+
.to_biguint()
114+
.ok_or_else(|| "Coefficient a must be nonnegative".to_string())?;
115+
let b = b
116+
.to_biguint()
117+
.ok_or_else(|| "Coefficient b must be nonnegative".to_string())?;
118+
let c = c
119+
.to_biguint()
120+
.ok_or_else(|| "Right-hand side c must be nonnegative".to_string())?;
121+
Self::validate_inputs(&a, &b, &c)?;
80122
Ok(Self { a, b, c })
81123
}
82124

83125
/// Create a new QuadraticDiophantineEquations instance.
84126
///
85127
/// # Panics
86128
///
87-
/// Panics if any of a, b, c is zero.
88-
pub fn new(a: u64, b: u64, c: u64) -> Self {
129+
/// Panics if any of `a`, `b`, `c` is zero.
130+
pub fn new<A, B, C>(a: A, b: B, c: C) -> Self
131+
where
132+
A: ToBigUint,
133+
B: ToBigUint,
134+
C: ToBigUint,
135+
{
89136
Self::try_new(a, b, c).unwrap_or_else(|msg| panic!("{msg}"))
90137
}
91138

92-
/// Get the coefficient a (coefficient of x²).
93-
pub fn a(&self) -> u64 {
94-
self.a
139+
/// Get the coefficient a (coefficient of x^2).
140+
pub fn a(&self) -> &BigUint {
141+
&self.a
95142
}
96143

97144
/// Get the coefficient b (coefficient of y).
98-
pub fn b(&self) -> u64 {
99-
self.b
145+
pub fn b(&self) -> &BigUint {
146+
&self.b
100147
}
101148

102149
/// Get the right-hand side constant c.
103-
pub fn c(&self) -> u64 {
104-
self.c
150+
pub fn c(&self) -> &BigUint {
151+
&self.c
152+
}
153+
154+
/// Number of bits needed to encode the coefficient a.
155+
pub fn bit_length_a(&self) -> usize {
156+
bit_length(&self.a)
157+
}
158+
159+
/// Number of bits needed to encode the coefficient b.
160+
pub fn bit_length_b(&self) -> usize {
161+
bit_length(&self.b)
105162
}
106163

107-
/// Compute the integer square root of n (floor(sqrt(n))).
108-
fn isqrt(n: u64) -> u64 {
109-
if n == 0 {
110-
return 0;
164+
/// Number of bits needed to encode the constant c.
165+
pub fn bit_length_c(&self) -> usize {
166+
bit_length(&self.c)
167+
}
168+
169+
fn max_x(&self) -> BigUint {
170+
if self.c < self.a {
171+
return BigUint::zero();
111172
}
112-
let mut x = (n as f64).sqrt() as u64;
113-
// Correct for floating-point imprecision.
114-
while x.saturating_mul(x) > n {
115-
x -= 1;
173+
Self::isqrt(&(&self.c / &self.a))
174+
}
175+
176+
fn witness_bit_length(&self) -> usize {
177+
let max_x = self.max_x();
178+
if max_x.is_zero() {
179+
0
180+
} else {
181+
bit_length(&max_x)
182+
}
183+
}
184+
185+
/// Encode a candidate witness integer `x` as a little-endian binary configuration.
186+
pub fn encode_witness(&self, x: &BigUint) -> Option<Vec<usize>> {
187+
if x.is_zero() || x > &self.max_x() {
188+
return None;
116189
}
117-
while (x + 1).saturating_mul(x + 1) <= n {
118-
x += 1;
190+
191+
let num_bits = self.witness_bit_length();
192+
let mut remaining = x.clone();
193+
let mut config = Vec::with_capacity(num_bits);
194+
195+
for _ in 0..num_bits {
196+
config.push(if (&remaining & BigUint::one()).is_zero() {
197+
0
198+
} else {
199+
1
200+
});
201+
remaining >>= 1usize;
202+
}
203+
204+
if remaining.is_zero() {
205+
Some(config)
206+
} else {
207+
None
119208
}
120-
x
121209
}
122210

123-
/// Compute the maximum value of x (floor(sqrt(c/a))).
124-
/// Returns 0 if c < a (no positive x is possible since x >= 1 requires a*1 <= c).
125-
fn max_x(&self) -> u64 {
126-
if self.c < self.a {
127-
return 0;
211+
/// Decode a little-endian binary configuration into its candidate witness `x`.
212+
pub fn decode_witness(&self, config: &[usize]) -> Option<BigUint> {
213+
if config.len() != self.witness_bit_length() || config.iter().any(|&digit| digit > 1) {
214+
return None;
128215
}
129-
Self::isqrt(self.c / self.a)
216+
217+
let mut value = BigUint::zero();
218+
let mut weight = BigUint::one();
219+
for &digit in config {
220+
if digit == 1 {
221+
value += &weight;
222+
}
223+
weight <<= 1usize;
224+
}
225+
Some(value)
130226
}
131227

132228
/// Check whether a given x yields a valid positive integer y.
133229
///
134-
/// Returns Some(y) if y is a positive integer, None otherwise.
135-
pub fn check_x(&self, x: u64) -> Option<u64> {
136-
if x == 0 {
230+
/// Returns `Some(y)` if `y` is a positive integer, `None` otherwise.
231+
pub fn check_x(&self, x: &BigUint) -> Option<BigUint> {
232+
if x.is_zero() {
137233
return None;
138234
}
139-
let ax2 = self.a.checked_mul(x)?.checked_mul(x)?;
235+
236+
let ax2 = &self.a * x * x;
140237
if ax2 >= self.c {
141238
return None;
142239
}
143-
let remainder = self.c - ax2;
144-
if !remainder.is_multiple_of(self.b) {
240+
241+
let remainder = &self.c - ax2;
242+
if (&remainder % &self.b) != BigUint::zero() {
145243
return None;
146244
}
147-
let y = remainder / self.b;
148-
if y == 0 {
245+
246+
let y = remainder / &self.b;
247+
if y.is_zero() {
149248
return None;
150249
}
250+
151251
Some(y)
152252
}
153253
}
154254

155255
#[derive(Deserialize)]
156256
struct QuadraticDiophantineEquationsData {
157-
a: u64,
158-
b: u64,
159-
c: u64,
257+
#[serde(with = "crate::models::misc::biguint_serde::decimal_biguint")]
258+
a: BigUint,
259+
#[serde(with = "crate::models::misc::biguint_serde::decimal_biguint")]
260+
b: BigUint,
261+
#[serde(with = "crate::models::misc::biguint_serde::decimal_biguint")]
262+
c: BigUint,
160263
}
161264

162265
impl<'de> Deserialize<'de> for QuadraticDiophantineEquations {
@@ -178,38 +281,42 @@ impl Problem for QuadraticDiophantineEquations {
178281
}
179282

180283
fn dims(&self) -> Vec<usize> {
181-
let max_x = self.max_x() as usize;
182-
if max_x == 0 {
183-
// No valid x exists; return empty config space.
184-
return vec![0];
284+
let num_bits = self.witness_bit_length();
285+
if num_bits == 0 {
286+
Vec::new()
287+
} else {
288+
vec![2; num_bits]
185289
}
186-
// One variable: x ranges over {1, ..., max_x}.
187-
// config[0] in {0, ..., max_x - 1} maps to x = config[0] + 1.
188-
vec![max_x]
189290
}
190291

191292
fn evaluate(&self, config: &[usize]) -> Or {
192-
Or({
193-
if config.len() != 1 {
194-
return Or(false);
195-
}
196-
let x = (config[0] as u64) + 1; // 1-indexed
197-
self.check_x(x).is_some()
198-
})
293+
let Some(x) = self.decode_witness(config) else {
294+
return Or(false);
295+
};
296+
297+
if x.is_zero() || x > self.max_x() {
298+
return Or(false);
299+
}
300+
301+
Or(self.check_x(&x).is_some())
199302
}
200303
}
201304

202305
crate::declare_variants! {
203-
default QuadraticDiophantineEquations => "sqrt(c)",
306+
default QuadraticDiophantineEquations => "2^bit_length_c",
204307
}
205308

206309
#[cfg(feature = "example-db")]
207310
pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
311+
let instance = QuadraticDiophantineEquations::new(3u32, 5u32, 53u32);
312+
let optimal_config = instance
313+
.encode_witness(&BigUint::from(1u32))
314+
.expect("x=1 should be a valid canonical witness");
315+
208316
vec![crate::example_db::specs::ModelExampleSpec {
209317
id: "quadratic_diophantine_equations",
210-
instance: Box::new(QuadraticDiophantineEquations::new(3, 5, 53)),
211-
// x=1 (config[0]=0) gives y=10: 3*1 + 5*10 = 53
212-
optimal_config: vec![0],
318+
instance: Box::new(instance),
319+
optimal_config,
213320
optimal_value: serde_json::json!(true),
214321
}]
215322
}

0 commit comments

Comments
 (0)